source: Klonkt/test/activitypub-as2.test.js@ 9a00f28

main
Last change on this file since 9a00f28 was 8b07c12, checked in by Robin Genis <roboburr@…>, 7 weeks ago

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@…>

  • Property mode set to 100644
File size: 5.0 KB
Line 
1// AS2 / JSON-LD validity guard.
2//
3// Every property key and `type` value our ActivityPub objects emit MUST be either an AS2-core
4// (or security/v1) term OR declared in the federation @context (AP_CONTEXT). A new feature that
5// emits an undeclared term fails this test → declare it in AP_CONTEXT (extensions) or add it to
6// the AS2 allowlist below. This keeps Klonkt's output valid AS2/JSON-LD forever — not just
7// "Mastodon tolerates it". No extra deps; in-memory SQLite. Run: npm test
8import { test } from 'node:test';
9import assert from 'node:assert/strict';
10
11process.env.DATABASE_PATH = ':memory:';
12process.env.PUBLIC_BASE_URL = 'https://test.example';
13
14const dbMod = await import('../src/config/database.js');
15const db = dbMod.default;
16dbMod.initializeDatabase();
17const AP = (await import('../src/services/ActivityPubService.js')).default;
18
19const BASE = 'https://test.example';
20
21// AS2-core + security/v1 vocabulary Klonkt uses (stable — only extend when AS2/security itself
22// adds a term we adopt). JSON-LD keywords included.
23const AS2 = new Set([
24 '@context', '@id', '@type',
25 'id', 'type', 'actor', 'object', 'target', 'to', 'cc',
26 'content', 'name', 'summary', 'url', 'href', 'mediaType',
27 'published', 'updated', 'attributedTo', 'inReplyTo', 'replies',
28 'attachment', 'tag', 'icon', 'image', 'duration',
29 'contentMap', 'nameMap', 'summaryMap', // AS2 @language-map counterparts of content/name/summary
30 'totalItems', 'orderedItems', 'items', 'first', 'last', 'partOf', 'next', 'prev',
31 'preferredUsername', 'inbox', 'outbox', 'followers', 'following', 'endpoints', 'sharedInbox',
32 // ActivityPub §5.6: the private blocked collection (owner-only GET).
33 'blocked',
34 // ActivityPub §4.1 `endpoints` vocabulary (same category as sharedInbox), used for C2S.
35 'oauthAuthorizationEndpoint', 'oauthTokenEndpoint', 'uploadMedia',
36 'publicKey', 'owner', 'publicKeyPem',
37 'Note', 'Person', 'Create', 'Update', 'Delete', 'Tombstone', 'Announce', 'Like', 'Follow',
38 'Accept', 'Reject', 'Undo', 'Add', 'Remove', 'Flag', 'Document', 'Image', 'Audio', 'Video',
39 'Mention', 'Link', 'Collection', 'OrderedCollection', 'OrderedCollectionPage',
40]);
41
42// The extension terms = exactly the keys declared in AP_CONTEXT's term-definition object.
43const ctxTerms = new Set();
44for (const part of AP.AP_CONTEXT) if (part && typeof part === 'object') for (const k of Object.keys(part)) ctxTerms.add(k);
45const allowed = new Set([...AS2, ...ctxTerms]);
46
47// Collect every property key + every `type` string value, recursively.
48function collect(obj, keys = new Set()) {
49 if (Array.isArray(obj)) { for (const x of obj) collect(x, keys); return keys; }
50 if (obj && typeof obj === 'object') {
51 for (const [k, v] of Object.entries(obj)) {
52 keys.add(k);
53 if (k === 'type' && typeof v === 'string') keys.add(v);
54 if (/Map$/.test(k)) continue; // a @language map (contentMap/…): its keys are BCP-47 tags, not vocab terms
55 collect(v, keys);
56 }
57 }
58 return keys;
59}
60function assertValid(obj, label) {
61 const undeclared = [...collect(obj)].filter((k) => !allowed.has(k));
62 assert.deepEqual(undeclared, [],
63 `${label}: undeclared AS2/JSON-LD term(s) — declare in AP_CONTEXT (extension) or the AS2 allowlist: ${undeclared.join(', ')}`);
64}
65
66// Seed one site that exercises the extension-heavy actor fields (profile links → PropertyValue,
67// photo → icon, primary → featured).
68db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@test', 'x', 'god');
69db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary, profile_links, profile_photo) VALUES (?,?,?,?,?,?,?)')
70 .run('s1', 'demo', 'Demo', 'u1', 1, JSON.stringify([{ platform: 'website', url: 'https://x.test' }]), '/media/x.png');
71const site = db.prepare('SELECT * FROM sites WHERE id = ?').get('s1');
72site.primary_slug = 'demo';
73
74// Kitchen-sink note: nsfw (→ sensitive + summary), a hashtag (→ Hashtag), a cover (→ attachment).
75const post = {
76 id: 'p1', slug: 'hello', title: 'Hi', content: '<p>hello #music</p>',
77 nsfw: 1, content_warning: 'cw', tags: JSON.stringify(['mood']),
78 cover_image_url: '/media/c.webp', cover_alt: 'A cover', language: 'en',
79 published_at: '2026-01-01T00:00:00Z', created_at: '2026-01-01T00:00:00Z',
80};
81
82test('actor is valid AS2 (every term declared)', () => assertValid(AP.buildActor(BASE, site), 'actor'));
83test('create+note is valid AS2 (every term declared)', () => assertValid(AP.buildCreate(BASE, site, post), 'create/note'));
84test('outbox/followers/featured collections are valid AS2', () => {
85 assertValid(AP.buildOutbox(BASE, site, [post]), 'outbox');
86 assertValid(AP.buildFollowers(BASE, site, 3), 'followers');
87 assertValid(AP.buildFollowing(BASE, site, 2), 'following');
88 assertValid(AP.buildFeatured(BASE, site, [post]), 'featured');
89});
90test('AP_CONTEXT declares every extension term we rely on', () => {
91 for (const t of ['sensitive', 'Hashtag', 'manuallyApprovesFollowers', 'discoverable', 'featured', 'PropertyValue', 'embedUrl'])
92 assert.ok(ctxTerms.has(t), `AP_CONTEXT must declare "${t}"`);
93});
Note: See TracBrowser for help on using the repository browser.