source: Klonkt/test/activitypub-as2.test.js@ 86e6a45

main
Last change on this file since 86e6a45 was 86e6a45, checked in by roboburr <roboburr@…>, 5 weeks ago

De apps krijgen de hulpstaat, en kunnen afhandelen (shaer-lgo)

Barts melding: afgehandelde hulpverzoeken blijven zichtbaar in de Shaer
GuardianshipView, en kan het afhandelen daar ook?

DIE TWEE ZIJN HETZELFDE PROBLEEM. De apps lazen hulpvragen uit de FEED -- losse
notes met een helpRequest-vlag -- en kregen de staat helemaal niet. Ze konden dus
niet weten of er al iemand op af was, en dan is een afgehandeld verzoek laten
staan nog het eerlijkste dat een app kan doen. Klonkt bewaarde de staat wel; hij
reisde alleen nergens heen.

Nu een queue help op de actor, met de staat PLAT erin: open, wie hem oppakte,
wie hem afsloot en wanneer. Een app hoeft hem niet af te leiden en kan hem dus
ook niet anders afleiden dan het paneel -- helpItemsFor is een plek, net als
wardGates. Twee berekeningen zouden twee guardians een ander beeld geven van
hetzelfde kind, en bij een reddingsboei is dat het gevaarlijkste dat er mis kan
gaan.

AFHANDELEN HOEFDE GEEN NIEUWE VORM. De markering IS al een gewone directe note
met shaer:helpPickup of shaer:helpHandled, precies zoals de zwaai. De outbox
herkent hem nu, dus de app stuurt letterlijk wat de PWA stuurt en het reist over
dezelfde bezorging naar de mede-guardians. Geen tweede weg.

Wel LOKAAL boeken, en daar staat een toets op: zonder dat zag de guardian die de
knop indrukt zijn eigen markering pas als hij bij zichzelf terugkwam, en die weg
bestaat niet.

Oppikken blijft OPEN. De faalstand hier is "iedereen denkt dat het geregeld is",
en die is gevaarlijker dan geen markering.

De AS2-toets ving dat help nog niet gedeclareerd stond op de actor -- precies
waarvoor die toets er is. Suite 718/718; zonder de lokale boeking valt er een om.

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