activitypods / activitypods/activitypods

Capability-based activities cannot be handled by app backends

Aperta
#448 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub
Lingua principale
JavaScript
Stelle
260
Fork
24
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Descrizione

**Issue**

A capability attached to an activity is deliberately kept in memory and never persisted:

```js
// activitypub/services/activitypub/subservices/outbox.ts, post()
// There might be a capability attached which cannot be persisted, if it has a non-resolvable id.
// So we won't persist it and re-attach it afterwards.
let { capability, ...activityToPersist } = activity;
```

```js
// activitypub/services/activitypub/subservices/outbox.ts, localPost()
// Leave the capability separate because we don't want to store it.
const { capability, ...activity } = activityToPost;
```

Services running **inside the Pod** are unaffected: `processInbox` receives the whole activity,
`verifyCapabilityIntegrity` runs, and any processor declaring `capabilityGrantMatchFnGenerator`
gets its grants checked. This is how the Pod provider's own contact invite links work.

**ActivityPods app backends cannot do the same.** An app learns about an activity through a Solid
notification that carries only its URI, then re-fetches it from the Pod
(`pod-activities-watcher.processWebhook`). By then the capability is gone.

Verified against a real Pod provider — a `Join` posted with both `capability` (a presentation
object) and `instrument` (the credential URI), read back from the sender's outbox:

```
POST outbox -> 201
GET of the stored activity -> 200
{ "type": "Join", "actor": ".../linkguest", "object": "...", "to": ".../linkorga",
"instrument": "http://localhost:3000/linkorga/data/d0a60a68-..." }

capability survived? false
instrument survived? "http://localhost:3000/linkorga/data/d0a60a68-..."
```

Same result when posting with the Pod's full JSON-LD context, so this is the deliberate stripping
above and not a JSON-LD term resolution problem (`capability` *is* defined, as `sec:capability`).

Consequence: any feature where an app must decide something on the strength of a capability has to
smuggle the credential URI through another predicate and re-verify it itself — duplicating what the
Pod already did, and without the presentation's proof, which is precisely the part an app *cannot*
re-check (the challenge is single-use and already consumed on the Pod).

Real case: "anyone with the link" event sharing in Welcome to my place, where the app backend must
decide whether to accept a `Join` from someone who was never invited.

## Components affected

- `@semapps/activitypub` — `activitypub.outbox` (`post`, `localPost`), `activitypub.inbox` (`post`)
- `@semapps/solid` — `solid-notifications.provider`, `notification-channel.mixin`
- `@activitypods/app` — `pod-activities-watcher`, `pod-activities-handler` (consumer side)

## Proposal

Half the plumbing already exists. The notification mixin already accepts a whole activity in place
of a URI (used today for unfetchable Mastodon activities):

```js
// solid/services/notifications/channels/notification-channel.mixin.ts
const { collectionUri, itemUri, item } = ctx.params;
this.onContainerOrCollectionEvent(collectionUri, itemUri || item, ACTIVITY_TYPES.ADD);
```

And the app side already handles both shapes, so **apps would need no change at all**:

```js
// @activitypods/app — pod-activities-watcher.js
const activity = isURL(object) ? await fetcher(object) : object;
```

**Option A — transport the capability.** When an activity carries one, emit
`activitypub.collection.added` with the in-memory activity as `item`, so the notification carries
it. Simple, but the presentation would then reach *every* listener registered on that inbox, not
just the app the activity concerns.

**Option B — transport the verification result (preferred).** Ship what the app actually needs
rather than bearer material, e.g. `capability: { verified: true, verifiableCredential: [] }`.
Nothing replayable is broadcast, and the Pod stays the single place where verification happens —
which it has to be, since it is the only holder of the challenge.

**Complement.** Give `PodActivitiesHandlerMixin` the same `capabilityGrantMatchFnGenerator` support
that `ActivitiesHandlerMixin` already has, so an app declares the grant it requires and the
framework enforces it, instead of every app hand-rolling the check.

## Related

`inbox.post` catches errors from `processInbox` and only logs them, and stores the activity anyway
— so a failed `verifyCapabilityIntegrity` does not stop delivery. An app cannot treat "it reached
me" as "the Pod validated it", which is worth stating explicitly in whichever option is chosen.

Guida per i contributori

Nessuna guida per i contributori indicizzata per questo repository

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.