2
7
Fork
You've already forked feps
0

[FEP-fe34]: Partially embedded objects #21

Open
opened 2026年05月19日 14:28:46 +02:00 by silverpill · 4 comments

FEP-fe34 states:

In some cases, an embedded object can be trusted when its wrapping object is trusted:

  • An embedded object has the same origin and the same owner as the wrapping object.

However, if an embedded object is partial, the consumer may inadvertently cache it, replacing the full object.

For example, this may happen with FEP-171b Add activities where target is a partial OrderedCollection object:

{
 ...
 "target": {
 "type": "OrderedCollection",
 "id": "https://alice.example/contexts/1/history",
 "attributedTo": "https://alice.example/actors/1"
 }
}

Related discussion: https://socialhub.activitypub.rocks/t/partially-embedded-objects/4450

Possible solutions and their downsides

1. Never trust embedded objects without an integrity proof

Too late: embedding is used by many implementations. It's a well-established practice for reducing the network load.

2. Use a special property or type to mark partial objects

The property could be named partial, as suggested in https://socialhub.activitypub.rocks/t/partially-embedded-objects/4450 (or $partial, or partialObject)

Implementations that are not aware of this mechanism still may cache the partial object. No implementation experience.

3. Identify partial objects using fragment IDs

IDs of partial objects would need to have special fragment ID, such as #fragment

Resolution of such ID leads to a fragment, not full object. Implementers would need to treat this as a special case.

4. Use a special "reference" object instead of a partial object

This solution was suggested in https://socialhub.activitypub.rocks/t/partially-embedded-objects/4450/7

It can't be used in situations where a specific object is expected. For example, it can't be used as an object of Undo(Follow) activity.

5. Don't allow partial objects

Some implementations already use partial embeddings.

It is a good way to simplify processing of some activities such as Undo and Add. An alternative to partial embedding is to insert a property such as objectType and targetType (instead of embedding "object": {"type": "..."}).

Conclusion

My preferred solution is 2 because it is backward compatible and shifts the burden to implementations that cache all objects they encounter (they are few, if exist; probably variations of a generic AP server).

