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

main
Last change on this file since e215090 was 7e53594, checked in by Bart <bart@…>, 4 weeks ago

shaer:log: het logboek staat naast de wachtrijen, niet erin

De apps konden er niet bij. Het logboek zat achter /guardian/api/events, een
PWA-route, dus een ward kon niet lezen waarom haar eigen aanbod geweigerd was --
en dat is precies de helft die §4.2 eist.

Eigen sleutel op de actor, GET /ap/users/:slug/log, dezelfde eigenaar-only
bearer als de wachtrijen. Bewust niet onder shaer:queues: alles daar wacht op
een antwoord en dit is wat er al besloten is. Geschiedenis onderbrengen bij een
woord dat "wachtend" betekent maakt van twee dingen één, en dat is de fout die
deze module vandaag drie keer heeft opgeruimd.

type is 'shaer:Event' en geen AS2-werkwoord. De meeste soorten zijn er geen: een
lapse-stem of een opgepakte hulpvraag is geen Accept, en het zo noemen zou
netter lezen dan het is.

Clients vinden hem via het actor-document, niet via een pad dat ze zelf
verzinnen (shaer-qa0). De AS2-test viel netjes om tot shaer:log was aangemeld --
tweede keer vandaag dat die het vangt.

De daemon volgt (shaer-6d9): zolang die geen shaer:log serveert, liegt een
client die tegen beide praat over een van de twee.

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

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