source: Klonkt/test/activitypub-as2.test.js@ c26cc18

main
Last change on this file since c26cc18 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
RevLine 
[432831c]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',
[0688b5f]29 'contentMap', 'nameMap', 'summaryMap', // AS2 @language-map counterparts of content/name/summary
[432831c]30 'totalItems', 'orderedItems', 'items', 'first', 'last', 'partOf', 'next', 'prev',
31 'preferredUsername', 'inbox', 'outbox', 'followers', 'following', 'endpoints', 'sharedInbox',
[8b07c12]32 // ActivityPub §5.6: the private blocked collection (owner-only GET).
33 'blocked',
[6b5d7da]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',
[d49b60b]37 // ActivityPub §4.1 `endpoints` vocabulary (same category as sharedInbox), used for C2S.
38 'oauthAuthorizationEndpoint', 'oauthTokenEndpoint', 'uploadMedia',
[432831c]39 'publicKey', 'owner', 'publicKeyPem',
40 'Note', 'Person', 'Create', 'Update', 'Delete', 'Tombstone', 'Announce', 'Like', 'Follow',
[1c2dcba]41 'Accept', 'Reject', 'Undo', 'Add', 'Remove', 'Flag', 'Document', 'Image', 'Audio', 'Video',
[432831c]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);
[0688b5f]57 if (/Map$/.test(k)) continue; // a @language map (contentMap/…): its keys are BCP-47 tags, not vocab terms
[432831c]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']),
[0688b5f]81 cover_image_url: '/media/c.webp', cover_alt: 'A cover', language: 'en',
[432831c]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.