Skip to content

Add FEP-7aa9 consent endpoint for featured collections#3277

Open
pfefferle wants to merge 20 commits into
trunkfrom
add/featured-collections-consent
Open

Add FEP-7aa9 consent endpoint for featured collections#3277
pfefferle wants to merge 20 commits into
trunkfrom
add/featured-collections-consent

Conversation

@pfefferle
Copy link
Copy Markdown
Member

Proposed changes:

Phase 1 of FEP-7aa9 (Featured Collections, also known as Mastodon Starter Packs). This PR implements the receive-side consent layer only. The plugin can now respond to incoming FeatureRequest activities from remote servers with Accept (issuing a verifiable FeatureAuthorization stamp) or Reject, governed by a new opt-in site policy.

Architecturally a near-mirror of the existing FEP-044f quote-request layer (Quote_Request, Quote_Authorization, activitypub_default_quote_policy), with three deliberate differences: actor-scoped storage instead of post-scoped, default me (deny) instead of anyone per the FEP "absence of policy = no consent" rule, and interactionPolicy.canFeature advertised on the actor rather than per-post.

  • New inbox handler Activitypub\Handler\Feature_Request — auto-accepts/rejects per policy, emits an Accept with a result URL pointing to the stamp, or a Reject.
  • New extended object Activitypub\Activity\Extended_Object\Feature_Authorization — the JSON-LD stamp document served when remote servers verify a featured item.
  • New global option activitypub_default_feature_policy (anyone / followers / me, default me).
  • New "Featured collection requests" dropdown on Settings → ActivityPub → Activities, sibling of the quote policy field.
  • Actor-level interactionPolicy.canFeature on every User, Blog, and Application actor (single override on the shared Actor base class).
  • Stamps live in _activitypub_featured_by user_meta. The umeta_id doubles as the stamp identifier; ownership enforced by cross-checking the row's user_id against the queried actor on dereference.
  • Stamp URL pattern: ?actor=USER_ID&stamp=UMETA_ID. Resolved by an extension to Activitypub\Query::maybe_get_stamp() that handles the actor-scoped variant alongside the existing post-scoped quote stamps. Includes a guard in Router::template_redirect so stamp URLs are not 404'd by the actor-username lookup.

Out of scope (deferred):

  • Manual approval / review queue — explicitly phase 2.
  • Curating outgoing FeaturedCollections — separate, larger feature.
  • Emitting Delete for FeatureAuthorization on revocation.
  • Per-user override of the global policy.

Other information:

  • Have you written new tests for your changes, if applicable?

27 new test cases / 87 assertions across Test_Feature_Authorization, Test_Feature_Request, Test_Interaction_Policy, Test_Query_Feature_Stamp, plus router-guard and end-to-end coverage in Test_Router and Test_Query_Feature_Stamp::test_stamp_url_routes_and_resolves_end_to_end.

Testing instructions:

  1. On a clean wp-env, go to Settings → ActivityPub → Activities.
  2. Confirm the new Featured collection requests dropdown appears below Default quote policy, defaulted to No one.
  3. Fetch your actor JSON, e.g. curl -H 'Accept: application/activity+json' https://example.com/?author=1. Confirm the response includes:
    "interactionPolicy": {
        "canFeature": { "automaticApproval": ["https://example.com/?author=1"] }
    }
    This is FEP-7aa9 explicit denial: only the actor itself is allowed, i.e. nobody.
  4. Switch the dropdown to Anyone and refetch the actor. automaticApproval should now be ["https://www.w3.org/ns/activitystreams#Public"].
  5. Send a FeatureRequest activity to the actor inbox while the policy is Anyone. Confirm an Accept is emitted to the outbox and the result URL dereferences to a FeatureAuthorization JSON document with interactingObject set to the requester's collection URI.
  6. Repeat with the policy on No one. Confirm a Reject is emitted and no usermeta row is created.
  7. With the policy on Anyone, send the same FeatureRequest twice. Confirm only one usermeta row exists (idempotency).
  8. Try fetching the stamp URL with a tampered actor ID, e.g. ?actor=2&stamp=<umeta_id_belonging_to_user_1>. Confirm a 404 — cross-actor stamps are rejected.

Changelog entry

Already added at .github/changelog/feature-collections-consent (significance: minor, type: added). No auto-create needed.

pfefferle added 17 commits May 8, 2026 15:14
…ape.

