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

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

Open hulpvragen worden nooit afgekapt (shaer-6wt)

Ik vond dit zelf en zette het op P3: "vergt tientallen hulpvragen bij een
guardian". Bart: "precies de jeugdzorgmedewerker."

DAT IS GEEN RANDGEVAL MAAR EEN CASELOAD. Een professionele voogd heeft geen
handvol wards; voor hem zijn tientallen open hulpvragen een gewone dinsdag. De
gebruiker die deze machinerie het hardst nodig heeft was precies degene voor wie
hij brak, en ik had hem als uitzondering weggeschreven.

Wat er misging: de queue kapte op 50. Een oudere vraag zat er niet in, de app
vond geen staat, en toonde hem -- terecht, want bij twijfel OPEN -- alsof er nog
iemand op moest. Een allang afgehandelde hulpvraag die weer om aandacht vraagt.

De regel bij-twijfel-open blijft; die is goed. Wat verandert is dat de twijfel
verdwijnt: OPEN vragen worden nooit afgekapt, dus mag een app concluderen dat wat
er niet in staat ook niet open is. De geschiedenis mag wel afgekapt -- die vraagt
niets, en staat nog gewoon op de server.

shaer:openComplete op de collectie zegt dat die gevolgtrekking is toegestaan.
Ontbreekt de vlag (een oudere server), dan valt de app terug op bij-twijfel-open,
en dat is de veilige kant.

Toets met 120 open vragen. Suite 720/720; met de afkap terug valt er een om.

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