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

main
Last change on this file since abcf51a was abcf51a, checked in by Robin <roboburr@…>, 4 weeks ago

Een track wijst naar de post die hem uitbrengt

Robins waarneming: Shaer toont sinds fb22f78 losse, niet-werkende
track-kaarten. Dat is mijn regressie -- ik zette Create(Audio) in de outbox
voor Funkwhale, en Shaer bouwt zijn HomeBase-feed uit precies die outbox.
noteFrom() daar kijkt niet naar type, dus een Audio wordt een C2SNote met lege
content, en onze url is een Link-array die as? String niet vangt.

Robins voorstel was beter dan wegfilteren: koppel ze aan de hostende post en
filter DAAROP. Die relatie bestond al, maar alleen in posts.content --
[[track:id]], [[playlist:id]], [[album:naam]] -- en nergens op de draad.

Nu wel, met twee velden die allebei al bestonden:

context AS2-KERN, "de context waarbinnen dit object bestaat". Wijst naar

de Note van de post. Funkwhale kijkt er niet naar, dus puur
additief.

url[0] de text/html-link naar de postpagina, vooraan zoals Funkwhale zijn

trackpagina zet. Dit veld lieten we bewust leeg omdat Klonkt geen
trackpagina heeft -- maar de post IS waar je het kunt horen.

Daarmee wordt de filterregel in Shaer principieel in plaats van bot: verberg
een Audio waarvan de context al in je feed staat. Een track die in GEEN post
zit houdt geen context en blijft zichtbaar -- dat is de enige plek waar je hem
zou zien.

Rangorde als een track op meerdere manieren is ingesloten: rechtstreeks boven
playlist boven albumnaam, en bij gelijke rang de nieuwste post. Specifieker
gaat dus voor recenter; daar staat een test op. Een concept telt niet als host.

EEN ZOEKOPDRACHT VOOR ALLES, niet per track: drie LIKE-scans maal
tweehonderd nummers is zeshonderd scans, en de collectie loopt over elke open
track. trackHostPosts levert een map, en die gaat als optie mee door de
collectie, de playlist en de outbox.

8 tests. context erbij in de AS2-allowlist -- het is een kernterm.

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

  • Property mode set to 100644