Adds explicit assertions for the gts:interactingObject and
gts:interactionTarget mappings in the JSON-LD context test, and a
docblock note explaining the deliberately minimal context.
- Replace inline namespace reference with a use import.
- Add explicit queue_reject coverage (visibility, recipient, key trim).
- Cover the snake_case 'feature_request' alias accepted by handle_blocked_request.
Without this guard the router's actor-username lookup runs first on
?actor=ID&stamp=ID URLs and returns 404 for any user whose username
isn't a number, breaking FeatureAuthorization stamp verification end
to end. The guard tells the router to leave the request alone when a
stamp is also requested so content negotiation can resolve the stamp.
- Merge with parent in Actor::get_interaction_policy so future canQuote / canReply additions on actors do not get dropped silently.
- Rename the feature-policy "Just me" option to "No one" since for featured-collection consent the deny choice is operationally "nobody at all", not "self-service".
- Drop the Phase 2 FeaturedCollection / FeaturedItem / featuredObject / featuredObjectType / featureAuthorization terms from the global Base_Object context. They are sender-side vocabulary that is not used in this phase, and including them bloated every JSON-LD document the plugin emits. They will be re-added in Phase 2 when the plugin actually emits FeaturedCollection objects.
- Document the router stamp guard inline so a future maintainer doesn't accidentally remove it.
- Cover the unresolvable target user path with an explicit Reject test.
- Strengthen the blog actor canFeature test with a value assertion (default-deny shape).
- Add an end-to-end stamp resolution test that goes through go_to() and template_redirect() so the layers can never silently disagree again.
Copilot AI review requested due to automatic review settings May 8, 2026 15:32
@pfefferle pfefferle added the Enhancement New feature or request label May 8, 2026
@pfefferle pfefferle self-assigned this May 8, 2026
@pfefferle pfefferle requested a review from a team May 8, 2026 15:32
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements the receive-side consent flow for FEP-7aa9 Featured Collections (Starter Packs): the plugin can now handle incoming FeatureRequest activities, reply with Accept (including a dereferenceable FeatureAuthorization stamp) or Reject based on a new site policy, and advertise interactionPolicy.canFeature on actor documents.

Changes:

  • Added FeatureRequest inbox handler with policy-controlled accept/reject behavior and actor-scoped stamp storage in user meta.
  • Added FeatureAuthorization extended object plus routing/query support to dereference stamps via ?actor=ID&stamp=UMETA_ID.
  • Added admin setting + option registration for activitypub_default_feature_policy, and comprehensive PHPUnit coverage for policy output and stamp lifecycle.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/phpunit/tests/includes/model/class-test-interaction-policy.php Verifies actor-level interactionPolicy.canFeature output for user/blog actors under each policy.
tests/phpunit/tests/includes/handler/class-test-feature-request.php Tests validation, blocked-request handling, policy branching, accept/reject queuing, and idempotent stamp creation.
tests/phpunit/tests/includes/class-test-router.php Adds coverage to ensure stamp URLs aren’t 404’d by the actor branch in template_redirect.
tests/phpunit/tests/includes/class-test-query-feature-stamp.php Tests Query resolution for actor-scoped stamps, including cross-actor rejection and end-to-end routing.
tests/phpunit/tests/includes/activity/extended-object/class-test-feature-authorization.php Verifies Feature_Authorization type and JSON-LD context mappings.
includes/wp-admin/class-settings-fields.php Adds “Featured collection requests” dropdown to ActivityPub settings UI.
includes/handler/class-feature-request.php New inbox handler for FeatureRequest, including policy enforcement and stamp issuance.
includes/class-router.php Adds guard to avoid actor-username 404 path for stamp URLs.
includes/class-query.php Extends stamp resolution to support actor-scoped FeatureAuthorization stamps.
includes/class-options.php Registers the new activitypub_default_feature_policy option with sanitization and default.
includes/class-handler.php Registers the new FeatureRequest handler during handler initialization.
includes/activity/extended-object/class-feature-authorization.php Introduces Feature_Authorization extended object and its JSON-LD context.
includes/activity/class-base-object.php Adds canFeature term to base JSON-LD context.
includes/activity/class-actor.php Emits actor-level interactionPolicy.canFeature derived from the site policy.
includes/activity/class-activity.php Adds FeatureRequest to Activity JSON-LD context and allowed activity types.
.gitignore Ignores docs/superpowers/.
.github/changelog/feature-collections-consent Adds changelogger entry documenting the new opt-in consent setting.

Comment thread includes/activity/class-actor.php
Comment thread includes/class-router.php Outdated
pfefferle added 3 commits May 8, 2026 17:47
- Move canFeature out of Base_Object's JSON-LD context. The term is only
  emitted by the actor (via Actor::get_interaction_policy), so carrying
  it on every Note / Article / Activity bloated their @context and
  silently broke Test_Outbox::test_add by altering the snapshot every
  Outbox object renders.
- Add interactionPolicy, canFeature, automaticApproval, and the gts:
  prefix to Actor::JSON_LD_CONTEXT so actor JSON now actually defines
  the terms it emits, rather than hand-waving them through inheritance.
- Tighten the router stamp guard so it only bypasses the actor branch
  when actor is numeric (the stamp pattern). Non-numeric actors still
  get the regular Mastodon-style profile lookup.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants