Who says objects cannot be created elsewhere?

Original dicussion in Vocata

@stevebate are currently discussing how to behave for a Create activity in an outbox (C2S).

It might have occurred to some by now that I am not exactly a conservative interpreter of ActivityPub (or maybe a very conservative interpreter, depending on from where you look) ;). Therefore, please don’t argue based on current practices in other software, or Mastodon compatibility.

What? Why?

The scenario at hand is the following:

Alice creates ablog post on her static website https://aliceblog.example.com/. The static site generator generates ActivityStreams objects for all new posts, and places them in stable URI locations, let’s say https://aliceblog.example.com/posts/my-new-post.

Being a static site generator, Alice’s website builder can now POST this somewhere, including a ton of inboxes on the fediverse, but with some drawbacks:

  • With a growing number of subscribers, the build will take very long
  • Being a static site, Alice’s website cannot wait for any replies, likes, etc.

Object creation

Thus, Alice decides to use her actor on Bob’s Vocata server, let’s say https://bobsvocata.example.com/users/alice. Her static site generator adds this as attributedTo to the generated objects, resulting in something like:

  "id": "https://aliceblog.example.com/posts/my-new-post",
  "type": "Note",
  "attributedTo": "https://bobsvocata.example.com/users/alice",
  "content": "This is my new blog post!"

