Changeset 8b07c12 in Klonkt


Ignore:
Timestamp:
07/22/2026 12:26:08 AM (7 weeks ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
dc4aa08
Parents:
4c70ecb
Message:

The AP blocked collection: the server blocklist drives Shaer's Orbit

Robins call: no separate client-side Orbit state; the block list on the
server is the source of truth. The actor now advertises the standard
ActivityPub blocked collection (spec 5.6) and the owner reads it over C2S;
Shaer hydrates "in Orbit" from it and keeps nothing locally.

Changed files:
src/services/ActivityPubService.js

  • buildActor: blocked: {id}/blocked (AP 5.6, owner-only GET)

src/routes/activitypub.js

  • GET /ap/users/:slug/blocked (bearer, owner-only): actor-kind blocks as an OrderedCollection of actor uris; domain blocks stay out (instance policy, not an Orbit member)

test/c2s-block.test.js

  • the actor advertises blocked; actor-kind items only

test/activitypub-as2.test.js

  • blocked added to the AS2 allowlist (a spec term, same family as liked/streams)

153 tests, all green.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

Files:
4 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    r4c70ecb r8b07c12  
    8787  ).all(site.id);
    8888  AP.sendAP(res, AP.buildOutbox(baseUrl(req), site, posts));
     89});
     90
     91// ── Blocked collection (owner only, AP §5.6) ──────────────────────
     92// The server blocklist is the source of truth for Shaer's "in Orbit":
     93// clients read it here instead of keeping their own state. Actor-kind
     94// blocks only (domain blocks are instance policy, not an Orbit member).
     95router.get('/ap/users/:slug/blocked', (req, res) => {
     96  const auth = OAuth.verifyBearer(req.headers.authorization);
     97  if (!auth || auth.site.slug !== req.params.slug) return res.status(403).end();
     98  const base = baseUrl(req);
     99  const items = AP.listBlocks(auth.site.slug)
     100    .filter((b) => b.kind === 'actor')
     101    .map((b) => b.target);
     102  AP.sendAP(res, {
     103    '@context': AP.AP_CONTEXT,
     104    id: `${base}/ap/users/${auth.site.slug}/blocked`,
     105    type: 'OrderedCollection',
     106    totalItems: items.length,
     107    orderedItems: items,
     108  });
    89109});
    90110
  • src/services/ActivityPubService.js

    r4c70ecb r8b07c12  
    167167    following: `${id}/following`,
    168168    featured: `${id}/featured`,
     169    // AP §5.6: the private blocked collection (owner-only GET). The server
     170    // list is the source of truth for Shaer's "in Orbit"; clients keep no
     171    // separate state.
     172    blocked: `${id}/blocked`,
    169173    // C2S clients (Shaer apps) discover auth + upload here — no hardcoded paths.
    170174    // All four are ActivityPub-spec `endpoints` terms. Dynamic client registration
  • test/activitypub-as2.test.js

    r4c70ecb r8b07c12  
    3030  'totalItems', 'orderedItems', 'items', 'first', 'last', 'partOf', 'next', 'prev',
    3131  'preferredUsername', 'inbox', 'outbox', 'followers', 'following', 'endpoints', 'sharedInbox',
     32  // ActivityPub §5.6: the private blocked collection (owner-only GET).
     33  'blocked',
    3234  // ActivityPub §4.1 `endpoints` vocabulary (same category as sharedInbox), used for C2S.
    3335  'oauthAuthorizationEndpoint', 'oauthTokenEndpoint', 'uploadMedia',
  • test/c2s-block.test.js

    r4c70ecb r8b07c12  
    4343  assert.equal(out.status, 400);
    4444});
     45
     46test('the actor advertises the blocked collection (AP 5.6)', () => {
     47  site.primary_slug = 'me';
     48  const actor = AP.buildActor('https://test.example', site);
     49  assert.equal(actor.blocked, 'https://test.example/ap/users/me/blocked');
     50});
     51
     52test('actor-kind blocks form the collection items; domain blocks stay out', async () => {
     53  await AP.ingestOutboxActivity(site, user, { type: 'Block', object: BULLY });
     54  await AP.blockTarget(site, 'nare-server.example');   // domain block
     55  const items = AP.listBlocks('me').filter((b) => b.kind === 'actor').map((b) => b.target);
     56  assert.deepEqual(items, [BULLY]);
     57});
Note: See TracChangeset for help on using the changeset viewer.