FEP-fe34 states: > In some cases, an embedded object can be trusted when its wrapping object is trusted: > - An embedded object has the same origin and the same owner as the wrapping object. However, if an embedded object is partial, the consumer may inadvertently cache it, replacing the full object. For example, this may happen with [FEP-171b](https://codeberg.org/fediverse/fep/src/branch/main/fep/171b/fep-171b.md#examples) `Add` activities where `target` is a partial `OrderedCollection` object: ``` { ... "target": { "type": "OrderedCollection", "id": "https://alice.example/contexts/1/history", "attributedTo": "https://alice.example/actors/1" } } ``` Related discussion: https://socialhub.activitypub.rocks/t/partially-embedded-objects/4450 ## Possible solutions and their downsides **1. Never trust embedded objects without an integrity proof** Too late: embedding is used by many implementations. It's a well-established practice for reducing the network load. **2. Use a special property or type to mark partial objects** The property could be named `partial`, as suggested in https://socialhub.activitypub.rocks/t/partially-embedded-objects/4450 (or `$partial`, or `partialObject`) Implementations that are not aware of this mechanism still may cache the partial object. No implementation experience. **3. Identify partial objects using fragment IDs** IDs of partial objects would need to have special fragment ID, such as `#fragment` Resolution of such ID leads to a fragment, not full object. Implementers would need to treat this as a special case. **4. Use a special "reference" object instead of a partial object** This solution was suggested in https://socialhub.activitypub.rocks/t/partially-embedded-objects/4450/7 It can't be used in situations where a specific object is expected. For example, it can't be used as an `object` of `Undo(Follow)` activity. **5. Don't allow partial objects** Some implementations already use partial embeddings. It is a good way to simplify processing of some activities such as `Undo` and `Add`. An alternative to partial embedding is to insert a property such as `objectType` and `targetType` (instead of embedding `"object": {"type": "..."}`). ## Conclusion My preferred solution is 2 because it is backward compatible and shifts the burden to implementations that cache all objects they encounter (they are few, if exist; probably variations of a [generic AP server](https://codeberg.org/fediverse/fep/src/branch/main/fep/fc48/fep-fc48.md)).

However, if an embedded object is partial, the consumer may inadvertently cache it, replacing the full object.

Consumers should be clear about what they are caching. An HTTP cache can be used to store HTTP resource representations obtained via HTTP GET. Those representations can describe an object. The only time you should be "caching objects" is indirectly via caching the HTTP resource representations that describe them.

Options 3 and 4 are not valid logically because they change the statements to no longer be about the actual subject anymore.

Option 5 seems unworkable as pointed out. I think the alternative mentioned has the same issue as 3 and 4, though.

For Option 2, all object descriptions are always "partial", so a "partial" property doesn't make sense.

Option 1 needs a qualifier -- any embedded statements are made by the host of the document, usually (possibly on behalf of an author which can be a different entity). When you have a proof whose proofPurpose is assertionMethod, you can know who is asserting the statements. When you don't have a proof, you don't know who is asserting the statements. Making assertions about other people's identifiers is where the question of authority arises. So it's not that you should "never trust embedded objects without a proof", it's more like "recognize the trust boundary around certain statements".

Assertions and trust

For example, given a resource representation like this:

HTTP/1.1 200 OK
Content-Type: application/activity+json
Content-Location: https://bob.example/add
Host: bob.example
{
 "id": "https://bob.example/add",
 "type": "Add",
 "target": {
 "type": "OrderedCollection",
 "id": "https://alice.example/contexts/1/history",
 "attributedTo": "https://alice.example/actors/1"
 }
}

You have the following statements made by bob.example (possibly on behalf of another entity):

  • https://bob.example/add is an Add activity.
  • https://bob.example/add is targeting https://alice.example/contexts/1/history.
  • https://alice.example/contexts/1/history is an OrderedCollection.
  • https://alice.example/contexts/1/history is attributed to https://alice.example/actors/1.

Via TLS, you know that you are currently speaking to bob.example. You can then apply a trust model like:

  • The first 2 statements are made about resources whose URIs contain an authority component of bob.example. I trust bob.example to make statements about resources on bob.example. Therefore, I trust the first 2 statements.
  • The latter 2 statements are made about resources whose URIs contain an authority component of alice.example. I don't trust bob.example to make statements about resources on alice.example. Therefore, I do not trust the latter 2 statements.

Dropping the untrusted statements yields the following information:

{
 "id": "https://bob.example/add",
 "type": "Add",
 "target": "https://alice.example/contexts/1/history"
}

But this depends on the trust model that says only origins are allowed to make statements about resources on that origin. With a different trust model, you could reach a different conclusion. VC proofs are a good example of this, because they let you specify an entity and specify a purpose of the proof, which combined make it possible to say which entity is asserting the statements, and the entity is identified as a https: URI and not as an HTTP Host.

Relation to other issues

Consider an alternative based on reputation: bob.example is trusted to make statements about resources on alice.example because bob.example has established a reputation of not lying.

This is in fact something proposed for link previews, which are another infamous source of additional load associated with the current fediverse trust model. It leads to the "thundering herd" problem because link previews are untrusted by default, and are always subject to fetching for verification purposes by basically every instance.

Or, a different alternative: it's okay for anyone to say anything about anything, because it's an open world. Something being untrusted doesn't mean it's untrue; it just means you haven't accepted it personally.

Basically, if you're going to say that embedded descriptions are untrusted, I would extend this to say that embedded descriptions should never update a "cache" of an object. Only a direct fetch of the containing document should do that. Or a delivery of a Create/Update/Delete activity, if you're using activities to do CRUD.

> However, if an embedded object is partial, the consumer may inadvertently cache it, replacing the full object. Consumers should be clear about what they are caching. An HTTP cache can be used to store HTTP resource representations obtained via HTTP GET. Those representations can describe an object. The only time you should be "caching objects" is indirectly via caching the HTTP resource representations that describe them. Options 3 and 4 are not valid logically because they change the statements to no longer be about the actual subject anymore. Option 5 seems unworkable as pointed out. I think the alternative mentioned has the same issue as 3 and 4, though. For Option 2, all object descriptions are always "partial", so a "partial" property doesn't make sense. Option 1 needs a qualifier -- any embedded statements are made by the host of the document, usually (possibly on behalf of an author which can be a different entity). When you have a proof whose proofPurpose is assertionMethod, you can know who is asserting the statements. When you don't have a proof, you don't know who is asserting the statements. Making assertions about other people's identifiers is where the question of authority arises. So it's not that you should "never trust embedded objects without a proof", it's more like "recognize the trust boundary around certain statements". ## Assertions and trust For example, given a resource representation like this: ```http HTTP/1.1 200 OK Content-Type: application/activity+json Content-Location: https://bob.example/add Host: bob.example { "id": "https://bob.example/add", "type": "Add", "target": { "type": "OrderedCollection", "id": "https://alice.example/contexts/1/history", "attributedTo": "https://alice.example/actors/1" } } ``` You have the following statements made by bob.example (possibly on behalf of another entity): - `https://bob.example/add` is an Add activity. - `https://bob.example/add` is targeting `https://alice.example/contexts/1/history`. - `https://alice.example/contexts/1/history` is an OrderedCollection. - `https://alice.example/contexts/1/history` is attributed to `https://alice.example/actors/1`. Via TLS, you know that you are currently speaking to `bob.example`. You can then apply a trust model like: - The first 2 statements are made about resources whose URIs contain an authority component of bob.example. I trust bob.example to make statements about resources on bob.example. Therefore, I trust the first 2 statements. - The latter 2 statements are made about resources whose URIs contain an authority component of alice.example. I don't trust bob.example to make statements about resources on alice.example. Therefore, I do not trust the latter 2 statements. Dropping the untrusted statements yields the following information: ```json { "id": "https://bob.example/add", "type": "Add", "target": "https://alice.example/contexts/1/history" } ``` But this depends on the trust model that says only origins are allowed to make statements about resources on that origin. With a different trust model, you could reach a different conclusion. VC proofs are a good example of this, because they let you specify an entity and specify a purpose of the proof, which combined make it possible to say which entity is asserting the statements, and the entity is identified as a https: URI and not as an HTTP Host. ## Relation to other issues Consider an alternative based on reputation: bob.example is trusted to make statements about resources on alice.example because bob.example has established a reputation of not lying. This is in fact something proposed for link previews, which are another infamous source of additional load associated with the current fediverse trust model. It leads to the "thundering herd" problem because link previews are untrusted by default, and are always subject to fetching for verification purposes by basically every instance. Or, a different alternative: it's okay for anyone to say anything about anything, because it's an open world. Something being untrusted doesn't mean it's untrue; it just means you haven't accepted it personally. Basically, if you're going to say that embedded descriptions are untrusted, I would extend this to say that embedded descriptions should never update a "cache" of an object. Only a direct fetch of the containing document should do that. Or a delivery of a Create/Update/Delete activity, if you're using activities to do CRUD.
Author
Owner
Copy link

It sounds like you're in favor of option 1. I have no problem with that option, but I am probably a minority. Also, there are two special cases where it is difficult to not trust an embedded object: Create and Update. In the case of Create, that would mean losing the performance improvements provided by HTTP signatures.

For Option 2, all object descriptions are always "partial", so a "partial" property doesn't make sense.

No, not all ActivityPub objects are "partial" (in the sense of "incomplete"). Top-level objects are complete by default.

Are you saying that the property name should be different?

It sounds like you're in favor of option 1. I have no problem with that option, but I am probably a minority. Also, there are two special cases where it is difficult to not trust an embedded object: `Create` and `Update`. In the case of `Create`, that would mean losing the performance improvements provided by HTTP signatures. > For Option 2, all object descriptions are always "partial", so a "partial" property doesn't make sense. No, not all ActivityPub objects are "partial" (in the sense of "incomplete"). Top-level objects are complete by default. Are you saying that the property name should be different?

Not exactly option 1, but a variant -- cache HTTP resources, not objects.

Create and Update can be treated normally or they can be special cased. If you don't trust the embedded statements, then it is valid to treat it as a cache invalidation and just refetch the object.id. If you do trust them, then you can choose to do CRUD using the embedded statements.

It heavily depends on your processing model how to handle those embedded statements. I would say if information is embedded, it should be rendered without fetching any additional information. It might be minimal information, but fetching additional information can change the meaning of the document. For example, an activity like:

{
 "actor": {
 "id": "alice",
 "name": "Alice",
 "icon": ...
 },
 // some activity
}

Say you fetch alice, and then get conflicting information, like a different name and/or different icon. That doesn't make the embedded information wrong -- perhaps it was true at some point and is simply outdated now.

  • If you trust the embedded information, you render the "old" information as-is, effectively turning it into a snapshot of when the activity was published.
  • If you don't trust it, you can ignore everything except the actor.id and fill in your own information from cache.
    • If you don't have alice's containing document cached, you can fetch it.

What I'm trying to say is that embedded descriptions inherit the temporal and trust concerns of their parent document. Even top-level objects can vary by authorization level, so someone else can fetch the same object and get more information than you. That's what makes them "always partial". It's not strictly about partial vs complete, it's more about whether you can trust the information or whether you can fetch more information (from your own cache if need be, assuming you trust that cache).

Not exactly option 1, but a variant -- cache HTTP resources, not objects. Create and Update can be treated normally or they can be special cased. If you don't trust the embedded statements, then it is valid to treat it as a cache invalidation and just refetch the object.id. If you do trust them, then you can choose to do CRUD using the embedded statements. It heavily depends on your processing model how to handle those embedded statements. I would say if information is embedded, it should be rendered without fetching any additional information. It might be minimal information, but fetching additional information can change the meaning of the document. For example, an activity like: ```json { "actor": { "id": "alice", "name": "Alice", "icon": ... }, // some activity } ``` Say you fetch alice, and then get conflicting information, like a different name and/or different icon. That doesn't make the embedded information *wrong* -- perhaps it was true at some point and is simply outdated now. - If you trust the embedded information, you render the "old" information as-is, effectively turning it into a snapshot of when the activity was published. - If you don't trust it, you can ignore everything except the actor.id and fill in your own information from cache. - If you don't have alice's containing document cached, you can fetch it. What I'm trying to say is that embedded descriptions inherit the temporal and trust concerns of their parent document. Even top-level objects can vary by authorization level, so someone else can fetch the same object and get more information than you. That's what makes them "always partial". It's not strictly about partial vs complete, it's more about whether you can trust the information or whether you can fetch more information (from your own cache if need be, assuming you trust that cache).
Author
Owner
Copy link

The recommendation to avoid partial objects was added in fediverse/fep#849

I thought that partial objects could be used for type hints, but now I am in favor of custom activities.

The recommendation to avoid partial objects was added in https://codeberg.org/fediverse/fep/pulls/849 I thought that partial objects could be used for type hints, but now I am in favor of custom activities.
Sign in to join this conversation.
No Branch/Tag specified
main
No results found.
Labels
Clear labels
No items
No labels
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
silverpill/feps#21
Reference in a new issue
silverpill/feps
No description provided.
Delete branch "%!s()"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?