Now, Alice’s static site generator POSTs this object, wrapped in a Create activity, to her outbox on Bob’s Vocata server.

  "type": "Create",
  "actor": "https://bobsvocata.example.com/users/alice",
  "cc": "Public",
  "object": {
    "id": "https://aliceblog.example.com/posts/my-new-post",
    "type": "Note",
    "attributedTo": "https://bobsvocata.example.com/users/alice",
    "cc": "Public",
    "content": "This is my new blog post!"

Vcata will remove the embedded object before processing (Alice’s SSG could have sent it as an ID only; for Vocata, this is the same – it will always pull foreign objects from their origin.)

Vocata now does the thing that is up for discussion here: It sees that the created object already has an ID, and that it is a foreign ID. It pulls the object from Alice’s website, and adds the Create activity to her outbox, without any further side effects to the object (because it is not repsonsible for the prefix of the object). It does, however, relay the activity to Alice’s followers

The following parts of the spec touch this field, but remain intact:

Section 6.2 Client to Server Interactions - Create Activity
The Create activity is used when posting a new object. This has the side effect that the object embedded within the Activity (in the object property) is created.

“created” here means we store a local copy. One could argue that this “is created” implies something the server has to actively do, but I see no explicit necessity for this in the wording of the spec.

A mismatch between addressing of the Create activity and its object is likely to lead to confusion. As such, a server SHOULD copy any recipients of the Create activity to its object upon initial distribution, and likewise with copying recipients from the object to the wrapping Create activity.

“SHOULD”. Also, of course, Alice’s SSG can take care of that i nthe first place.

Likes, replies, etc.

Now to make real use of this, Alice wants Bob’s server to handle replies and likes of her post.

Assuming Vocata provides a way to create likes and replies collections for something (this is out of scope here), Alice changes her object from above:

  "type": "Create",
  "actor": "https://bobsvocata.example.com/users/alice",
  "cc": "Public",
  "object": {
    "id": "https://aliceblog.example.com/posts/my-new-post",
    "type": "Note",
    "attributedTo": "https://bobsvocata.example.com/users/alice",
    "cc": "Public",
    "content": "This is my new blog post!",
    "likes": "https://bobsvocata.example.com/collections/4eba371b-0e7c-4d95-a23c-34822e7bee1e",
    "replies": "https://bobsvocata.example.com/colelctions/2d428460-81db-4ca0-99f8-5254a640bb98"

Now, if someone likes Alice’s blog psot, their server will send a Like activity to aatributedTo, which is Alice’s actor on Bob’s server. It receives the activity, and, being responsible for the likes collection of the object, adds the activity to the collection, and to Alice’s inbox.

Alice’s SSG now can connect to her inbox and retrieve likes, shares, etc., and blend them into the statically generated website.

Why not?

So, the “Why?” has been answered above. Now here’s the question for the rest of you: “Why not?”

Obviously, support for this on other servers might be bad, I am entirely aware of that. Other servers might refuse to process a federated Create with an object under another prefix. The question is: They might refuse, but why should they?

As noted above, please argue based on the spec and core ActivityPub mechanisms, not based on prior art.


I’m too lazy to read everything. Requiring the actor = attributedTo of object, the id of the create, the id of the object, to be from the same domain, is the simplest way to verify that the server sending the message has the authority to do so. The general practice of this is called single origin principle.

I think somebody (unaffiliated with amazon) gets to use the handle @aws.com or something on Bluesky, because Bluesky didn’t stick to the simple stuff.

Okay, so, the way I see it: This is not violating the spec, but it is violating a recommended policy of the spec. (It’s also a bit “weird” because it goes against the intention or spirit of the C2S API, but I’ll get into that later.)

Let’s consider a related situation: you want to Update the thing that you created. Per 7.3, S2S Update:

The receiving server MUST take care to be sure that the Update is authorized to modify its object. At minimum, this may be done by ensuring that the Update and its object are of same origin.

So, yeah, there’s nothing technically preventing you from sending a Create for something not technically created by the Server. But it’s going to lead to problems down the line, because there’s no specified permission model within ActivityPub. Some implementers are going to use same-origin, and some implementers are going to check if Create.actor is equal to or included in Create.object.attributedTo, and some implementers are going to do something entirely different. All of these are “right”, and all of these are “wrong”. Arguably, that’s besides the point.

Think of it like this: you are not really “creating” the object. You are instead sending a notification that the object has been created. The intended interaction here is not for aliceblog to wrap the Note in a Create, but for aliceblog to POST the Note directly to the Server, where it will get wrapped in a Create (and both the activity ID and the object ID will be generated by the server, etc).

From a C2S perspective, does bobsvocata have permission to edit the static file over on aliceblog?

When a Create activity is posted, the actor of the activity SHOULD be copied onto the object’s attributedTo field.

Pre-copying this attributedTo doesn’t really solve the issue of ID generation and the associated semantics of the Create-as-an-action vs. the Create-as-a-notification, and how this interacts with the permission model (or lack thereof).

And from an S2S perspective, what happens after a Create activity is received in an inbox is, for the most part, unspecified. As the spec will tell you in 7.2 S2S Create,

Receiving a Create activity in an inbox has surprisingly few side effects

The Server’s responsibility for a C2S Create is, strictly speaking, simply to generate the ID and possibly store the JSON and nothing more. Again, there is a difference between Create of an object vs. sending a Create activity of an already existing object. The former assigns an ID to the object. The latter is just a notification.

Put another way: the Server is basically a CMS. The site generator’s intended role is as a Client for that CMS, and it is more natural to generate the site based on the Server’s data and not anything static on the Client – the authority is passed from the site generator to the Server. Anything done at the SSG level without AP C2S is going to lead to a state desync on the AP Server level.

Assuming you want to pursue the SSG model anyway: you will probably want to consider whether you are okay with the “notification” aspect of the Create activity, where your actor is authoring the Create and not authoring the object. (The object is authored externally to the ActivityPub Server.)

For S2S: a Create is a notification anyway. It’s just that certain implementations will have validations and side-effects of creating some associated resource based on that notification.

So… why not? “Because other servers won’t trust it.” I think the answer is also gonna have to do with whether you are intending to do publishing via ActivityPub, or whether you are intending to do notifications via Linked Data Notifications… but it really comes down to trust and validation. We can even legally author a Create activity of an object attributedTo someone else, no? But who’s going to trust that?

1 Like

Simple, maybe, but…

…that means we must do two things:

  • work with what we have
  • maybe come up with new things (but that is a separate issue for another time)

What we do have is the actor and attributedTo predicates. When the spec talks about same origin, it is written nowhere that this refers to the origin as defines by HTTP. The identity of the sending actor can also be described as the origin.

I can understand that point of view as long as we leave the graph structure of ActivityStream out of the picture. As I wrote in the Vocata issue:

If you look at it from the graph perspective, and all activities being really just graph transformations on a global social graph (for which we need to hold a local subgraph to carry them out), then there is no reason to separate C2S and S2S at all.

As a local actor (what the spec calls C2S), I just happen to be authenticated as an actor with a local prefix, allowing the server to manage my stuff under its namespace – no more, no less. And that I use outbox instead of inbox really is just a coincidence, to make my activities discoverable by others looking at my outbox collection (other than the activity being added to the outbox collection, the outbox endpoint has no special meaning when looking at everything this way).

We can, and any server should record that, and then, as you said, decide to not trust it.

But that is something completely different – in my scenario, activity actor and object attributedTo do match, and I still see no reason why this shouldn’t be enough. I succeeded to place an onject attributed to my actor under a publicly reachable URI, and I succeeded in making my actor’s ActivityPub server send out an activity signed with the private key, so I have fully proven that I control both the object and the actor.

If you consider it, there is nothing really new about this mechanism – it’s basically the foundation the whole security of the whole web is founded on these days, because it is the exact mechanism by which, for example, Let’s Encrypt works. So, while it might not be common practice on the ActivityStreams graph right now, it certainly is a well-established mechanism elsewhere.

As noted in my original post, I am fully aware that this is probably not how many implementations will see it – and that I am not here to find out what existing implementations do.

I think you should really consider using an Add-based model instead of a Create-based model. Add your external AS2 objects to some Collection, and then federate that activity. It neatly sidesteps the authority issue in a lot of ways. In fact, I wish this was more generally used – Create being reserved for the C2S API and possibly RPC-like cross-domain actions… then Add for explicitly maintaining Collections representing a feed, profile, category, etc. The side effects are also much clearer (as they are actually defined) in this case.

1 Like

That sounds very reasonable.

That said, new question: What stops me from using Add to add an object to my outbox?

And, if we think we have established that Create explicitly places an object under dedicated management of the ActivityPub server receiving it: Can we move even further from there, and say that by issuing a Create, we really ultimately give up control of the object, and allow the server to do any transformation on it is considers reasonable? I have, until now, assumed that the server should leave the object entirely intact, apart from copying the audience.

sure, why not? in a way, that’s kind of how i already see it. you described 3 cases in the linked issue on Vocata’s codeberg page:

  • Create with referenced object: sends a notification that the referenced object has been “created” to the audience.
  • Create with embedded object: assigns an id to the embedded object, then delivers the Create to the audience.
    • Create with embedded object that already has an id: there is an argument that the Server should ignore this object.id and assign a new one anyway.

If an Activity is submitted with a value in the id property, servers MUST ignore this and generate a new id for the Activity. Servers MUST return a 201 Created HTTP code, and unless the activity is transient, MUST include the new id in the Location header.

For non-transient objects, the server MUST attach an id to both the wrapping Create and its wrapped Object.

so we see this pattern for “object creation without a wrapping Create”, and it seems natural to apply it to “object creation with a wrapping Create” as well. it’s not stated as a requirement in the spec, but it honestly feels more like an oversight to me. the latter quoted sentence feels like it could be applied more generally to Create in both cases. the only way to get around this is to use a reference and treat the Create as a notification.

in general? the Server, most likely. this depends on the Server implementation, but there could be an argument made that Clients should not be able to manage “special collections” like inbox, outbox, followers, following, and possibly a few others like replies, shares, likes.

it’s a little unspecified how this might be indicated or how a Client might control which objects even have these collections at all. at the barest level, you can use C2S to Create some Collections, then use C2S to Update the object and reference those Collections. so, Creating a single object requires Creating up to three additional collections, and Creating an actor requires Creating up to four additional collections.

now, it’s probably not the best idea to explode a single action into 4 or 5 activities (and therefore 4 or 5 POST requests)… so maybe there’s room for an FEP that allows a way to signal which of these collections should be created and which ones shouldn’t. (maybe they get created by default unless explicitly set to null? not sure what it would look like.)

likewise, there’s possibly room for an FEP to signal which of these collections are off-limits to C2S Add/Remove. otherwise, you might indirectly allow manipulation via Clients doing an Add to followers despite never receiving a Follow. (this is analogous to some mailing list spammer getting your address from someone else.) although even then, there might still be room to argue that the real antispam mechanism should be something like “if i never sent you a Follow, i will drop your incoming message entirely.”

OK, with the last parts, we are moving quite a bit away from the scope of this discussion…

Certainly there is a lot of room for a general “how to manage collections” discussion/FEP. I do have that on my list of things to discuss, but let’s get back tothe original question (to which I think I got your conclusive answer).

to avoid the problem of outbox i think you want a collection that stores objects, not activities. (i forgot that outbox stores activities when doing that writeup earlier, oops.)

re: Create, my answer is indeed what was previously said about giving control to the Server through id assignment (and more generally “creating” objects in a CMS sense, inserting them into the “database” of the server.)

1 Like