File size: 6.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 // AS2-kern: de context waarbinnen een object bestaat. Een track wijst ermee
29 // naar de post die hem uitbrengt (shaer-0nh).
30 'context',
31 'attachment', 'tag', 'icon', 'image', 'duration',
32 'contentMap', 'nameMap', 'summaryMap', // AS2 @language-map counterparts of content/name/summary
33 'totalItems', 'orderedItems', 'items', 'first', 'last', 'partOf', 'next', 'prev',
34 'preferredUsername', 'inbox', 'outbox', 'followers', 'following', 'endpoints', 'sharedInbox',
35 // ActivityPub §5.6: the private blocked collection (owner-only GET).
36 'blocked',
37 // ActivityPub §4.1: supplementary collections on the actor — Klonkt wijst
38 // ermee naar de playlist-lijst (shaer-ayc).
39 'streams',
40 // FEP-633c (Guardians): the owner-only dashboard queues on the actor; the
41 // sub-keys are the daemon-contract collection names the Shaer clients read.
42 // `guardians` is the availability queue (3.6.1: never public, owner-only).
43 // `outgoingFollows` is §5.3 turned around: the ward's own follow requests,
44 // waiting for the guardians (shaer-p729).
45 // `help` is de vragenlijst met haar STAAT (5.2.1): de apps lazen hulpvragen uit
46 // de feed en wisten niet of er al iemand op af was.
47 'shaer:queues', 'offers', 'follows', 'outgoingFollows', 'wards', 'guardians', 'help',
48 // Elke OPEN hulpvraag zit in de collectie; daarom mag een app uit afwezigheid
49 // concluderen dat iets niet open is (shaer-6wt).
50 'shaer:openComplete',
51 // ActivityPub §4.1 `endpoints` vocabulary (same category as sharedInbox), used for C2S.
52 'oauthAuthorizationEndpoint', 'oauthTokenEndpoint', 'uploadMedia',
53 'publicKey', 'owner', 'publicKeyPem',
54 'Note', 'Person', 'Create', 'Update', 'Delete', 'Tombstone', 'Announce', 'Like', 'Follow',
55 'Accept', 'Reject', 'Undo', 'Add', 'Remove', 'Flag', 'Document', 'Image', 'Audio', 'Video',
56 'Mention', 'Link', 'Collection', 'OrderedCollection', 'OrderedCollectionPage',
57]);
58
59// The extension terms = exactly the keys declared in AP_CONTEXT's term-definition object.
60const ctxTerms = new Set();
61for (const part of AP.AP_CONTEXT) if (part && typeof part === 'object') for (const k of Object.keys(part)) ctxTerms.add(k);
62const allowed = new Set([...AS2, ...ctxTerms]);
63
64// Collect every property key + every `type` string value, recursively.
65function collect(obj, keys = new Set()) {
66 if (Array.isArray(obj)) { for (const x of obj) collect(x, keys); return keys; }
67 if (obj && typeof obj === 'object') {
68 for (const [k, v] of Object.entries(obj)) {
69 keys.add(k);
70 if (k === 'type' && typeof v === 'string') keys.add(v);
71 if (/Map$/.test(k)) continue; // a @language map (contentMap/…): its keys are BCP-47 tags, not vocab terms
72 collect(v, keys);
73 }
74 }
75 return keys;
76}
77function assertValid(obj, label) {
78 const undeclared = [...collect(obj)].filter((k) => !allowed.has(k));
79 assert.deepEqual(undeclared, [],
80 `${label}: undeclared AS2/JSON-LD term(s) — declare in AP_CONTEXT (extension) or the AS2 allowlist: ${undeclared.join(', ')}`);
81}
82
83// Seed one site that exercises the extension-heavy actor fields (profile links → PropertyValue,
84// photo → icon, primary → featured).
85db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@test', 'x', 'god');
86db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary, profile_links, profile_photo) VALUES (?,?,?,?,?,?,?)')
87 .run('s1', 'demo', 'Demo', 'u1', 1, JSON.stringify([{ platform: 'website', url: 'https://x.test' }]), '/media/x.png');
88const site = db.prepare('SELECT * FROM sites WHERE id = ?').get('s1');
89site.primary_slug = 'demo';
90
91// Kitchen-sink note: nsfw (→ sensitive + summary), a hashtag (→ Hashtag), a cover (→ attachment).
92const post = {
93 id: 'p1', slug: 'hello', title: 'Hi', content: '<p>hello #music</p>',
94 nsfw: 1, content_warning: 'cw', tags: JSON.stringify(['mood']),
95 cover_image_url: '/media/c.webp', cover_alt: 'A cover', language: 'en',
96 published_at: '2026-01-01T00:00:00Z', created_at: '2026-01-01T00:00:00Z',
97};
98
99test('actor is valid AS2 (every term declared)', () => assertValid(AP.buildActor(BASE, site), 'actor'));
100test('create+note is valid AS2 (every term declared)', () => assertValid(AP.buildCreate(BASE, site, post), 'create/note'));
101test('outbox/followers/featured collections are valid AS2', () => {
102 assertValid(AP.buildOutbox(BASE, site, [post]), 'outbox');
103 assertValid(AP.buildFollowers(BASE, site, 3), 'followers');
104 assertValid(AP.buildFollowing(BASE, site, 2), 'following');
105 assertValid(AP.buildFeatured(BASE, site, [post]), 'featured');
106});
107test('AP_CONTEXT declares every extension term we rely on', () => {
108 for (const t of ['sensitive', 'Hashtag', 'manuallyApprovesFollowers', 'discoverable', 'featured', 'PropertyValue', 'embedUrl'])
109 assert.ok(ctxTerms.has(t), `AP_CONTEXT must declare "${t}"`);
110});
Note: See TracBrowser for help on using the repository browser.