Some properties represent special collections, such as:
outbox
(ActivityPub)inbox
(ActivityPub)followers
(ActivityPub)following
(ActivityPub)liked
(ActivityPub)likes
(ActivityPub)shares
(ActivityPub)replies
(FEP-7458)context
(FEP-7888)Verifying that any given object is part of a special collection is usually only possible by resolving that collection and checking its items one-by-one until the current object is found. This can be inefficient to verify. It would be easier if there was an inverse claim for each claim made about an object being part of a special collection. This FEP aims to define some properties that can be used to make those inverse claims.
Given the special collections above, we can map the following claims and inverse claims. The last two inverse claims are particularly salient, since inclusion in replies
and/or context
carries additional semantic meaning and is a socially loaded relation used to establish conversational constructs.
outbox
<A>
’s actor (<A>.actor
) is actor <B>
Inverse claim<B>
’s outbox collection (<B>.outbox
) contains activity <A>
This is not particularly useful to prove.
inbox
<A>
has to
/cc
/audience
including actor <B>
Inverse claim<B>
’s inbox collection (<B>.inbox
) contains activity <A>
This is not particularly useful to prove, and at best can only be implied if <B>
is addressed directly and not through some collection that triggers inbox forwarding.
followers
<B>
’s following collection (<B>.following
) contains actor <A>
<B>
claims that <B>
is following <A>
<B>
claims that <A>
is followed by <B>
<A>
’s followers collection (<A>.followers
) contains actor <B>
<A>
claims that <A>
is followed by <B>
<A>
claims that <B>
is following <A>
This can be verified by showing one of the following:
<A>.followers
includes <B>
as wellattributedTo
is <A>
subject
is <B>
relationship
is IsFollowing
object
is <A>
attributedTo
is <A>
subject
is <A>
relationship
is IsFollowedBy
object
is <B>
actor
is <A>
type
is Accept
object.actor
is <B>
object.type
is Follow
object.object
is <A>
actor
is <A>
type
is Add
object
is <B>
target
is <A>.followers
following
<B>
’s followers collection (<B>.followers
) contains actor <A>
<B>
claims that <B>
is followed by <A>
<B>
claims that <A>
is following <B>
<A>
’s following collection (<A>.following
) contains actor <B>
<A>
claims that <A>
is following <B>
<A>
claims that <B>
is followed by <A>
This can be verified by showing one of the following:
<A>.following
includes <B>
as wellattributedTo
is <A>
subject
is <A>
relationship
is IsFollowing
object
is <B>
attributedTo
is <A>
subject
is <B>
relationship
is IsFollowedBy
object
is <A>
actor
is <B>
type
is Accept
object.actor
is <A>
object.type
is Follow
object.object
is <B>
object
is a trusted activityactor
is <A>
type
is Add
object
is <B>
target
is <A>.following
liked
<A>
’s liked collection (<A>.liked
) contains object <O>
This can be verified by showing a trusted activity exists in <O>.likes
where:
actor
is <A>
type
is Like
object
is <O>
Note that there is an issue that may occur if <A>
issues multiple Like
activities for the same object <O>
, and then issues any Undo Like
activities at a later point in time. The most recent activity will have its side-effects carried out. It is possible for some of these functionally duplicate Like
activites to remain in <O>.likes
even though the object <O>
is no longer in <A>.liked
. See https://github.com/w3c/activitypub/issues/381 for more information.
likes
<O>
’s likes collection (<O>.likes
) contains a Like activity <L>
from actor <A>
If <L>
is not already trusted via some other mechanism, this can be verified by showing that <A>.liked
includes <O>
. Note the duplication issue from the previous section.
The Like activity <L>
MAY result
in an Add
activity <R>
targeting <O>.likes
. This result
can be used by <A>
as proof of the claim, if it can be shown to be a trusted activity.
shares
<O>
’s shares collection (<O>.shares
) contains an Announce activity <S>
from actor <A>
The Announce activity <S>
MAY result
in an Add
activity <R>
targeting <O>.shares
. This result
can be used by <A>
as proof of the claim, if it can be shown to be a trusted activity.
replies
<R>
is inReplyTo
object <O>
<O>
’s replies collection (<O>.replies
) contains object <R>
attributed to actor <A>
The inverse claim can be verified by showing that an activity exists where:
actor
is <O>.attributedTo
type
set includes Add
object
is <R>
target
is <O>.replies
context
<O>
has a context
pointing to context collection <C>
<C>
contains object <O>
The inverse claim can be verified by showing that an activity exists where:
actor
is <O>.context.attributedTo
type
set includes Add
object
is <R>
target
is <O>.context
We can map proofs onto the result
property provided that its subject is an activity and its referent is a trusted activity. We may also define some extension properties for non-activity objects.
replies
collections via a reply stampProvides proof that some object was added to some replies
collection.
On an activity where inReplyTo
has been set, the proof can be verified if all of the following are satisfied:
result.actor
MUST be included in either inReplyTo.actor
or inReplyTo.attributedTo
result.type
MUST include Add
result.object
MUST be equivalent to the current activityresult.target
MUST be equivalent to inReplyTo.replies
On a Create
activity where the object
has inReplyTo
set, the proof can be verified if all of the following are satisfied:
result.actor
MUST be included in either object.inReplyTo.attributedTo
or object.inReplyTo.actor
result.type
MUST include Add
result.object
MUST be equivalent to object
result.target
MUST be equivalent to object.inReplyTo.replies
On a non-activity Object where inReplyTo
has been set, we define the extension property inReplyToProof
since the use of result
is invalid on non-Activity types. The proof is valid if all of the following are satisfied:
inReplyToProof.actor
MUST be included in either inReplyTo.attributedTo
or inReplyTo.actor
inReplyToProof.type
MUST include Add
inReplyToProof.object
MUST be equivalent to the current objectinReplyToProof.target
MUST be equivalent to inReplyTo.replies
Example of an activity with inReplyTo
set to some activity with a replies
collection:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/some-activity",
"actor": "https://example.com/actors/2",
"type": "Activity",
"object": "https://example.com/some-object",
"inReplyTo": {
"id": "https://example.com/some-other-create",
"actor": "https://example.com/actors/1",
"type": "Create",
"object": "https://example.com/some-other-object",
"content": "I am accepting replies to this activity.",
"replies": "https://example.com/some-other-create/replies"
},
"result": {
"id": "https://example.com/some-proof",
"actor": "https://example.com/actors/1",
"type": "Add",
"object": "https://example.com/some-activity",
"target": "https://example.com/some-other-create/replies",
"attributedTo": "https://example.com/some-activity"
}
}
Example of a Create
activity with object.inReplyTo
set to some non-activity object with a replies
collection:
{
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/fep/0391"],
"id": "https://example.com/create-some-reply",
"actor": "https://example.com/actors/2",
"type": "Create",
"object": {
"id": "https://example.com/some-reply",
"type": "Note",
"attributedTo": "https://example.com/actors/2",
"content": "This is a reply, and I can prove it was added to the replies collection.",
"inReplyTo": {
"id": "https://example.com/some-object",
"type": "Note",
"attributedTo": "https://example.com/actors/1",
"content": "I am accepting replies to this object.",
"replies": "https://example.com/some-object/replies"
},
"inReplyToProof": "https://example.com/some-proof"
},
"result": {
"id": "https://example.com/some-proof",
"actor": "https://example.com/actors/1",
"type": "Add",
"object": "https://example.com/some-reply",
"target": "https://example.com/some-object/replies",
"attributedTo": "https://example.com/create-some-reply"
}
}
context
collections via a context stampProvides proof that some object was added to some context
collection.
On an activity where the context
has been set to a collection, the proof can be verified if all of the following are satisfied:
result.actor
MUST be included in context.attributedTo
result.type
MUST include Add
result.object
MUST be equivalent to the current activityresult.target
MUST be equivalent to context
On a Create
activity where the object
has context
set, the proof can be verified if all of the following are satisfied:
result.actor
MUST be included in object.context.attributedTo
result.type
MUST include Add
result.object
MUST be equivalent to object
result.target
MUST be equivalent to object.context
On a non-activity Object where context
has been set, we define the extension property contextProof
since the use of result
is invalid on non-Activity types. The proof is valid if all of the following are satisfied:
contextProof.actor
MUST be included in context.attributedTo
contextProof.type
MUST include Add
contextProof.object
MUST be equivalent to the current objectcontextProof.target
MUST be equivalent to context
Example of an activity with context
set to some owned collection:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/some-activity",
"actor": "https://example.com/some-actor",
"type": "Activity",
"object": "https://example.com/some-object",
"context": {
"id": "https://example.com/some-context",
"type": "Collection",
"attributedTo": "https://example.com/some-context-moderator"
},
"result": {
"id": "https://example.com/some-proof",
"actor": "https://example.com/some-context-moderator",
"type": "Add",
"object": "https://example.com/some-activity",
"target": "https://example.com/some-context",
"attributedTo": "https://example.com/some-activity"
}
}
Example of a Create
activity with object.context
set to some owned collection:
{
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/fep/0391"],
"id": "https://example.com/create-some-object",
"actor": "https://example.com/some-actor",
"type": "Create",
"object": {
"id": "https://example.com/some-object",
"type": "Note",
"attributedTo": "https://example.com/some-actor",
"content": "This object is part of some context, and I can prove it was added to the context collection.",
"context": {
"id": "https://example.com/some-context",
"type": "Collection",
"attributedTo": "https://example.com/some-context-moderator"
},
"contextProof": "https://example.com/some-proof"
},
"result": {
"id": "https://example.com/some-proof",
"actor": "https://example.com/some-context-moderator",
"type": "Add",
"object": "https://example.com/some-object",
"target": "https://example.com/some-context",
"attributedTo": "https://example.com/create-some-object"
}
}
likes
collections via a like stampProvides proof that the current activity was added to the object.likes
collection.
On a Like
activity where the object
has a likes
collection, the proof can be verified if all of the following are satisfied:
result.actor
MUST be included in object.attributedTo
result.type
MUST include Add
result.object
MUST be equivalent to the current activityresult.target
MUST be equivalent to object.likes
Example:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/some-like",
"summary": "A Like activity, with proof that it was added to the likes collection.",
"actor": "https://example.com/actors/2",
"type": "Like",
"object": {
"id": "https://example.com/some-object",
"type": "Note",
"content": "I am accepting likes of this object.",
"likes": "https://example.com/some-object/likes",
"attributedTo": "https://example.com/actors/1"
},
"result": {
"id": "https://example.com/some-proof",
"actor": "https://example.com/actors/1",
"type": "Add",
"object": "https://example.com/some-like",
"target": "https://example.com/some-object/likes",
"attributedTo": "https://example.com/some-like"
}
}
shares
collections via a share stampProvides proof that the current activity was added to the object.shares
collection.
On an Announce
activity where the object
has a shares
collection, the proof can be verified if all of the following are satisfied:
result.actor
MUST be included in object.attributedTo
result.type
MUST include Add
result.object
MUST be equivalent to the current activityresult.target
MUST be equivalent to object.shares
Example:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/some-announce",
"summary": "An Announce activity, with proof that it was added to the shares collection.",
"actor": "https://example.com/actors/2",
"type": "Announce",
"object": {
"id": "https://example.com/some-object",
"type": "Note",
"content": "I am accepting shares of this object.",
"shares": "https://example.com/some-object/shares",
"attributedTo": "https://example.com/actors/1"
},
"result": {
"id": "https://example.com/some-proof",
"actor": "https://example.com/actors/1",
"type": "Add",
"object": "https://example.com/some-announce",
"target": "https://example.com/some-object/shares",
"attributedTo": "https://example.com/some-announce"
}
}
[!WARNING] Experimental, requires further thought.
[!WARNING] Currently bugged. See https://github.com/w3c/activitystreams/issues/593 for more details.
Provides proof that the current relationship is reciprocally claimed.
Relationships other than following or being a follower may be proved using this property, but the requirements for such a proof are out of scope of this FEP.
We define the extension property relationshipProof
since the use of result
is invalid on non-Activity types. The proof is valid if all of the following are satisfied:
Example that proves a user is following another user:
{
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/fep/0391"],
"id": "https://example.com/some-relationship",
"type": "Relationship",
"attributedTo": "https://example.com/actors/1",
"subject": {
"id": "https://example.com/actors/1",
"following": "https://example.com/actors/1/following"
},
"relationship": "IsFollowing",
"object": {
"id": "https://example.com/actors/2",
"followers": "https://example.com/actors/2/followers"
},
"relationshipProof": [
{
"id": "https://example.com/not-enough-proof",
"actor": "https://example.com/actors/1",
"type": "Add",
"object": "https://example.com/actors/2",
"target": "https://example.com/actors/1/following"
},
{
"id": "https://example.com/proof-by-inverse-relationship",
"type": "Relationship",
"attributedTo": "https://example.com/actors/2",
"subject": "https://example.com/actors/2",
"relationship": "IsFollowedBy",
"object": "https://example.com/actors/1"
},
{
"id": "https://example.com/proof-by-being-added-to-followers",
"actor": "https://example.com/actors/2",
"type": "Add",
"object": "https://example.com/actors/1",
"target": "https://example.com/actors/2/followers"
},
{
"id": "https://example.com/proof-by-having-follow-accepted",
"actor": "https://example.com/actors/2",
"type": "Accept",
"object": {
"actor": "https://example.com/actors/1",
"type": "Follow",
"object": "https://example.com/actors/2"
}
}
]
}
Example that proves a user is followed by another user:
{
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/fep/0391"],
"id": "https://example.com/some-relationship",
"type": "Relationship",
"attributedTo": "https://example.com/actors/1",
"subject": {
"id": "https://example.com/actors/1",
"followers": "https://example.com/actors/1/following"
},
"relationship": "IsFollowedBy",
"object": {
"id": "https://example.com/actors/2",
"following": "https://example.com/actors/2/followers"
},
"relationshipProof": [
{
"id": "https://example.com/not-enough-proof",
"actor": "https://example.com/actors/1",
"type": "Add",
"object": "https://example.com/actors/2",
"target": "https://example.com/actors/1/followers"
},
{
"id": "https://example.com/proof-by-inverse-relationship",
"type": "Relationship",
"attributedTo": "https://example.com/actors/2",
"subject": "https://example.com/actors/2",
"relationship": "IsFollowing",
"object": "https://example.com/actors/1"
},
{
"id": "https://example.com/proof-by-being-added-to-following",
"actor": "https://example.com/actors/2",
"type": "Add",
"object": "https://example.com/actors/1",
"target": "https://example.com/actors/2/following"
},
{
"id": "https://example.com/proof-by-having-follow",
"actor": "https://example.com/actors/2",
"type": "Follow",
"object": "https://example.com/actors/1"
}
]
}
Miscellaneous examples:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/some-follow",
"actor": {
"id": "https://example.com/actors/2",
"following": "https://example.com/actors/2/following"
},
"type": "Follow",
"object": {
"id": "https://example.com/actors/1",
"followers": "https://example.com/actors/1/followers"
},
"result": {
"id": "https://example.com/accept-follow",
"actor": "https://example.com/actor/1",
"type": "Accept",
"object": "https://example.com/some-follow",
"result": [
{
"id": "https://example.com/resulting-add-to-followers",
"actor": "https://example.com/actors/1",
"type": "Add",
"object": "https://example.com/actors/2",
"target": "https://example.com/actors/1/followers",
"attributedTo": "https://example.com/accept-follow"
},
{
"id": "https://example.com/resulting-add-to-following",
"actor": "https://example.com/actors/2",
"type": "Add",
"object": "https://example.com/actors/1",
"target": "https://example.com/actors/2/following",
"attributedTo": "https://example.com/accept-follow"
}
]
}
}
Verifying stamps has an issue with bootstrapping trust. In addition to verifying stamps via logical inference, ActivityPub clients SHOULD also consider the following:
Upon receiving an activity with a side effect of adding something to a special collection, ActivityPub servers SHOULD generate and deliver an Add
activity representing this side-effect. The ActivityPub server MAY require manual action by a user. The resulting stamp activity SHOULD be attributedTo
the activity that caused the side-effect, delivered to its actor
, and additionally SHOULD either be resolvable (to allow direct same-origin checking) or otherwise include a cryptographic proof. If a cryptographic proof is included, the proof SHOULD expire after some reasonable window, beyond which point a new proof should be issued. If the resulting stamp activity is resolvable, it MAY become unresolvable after some time; HTTP caching SHOULD be used to indicate a time-to-live for ActivityPub clients to re-check cached stamps. The time-to-live MAY be used as the duration of the cryptographic proof, if one exists.
The recipient of a stamp SHOULD Update
their object to include the appropriate property for the stamp – result
if it is an activity, inReplyToProof
if it is a non-activity object that declares inReplyTo
, contextProof
if it is a non-activity object that declares context
. [TODO: how to handle relationship proofs?]
Revoking a stamp can be done by:
Update
the stamp with some property to signal that the Add
activity has been undone. [TODO: flesh this out more – how does this work exactly? should it use Remove? Tombstone? Undo? how does this interact with outbox
and the activity history? i’m thinking Update -> Tombstone, or extension like “undoneBy”]This flow is similar for any stamp that uses result
on an activity – Like
, Announce
, Activity with inReplyTo
, Activity with context
.
Actor 2 likes a post by actor 1:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/some-like",
"summary": "Actor 2 liked a Note by actor 1",
"actor": {
"id": "https://example.com/actors/2",
"followers": "https://example.com/actors/2/followers"
},
"type": "Like",
"object": {
"id": "https://example.com/some-object",
"type": "Note",
"content": "I am accepting likes of this object.",
"likes": "https://example.com/some-object/likes",
"attributedTo": {
"id": "https://example.com/actors/1",
"followers": "https://example.com/actors/1/followers"
},
"cc": [
"https://example.com/actors/1/followers",
"as:Public"
],
"audience": "https://example.com/some-object/audience"
},
"to": "https://example.com/actors/1",
"cc": [
"https://example.com/actors/2/followers",
"https://example.com/some-object/audience",
"as:Public"
],
"audience": "https://example.com/some-like/audience"
}
Actor 1 sends a like stamp:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/some-proof",
"summary": "Actor 1 approved a like",
"actor": "https://example.com/actors/1",
"type": "Add",
"object": "https://example.com/some-like",
"target": "https://example.com/some-object/likes",
"attributedTo": "https://example.com/some-like",
"to": "https://example.com/actors/2",
"cc": [
"https://example.com/actors/2/followers",
"https://example.com/some-like/audience",
"as:Public"
]
}
Actor 2 updates their Like activity with proof:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/some-update",
"summary": "Actor 2 updated their Like with proof",
"actor": {
"id": "https://example.com/actors/2",
"followers": "https://example.com/actors/2/followers"
},
"type": "Update",
"object": {
"id": "https://example.com/some-like",
"summary": "Actor 2 liked a Note by actor 1, with proof",
"actor": "https://example.com/actors/2",
"type": "Like",
"object": "https://example.com/some-object",
"to": "https://example.com/actors/1",
"cc": [
"https://example.com/actors/2/followers",
"https://example.com/some-object/audience",
"as:Public"
],
"audience": "https://example.com/some-like/audience",
"result": "https://example.com/some-proof"
},
"cc": [
"https://example.com/actors/2/followers",
"https://example.com/some-like/audience",
"as:Public"
]
}
If actor 1 had provided cryptographic proof, actor 2 would be able to authoritatively serve the stamp until the proof expired. Actor 1 can silently revoke the stamp by making it no longer resolve, or actively revoke the stamp by issuing a revocation activity [TODO: Update with “undoneBy” pointing to a Remove?]
This flow uses inReplyToProof
or contextProof
on the object
of a Create
instead of using only result
on the activity itself.
Some actor creates some object that is part of some context, owned by some context moderator:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/create-some-object",
"actor": "https://example.com/some-actor",
"type": "Create",
"object": {
"id": "https://example.com/some-object",
"type": "Note",
"attributedTo": {
"id": "https://example.com/some-actor",
"followers": "https://example.com/some-actor/followers"
},
"content": "This object is part of some context, and I can prove it was added to the context collection.",
"context": {
"id": "https://example.com/some-context",
"type": "Collection",
"attributedTo": "https://example.com/some-context-moderator",
"audience": "https://example.com/some-context/audience"
},
"to": "https://example.com/some-context-moderator",
"cc": "as:Public"
},
"to": [
"https://example.com/some-context-moderator",
"https://example.com/some-context/audience"
],
"cc": "as:Public",
"audience": "https://example.com/create-some-object/audience"
}
The context moderator adds the object to the context:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/some-proof",
"summary": "A new post was added to the context",
"actor": "https://example.com/some-context-moderator",
"type": "Add",
"object": "https://example.com/some-object",
"target": "https://example.com/some-context",
"attributedTo": "https://example.com/create-some-object",
"cc": [
"https://example.com/some-context/audience",
"https://example.com/some-actor",
"https://example.com/create-some-object/audience",
"as:Public"
]
}
The actor who created the object now can issue two updates: one for the Create activity to add a result
, and one for the object
of that activity to add object.contextProof
:
{
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/fep/0391"],
"id": "https://example.com/update-some-object",
"actor": {
"id": "https://example.com/some-actor",
"followers": "https://example.com/some-actor/followers"
},
"type": "Update",
"object": {
"id": "https://example.com/some-object",
"type": "Note",
"attributedTo": "https://example.com/some-actor",
"content": "This object is part of some context, and I can prove it was added to the context collection.",
"context": "https://example.com/some-context",
"contextProof": "https://example.com/some-proof",
"to": "https://example.com/some-context-moderator",
"cc": "as:Public"
},
"to": [
"https://example.com/some-actor/followers",
"as:Public"
]
}
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/update-create",
"actor": "https://example.com/some-actor",
"type": "Update",
"object": {
"id": "https://example.com/create-some-object",
"actor": "https://example.com/some-actor",
"type": "Create",
"object": "https://example.com/some-object",
"to": "https://example.com/some-context-moderator",
"cc": "as:Public",
"result": "https://example.com/some-proof"
}
}
CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
To the extent possible under law, the authors of this Fediverse Enhancement Proposal have waived all copyright and related or neighboring rights to this work.