source: Klonkt/test/activitypub-as2.test.js@ 6b5d7da

main
Last change on this file since 6b5d7da was 6b5d7da, checked in by Robin Genis <roboburr@…>, 7 weeks ago

Guardianship als eigen module + gedeelde BlocklistService

FEP-633c (Guardians) lag verspreid door ActivityPubService; nu is het één
cohesief onderdeel in src/services/guardianship/ met submodules. De
blocklist staat er bewust NAAST (BlocklistService): die wordt gedeeld met
Klonkt zelf (Block-tab) en is niet guardianship-specifiek.

De module importeert ActivityPubService nooit terug: de AP-helpers gaan er
één keer in via wireDelivery/wireHandshake, en de service delegeert met
dunne wrappers zodat elke bestaande aanroep blijft werken.

Naast de verhuizing ook de serverkant die nog miste (shaer-bh1): de
ap_guardianships-relaties, shaer:guardians/isGuardian/queues op het
actor-doc, en de adoptie-handshake Offer/Accept/Reject over C2S en S2S,
met het contract van de Shaer test-daemon zodat de iOS/Android-clients het
ongewijzigd spreken. Een inkomend hulpverzoek (shaer:helpRequest op een
directe mention) krijgt een eigen vlag in ap_mentions en pusht als
'help'-type richting de Guardian-PWA (volgende commit).

Changed files:
src/services/ActivityPubService.js

  • shaer-context, actor-props en helpRequest uit de module gespread
  • blocklist-functies zijn delegaties naar BlocklistService
  • c2sVisibility/deliverDirectNote re-export uit guardianship/delivery
  • C2S: Offer/Accept/Reject eerst langs de handshake-module
  • S2S: Offer aan GATED (signature-eis) + handshake-routering
  • inbound mention: help_request-vlag + 'help'/'guardian'-push-events

src/config/database.js

  • tabel ap_guardianships (slug, role, other_uri, status, offer_id)
  • kolom ap_mentions.help_request

test/activitypub-as2.test.js

  • shaer:queues/offers/follows/wards in de AS2-allowlist

New file:
src/services/BlocklistService.js

  • ap_blocks-opslag, blockTarget/unblock/listBlocks/isBlockedAny, purge; handle-resolver via injectie (geen circulaire import)

src/services/guardianship/index.js

  • de publieke API van het onderdeel

src/services/guardianship/context.js

  • shaer-namespace + Relationship-vocabulaire

src/services/guardianship/relations.js

  • ap_guardianships-API + actor-props (FEP-633c paragraaf 2)

src/services/guardianship/handshake.js

src/services/guardianship/queues.js

src/services/guardianship/notes.js

  • shaer:helpRequest lezen/schrijven

src/services/guardianship/delivery.js

  • de directe-note-route (call-for-help), gedrag ongewijzigd

remarks: alle 158 tests groen. push-teksten (push.n_help_*, push.n_guard_*)
en de queue-routes + Guardian-PWA volgen in de volgende commits.

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

  • Property mode set to 100644
File size: 5.2 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 // FEP-633c (Guardians): the owner-only dashboard queues on the actor; the
35 // sub-keys are the daemon-contract collection names the Shaer clients read.
36 'shaer:queues', 'offers', 'follows', 'wards',
37 // ActivityPub §4.1 `endpoints` vocabulary (same category as sharedInbox), used for C2S.
38 'oauthAuthorizationEndpoint', 'oauthTokenEndpoint', 'uploadMedia',
39 'publicKey', 'owner', 'publicKeyPem',
40 'Note', 'Person', 'Create', 'Update', 'Delete', 'Tombstone', 'Announce', 'Like', 'Follow',
41 'Accept', 'Reject', 'Undo', 'Add', 'Remove', 'Flag', 'Document', 'Image', 'Audio', 'Video',
42 'Mention', 'Link', 'Collection', 'OrderedCollection', 'OrderedCollectionPage',
43]);
44
45// The extension terms = exactly the keys declared in AP_CONTEXT's term-definition object.
46const ctxTerms = new Set();
47for (const part of AP.AP_CONTEXT) if (part && typeof part === 'object') for (const k of Object.keys(part)) ctxTerms.add(k);
48const allowed = new Set([...AS2, ...ctxTerms]);
49
50// Collect every property key + every `type` string value, recursively.
51function collect(obj, keys = new Set()) {
52 if (Array.isArray(obj)) { for (const x of obj) collect(x, keys); return keys; }
53 if (obj && typeof obj === 'object') {
54 for (const [k, v] of Object.entries(obj)) {
55 keys.add(k);
56 if (k === 'type' && typeof v === 'string') keys.add(v);
57 if (/Map$/.test(k)) continue; // a @language map (contentMap/…): its keys are BCP-47 tags, not vocab terms
58 collect(v, keys);
59 }
60 }
61 return keys;
62}
63function assertValid(obj, label) {
64 const undeclared = [...collect(obj)].filter((k) => !allowed.has(k));
65 assert.deepEqual(undeclared, [],
66 `${label}: undeclared AS2/JSON-LD term(s) — declare in AP_CONTEXT (extension) or the AS2 allowlist: ${undeclared.join(', ')}`);
67}
68
69// Seed one site that exercises the extension-heavy actor fields (profile links → PropertyValue,
70// photo → icon, primary → featured).
71db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@test', 'x', 'god');
72db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary, profile_links, profile_photo) VALUES (?,?,?,?,?,?,?)')
73 .run('s1', 'demo', 'Demo', 'u1', 1, JSON.stringify([{ platform: 'website', url: 'https://x.test' }]), '/media/x.png');
74const site = db.prepare('SELECT * FROM sites WHERE id = ?').get('s1');
75site.primary_slug = 'demo';
76
77// Kitchen-sink note: nsfw (→ sensitive + summary), a hashtag (→ Hashtag), a cover (→ attachment).
78const post = {
79 id: 'p1', slug: 'hello', title: 'Hi', content: '<p>hello #music</p>',
80 nsfw: 1, content_warning: 'cw', tags: JSON.stringify(['mood']),
81 cover_image_url: '/media/c.webp', cover_alt: 'A cover', language: 'en',
82 published_at: '2026-01-01T00:00:00Z', created_at: '2026-01-01T00:00:00Z',
83};
84
85test('actor is valid AS2 (every term declared)', () => assertValid(AP.buildActor(BASE, site), 'actor'));
86test('create+note is valid AS2 (every term declared)', () => assertValid(AP.buildCreate(BASE, site, post), 'create/note'));
87test('outbox/followers/featured collections are valid AS2', () => {
88 assertValid(AP.buildOutbox(BASE, site, [post]), 'outbox');
89 assertValid(AP.buildFollowers(BASE, site, 3), 'followers');
90 assertValid(AP.buildFollowing(BASE, site, 2), 'following');
91 assertValid(AP.buildFeatured(BASE, site, [post]), 'featured');
92});
93test('AP_CONTEXT declares every extension term we rely on', () => {
94 for (const t of ['sensitive', 'Hashtag', 'manuallyApprovesFollowers', 'discoverable', 'featured', 'PropertyValue', 'embedUrl'])
95 assert.ok(ctxTerms.has(t), `AP_CONTEXT must declare "${t}"`);
96});
Note: See TracBrowser for help on using the repository browser.