fedify icon indicating copy to clipboard operation
fedify copied to clipboard

Outbox listeners — Client-to-Server interactions

Open mrkvon opened this issue 3 months ago • 9 comments

Summary

Client-to-server interactions in the ActivityPub specification are defined as POST requests to the actor's outbox. Is there a plan to implement something like setOutboxListeners?

Problem

A custom client (e.g. a fediverse app) wants to interact with the server in a standard way — for example tell it about actions that user performed via app's UI, like creating a post, or following somebody.

Proposed Solution

The ActivityPub spec defines such interactions and contains numerous examples. POSTing an Activity to actor's outbox is a standard way to perform this communication.

The important consideration is the authentication and authorization:

The request MUST be authenticated with the credentials of the user to whom the outbox belongs.

Only the user represented by the actor is supposed to POST to their own outbox.

IIUC The outbox POST requests coming from the client may be authenticated in a custom way, as the Bearer in Example 11 in the spec indicates.

POST /outbox/ HTTP/1.1 Host: dustycloud.org Authorization: Bearer XXXXXXXXXXX Content-Type: application/ld+json; profile="https://www.w3.org/ns/activitystreams"

Alternatives Considered

I think servers (Mastodon) implement their custom non-standard API for the same purpose. Such interface may be better omitted from Fedify, as it is not ActivityPub.

I'm relatively new to tinkering with fediverse, so I may not be aware of some contexts.

Scope / Dependencies

It would probably mean implementing setOutboxListeners, a new listener to outbox POST requests. Make a possibility for user to define a custom authentication. Then for each received valid activity, the server is supposed to send the activity to the actors specified in the "to", "cc", and "bcc" fields. This process may or may not be user-defined in a callback. That's probably open to a discussion (as all of this, actually 🙂)

mrkvon avatar Sep 12 '25 18:09 mrkvon

There's some debate of whether you can actually serve both S2S outboxes and C2S outboxes simultaneously, unless you do some negotiation based on the authentication presented.

Basically if you do a POST /outbox then the GET /outbox needs to return the same activities that were posted. However GET /outbox for S2S is activities that are to be federated to other servers.

ThisIsMissEm avatar Sep 12 '25 18:09 ThisIsMissEm

Thank you for the insights @ThisIsMissEm!

Basically if you do a POST /outbox then the GET /outbox needs to return the same activities that were posted.

Is this behavior a standard or a common practice? If it is, then this would indeed be an inconsistency in specification or implementations.

However GET /outbox for S2S is activities that are to be federated to other servers.

That makes sense, thank you for clarifying! Thanks to this, I looked to specification which says:

The outbox stream contains activities the user has published, subject to the ability of the requestor to retrieve the activity (that is, the contents of the outbox are filtered by the permissions of the person reading it). If a user submits a request without Authorization the server should respond with all of the Public posts.

which seems consistent with the latter interpretation.

The term "published" seems a bit vague. The question is, does it refer to every activity posted to inbox, or a subset of them? e.g. I suppose Follow only needs to be kept until a corresponding Accept/Reject is received. A successful Accept response to a received Follow wouldn't need to be stored at all - it got saved in the followers/following collections. Does that align with your understanding?

In this interpretation, POST outbox would accept instructions without any obligation to store them; GET outbox would publish activities to audiences. The two being decoupled...

Another question is, what's the practice... 🙂

mrkvon avatar Sep 12 '25 22:09 mrkvon

Thank you @mrkvon for bringing up this important topic, and @ThisIsMissEm for the valuable insights!

As a Fedify maintainer, I'm not only open to adding C2S support—I've actually been hoping for this for a while. Let me share my thoughts on the design challenges you've raised.

For actor authentication on POST /outbox, I think Fedify should remain authentication-agnostic. We could provide a flexible mechanism where users define their own authentication logic, similar to how we handle other customizable parts of the framework. Here's what I'm imagining the API could look like, following our existing patterns:

federation
  .setOutboxListeners("/outbox/{identifier}")
  .on(Create, async (ctx, identifier, activity) => {
    // Handle Create activity posted via C2S
  })
  .on(Follow, async (ctx, identifier, activity) => {
    // Handle Follow activity posted via C2S
  })
  .authenticate(async (ctx, identifier) => {
    const token = ctx.request.headers.get("authorization");
    if (validateToken(identifier, token)) {
      return true;
    }
    return false;
  });

This way, users could integrate whatever authentication scheme they need—Bearer tokens, OAuth, or something custom.

Regarding automatic sendActivity() processing, I'm leaning towards making it automatic by default but with an option to override. Most users would probably expect activities posted via C2S to be federated automatically (that's what the spec implies), but there are definitely cases where you'd want more control over the process.

The S2S vs C2S incompatibility that @ThisIsMissEm mentioned is indeed tricky. The most pragmatic approach might be content negotiation based on authentication—authenticated requests get C2S behavior, unauthenticated ones get S2S behavior. This aligns with how the spec talks about filtering by permissions. Though I'm also tempted by the idea of just using separate endpoints (/outbox for S2S, /outbox/client for C2S) even if it's not strictly spec-compliant, simply because it would eliminate confusion.

I'm curious if there are any existing C2S implementations we should look at for compatibility. Given how rare C2S implementations are in the wild, we might be pioneering here, which is both exciting and a bit daunting.

dahlia avatar Sep 13 '25 04:09 dahlia

The outlined API looks great to me!

To my understanding, POST outbox/ is reserved for C2S interactions and thus is not ambiguous. The ambiguity affects GET outbox/, but this is the case for other readable (GET) collections as well.

My motivation to open this issue was listeners for POST outbox/ specifically.

I agree with the relevance of the ambiguity of GET. Maybe it could be tracked in a separate issue?

mrkvon avatar Sep 13 '25 14:09 mrkvon

I'm curious if there are any existing C2S implementations we should look at for compatibility. Given how rare C2S implementations are in the wild, we might be pioneering here, which is both exciting and a bit daunting.

ActivityPods seem to have POST outbox implemented.

mrkvon avatar Sep 13 '25 14:09 mrkvon

Thanks for the positive feedback! And great find with ActivityPods—I'll definitely look into their implementation for reference.

You're right that POST /outbox is unambiguous since it's C2S-only. Let's focus on that for this issue and handle the GET /outbox ambiguity separately when we get there. That'll keep things more manageable.

dahlia avatar Sep 13 '25 14:09 dahlia

I think I can work on implementing this (already working on related things)

ThisIsMissEm avatar Sep 13 '25 16:09 ThisIsMissEm

@dahlia on the API, I think we may want a catch-all route too "for all other activities do this"

ThisIsMissEm avatar Sep 13 '25 16:09 ThisIsMissEm

@ThisIsMissEm Great to hear you can work on this!

And yes, good point about the catch-all handler. We can use .on(Activity, handler) for that—the inbox listeners already work this way, so it'll be consistent.

dahlia avatar Sep 14 '25 02:09 dahlia