Files
festipod/backend/services/collection-attach.js
Sylvain Duchesne 908f7ea973 Keep sign-ups unlinkable on ActivityPods, and land the pending coherence-pass work
Participation privacy (decision_2026-10-29_participation-privacy-mechanism):
a sign-up is a private Join naming the meeting point, addressed to nobody,
handled by the backend on emission. Attendee lists hold member URNs hashed
from (WebID, event, a secret kept in the participant's profile), so counts
stay public and only contacts recognise anyone; the creator has no read
right and is notified anonymously. Each person's participations collection,
created by the backend at the first sign-up, is readable by their /contacts
WAC group only, never world-readable. The member URN is sticky per
(meeting point, participant) and every collection write is verified by
reading back, so a refused write leaves the act unrecorded and retryable.
The EventRef mechanism and its browser-side provisioning are gone.

The backend now runs with WebACL on, as the reference apps do, on its own
secure datasets festipod / settings-festipod instead of the pod provider's
settings dataset; pod-notifications needs it. The compose header and the
deployment leaf say how to reset a stack whose datasets predate this.

The rights matrix covers the new mechanism live (148 hard lines), including
a forged contact Accept between throwaway accounts: the stranger enters the
contacts list (platform defect, reported upstream) but is refused the profile
and the participations. Tests may attack as long as they never break the dev
environment (rule_tests-never-break-the-dev-environment).

Also lands the coherence-pass work that was still uncommitted: contacts as
the Pod's social graph and invite links, image upload to the Pod, the
meeting-point form, profile completion gate, and the doctrine for them.
Superseded briefs and decisions are deleted rather than annotated.
2026-10-30 17:06:38 +01:00

46 lines
2.1 KiB
JavaScript

// The `createAndAttach` action every Festipod collection handler uses (attendees, contacts,
// participations), in place of the one `PodCollectionsHandlerMixin` (@activitypods/app 2.3.2) gives them.
//
// The mixin's version calls `pod-collections.createAndAttach`, then derives a WAC group slug from the
// collection URI it returns. That call returns undefined when it attaches nothing — the resource
// already carries a value for the predicate (written by its creator or by another app), or the pod
// refused to create the collection — and the mixin then runs `new URL(undefined)`, which throws. The
// webhook call that runs the watcher is not awaited by the platform, so that throw ended the whole
// backend process (observed 2026-10-12, an event POSTed with `apods:attendees` already set). Here,
// nothing attached means no group to create.
/** The action definition; runs bound to a service with the mixin's settings and `getGroupSlugFromCollectionUri`. */
const guardedCreateAndAttach = {
async handler(ctx) {
const { resourceUri, actorUri } = ctx.params;
const { attachPredicate, collectionOptions, createWacGroup } = this.settings;
const collectionUri = await ctx.call('pod-collections.createAndAttach', {
resourceUri,
attachPredicate,
collectionOptions,
actorUri
});
if (!collectionUri) {
this.logger.info(`No ${attachPredicate} collection attached to ${resourceUri} (already carries one, or creation refused)`);
return undefined;
}
// The service's own follow-up for a collection it has just created (attendees: record it as
// Festipod's, see public-read-ledger.js) — right after the attach, so a failure of what follows
// (the WAC group) cannot skip it.
if (typeof this.onCollectionAttached === 'function') {
await this.onCollectionAttached(ctx, { resourceUri, collectionUri, actorUri });
}
if (createWacGroup) {
await ctx.call('pod-wac-groups.create', {
groupSlug: this.getGroupSlugFromCollectionUri(collectionUri),
actorUri
});
}
return collectionUri;
}
};
module.exports = { guardedCreateAndAttach };