Deliver Join and Leave to the app's inbox instead of an unaddressed outbox post
A participation activity is now addressed to the Festipod app actor alone and handled on reception in the app's inbox, the way the ActivityPods developer advised: use the ActivityPub protocol rather than a side channel. Any compatible sender may deliver; the inbox accepts only the activity's own signer, and the backend still requires the activity to lie in that actor's pod and acts on the stored copy. The meeting point is read again in its creator's pod before anything is written: read through the participant's pod, a foreign server's proxy chose the attendee list and the notification's name. Confidentiality is unchanged: the Join stays readable by its author and the app only. Recorded in decision_2026-11-03_participation-delivered-to-the-app, a named exception to festipod-only-reference-app-patterns.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
// What the backend may believe about an activity its watcher reports — the check in front of every
|
||||
// Festipod handler fed by that watcher (public-read, catalog, attendees, contacts, registration).
|
||||
// What the backend may believe about an activity its watcher reports, or a server delivers to the
|
||||
// app's inbox — the check in front of every Festipod handler fed by that watcher (public-read,
|
||||
// catalog, attendees, contacts) or by that inbox (registration). A delivered body, like a webhook
|
||||
// body, is only a pointer.
|
||||
//
|
||||
// THE WEBHOOK IS A HINT, NOT A SOURCE. The webhook route (`/.webhooks/:uuid`) is unauthenticated: the
|
||||
// listener only matches the uuid, the watcher derives the box owner from the body's `target`, and an
|
||||
@@ -16,7 +18,11 @@
|
||||
// activity must lie in that owner's pod and the stored activity must name that owner as `actor`;
|
||||
// - an INBOX notification does not (`verifiedReceive`): the emitter is the stored activity's own
|
||||
// `actor`, believed only because the activity lies in THAT actor's pod — someone else's claim
|
||||
// about an actor cannot be stored there.
|
||||
// about an actor cannot be stored there;
|
||||
// - a DELIVERY to the app's own inbox names its emitter (`verifiedDelivery`): the inbox accepts it
|
||||
// only signed by the activity's `actor` (@semapps/activitypub 1.2.7 `inbox.post` refuses an actor
|
||||
// other than the signer, INVALID_ACTOR). Nothing else in the body is believed: the activity must
|
||||
// lie in that actor's pod and the copy stored there name them.
|
||||
//
|
||||
// An activity is accepted only when all of this holds:
|
||||
// 1. it is identified by a URI (an embedded activity without an id was never stored);
|
||||
@@ -87,13 +93,14 @@ const ANY_POD = Symbol('any pod');
|
||||
* { ok: true, activity, emitterUri, objectUri, object } (object: the stored object; undefined for a Delete)
|
||||
* { ok: false, reason }
|
||||
*
|
||||
* `activity` is what the watcher handed over (a URI or a node — only its id is used). `type` is the
|
||||
* activity type the handler expects. `emitterUri` is the owner of the outbox the notification names,
|
||||
* or `null` for an inbox notification (the emitter is then the stored activity's actor). The object
|
||||
* must lie in the pod of `objectOwnerUri`, or anywhere with `ANY_POD`. `objectType` is the type the
|
||||
* stored object must declare — an ActivityStreams name or a full IRI; not checked for a Delete.
|
||||
* `read(uri)` reads a resource as the box owner and resolves `{ status, body }` (`status` 0 when the
|
||||
* request itself failed). Never throws for a refusal; a throwing `read` propagates.
|
||||
* `activity` is what the watcher or the app's inbox handed over (a URI or a node — only its id is
|
||||
* used). `type` is the activity type the handler expects. `emitterUri` is the owner of the outbox the
|
||||
* notification names, the `actor` the app's inbox verified as the signer of a delivery, or `null` for
|
||||
* an inbox notification (the emitter is then the stored activity's actor). The object must lie in the
|
||||
* pod of `objectOwnerUri`, or anywhere with `ANY_POD`. `objectType` is the type the stored object
|
||||
* must declare — an ActivityStreams name or a full IRI; not checked for a Delete. `read(uri)` reads a
|
||||
* resource as the box owner — for a delivery, as its emitter — and resolves `{ status, body }`
|
||||
* (`status` 0 when the request itself failed). Never throws for a refusal; a throwing `read` propagates.
|
||||
*/
|
||||
async function verifyActivity({ activity, type, emitterUri, objectOwnerUri, objectType, read }) {
|
||||
if (!ACTIVITY_TYPES.has(type)) throw new Error(`Unsupported activity type ${type}`);
|
||||
@@ -227,6 +234,45 @@ function verifiedReceive(type, handle, { objectType } = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* An `onReceive` for the APP's own inbox (`@semapps/activitypub` `ActivitiesHandlerMixin`, whose
|
||||
* third argument is the inbox owner, the app): runs `handle(ctx, verdict, emitterUri)` only for an
|
||||
* activity `verifyActivity` accepts as emitted by the delivered activity's `actor`, the signer the
|
||||
* inbox verified. Before anything is read, the activity's URI must lie in that actor's pod; the
|
||||
* activity and its object are then read through that actor's pod (`podReader`), the stored activity
|
||||
* having to name that same actor. `objectType` and `objectIn` are those of `verifiedEmit`. With
|
||||
* `objectIn: 'any'`, an object outside the actor's pod is read through the proxy THEIR actor document
|
||||
* declares — for an actor on another server, an answer that server chooses: a handler acting on such
|
||||
* an object reads it again where it lies (registration's `meetingPointOf`).
|
||||
*/
|
||||
function verifiedDelivery(type, handle, { objectType, objectIn = 'emitter' } = {}) {
|
||||
if (objectIn !== 'emitter' && objectIn !== 'any') throw new Error(`Unknown objectIn ${objectIn}`);
|
||||
return async function (ctx, activity, inboxOwnerUri) {
|
||||
try {
|
||||
const claimed = activity && typeof activity === 'object' ? singleUriOf(activity.actor) : undefined;
|
||||
if (!claimed) {
|
||||
this.logger.warn(`${this.name} ignored a ${type} delivered to ${inboxOwnerUri}: it names no single actor`);
|
||||
return;
|
||||
}
|
||||
const verdict = await verifyActivity({
|
||||
activity,
|
||||
type,
|
||||
emitterUri: claimed,
|
||||
objectOwnerUri: objectIn === 'any' ? ANY_POD : claimed,
|
||||
objectType,
|
||||
read: podReader(ctx, claimed)
|
||||
});
|
||||
if (!verdict.ok) {
|
||||
this.logger.warn(`${this.name} ignored a ${type} delivered to ${inboxOwnerUri} as from ${claimed}: ${verdict.reason}`);
|
||||
return;
|
||||
}
|
||||
await handle.call(this, ctx, verdict, verdict.emitterUri);
|
||||
} catch (e) {
|
||||
this.logger.error(`${this.name} could not process a ${type} delivered to ${inboxOwnerUri}: ${e.message}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
verifyActivity,
|
||||
verifyEmittedActivity,
|
||||
@@ -234,6 +280,7 @@ module.exports = {
|
||||
neverThrowingMatcher,
|
||||
verifiedEmit,
|
||||
verifiedReceive,
|
||||
verifiedDelivery,
|
||||
uriOf,
|
||||
singleUriOf,
|
||||
hasAsType,
|
||||
|
||||
Reference in New Issue
Block a user