source: Klonkt/test/activitypub-as2.test.js@ 26c5f71

main
Last change on this file since 26c5f71 was 6eab7e9, checked in by Robin Genis <roboburr@…>, 6 weeks ago

Beschikbaarheid van guardians (FEP-633c 3.6): away, dormant, lapse

De Klonkt-kant van het beschikbaarheidsvoorstel, nagemaakt zoals eerst in de
daemon gevalideerd (shaer-8z7): dezelfde toestanden, dezelfde regels, dezelfde
weigeringen. De spiegel-tests dragen dezelfde namen als de daemon-tests, zodat
drift tussen de twee backends opvalt als een falende test met dezelfde woorden.

De kern is guardianship/availability.js: drie toestanden per (ward, guardian),
met als regel boven alles dat een antwoord alles herstelt, tot en met een
lopende lapse. Elke geverifieerde inbox-activiteit en elke C2S-handeling van
een guardian herstelt hem en annuleert een lapse tegen hem, nog voor er naar de
activiteit gekeken wordt. Bewust achter de handtekening-poort: een ongeverifieerde
bewering oma te zijn mag oma niet wakker maken.

Afwezig komt binnen over beide wegen: S2S als directe note met shaer:away en
endTime van een guardian elders (het gewone geval), en C2S als een guardian
hier zich afmeldt; die note draagt de marker mee naar wards elders en wordt
voor wards op deze instance direct toegepast, want een lokale inbox ontvangt
zijn eigen bezorging niet. Zonder (toekomstig) einde faalt het luid met 400,
precies zoals de daemon weigert.

Slapend volgt alleen uit onbeantwoorde direct geadresseerde verzoeken; de
follow-gating registreert die nu als bewijs. De markering notificeert verplicht
via protocol en de 6-handle, eenmalig op de overgang, centraal bedraad zodat
elke plek waar een promotie kan gebeuren hetzelfde notificeert.

De drempel van 3.5 rekent voortaan over de beschikbare set: de follow-quorums
en de gated settings allebei. De test die het waarom draagt: vijf guardians van
wie twee weg zijn gaven een drempel van drie die de twee levenden nooit haalden;
over de beschikbare set beslissen zij weer.

De lapse loopt over dezelfde draden als de gated settings: een Offer van
shaer:Lapse opent op de server van het kind, Accept/Reject stemt, het venster
loopt altijd vol, en de voltooiing verwijdert de relatie met de
nooit-leeg-grens uit 3.4 als tweede slot eronder. De offers-queue draagt de
lopende lapses en de nieuwe owner-only guardians-queue de beschikbaarheid, in
precies de vorm die de daemon serveert, dus de Shaer-apps van gisteren werken
zonder wijziging.

Changed files:
src/config/database.js

  • tabellen ap_guardian_attention, ap_attention_requests, ap_lapses
  • kolom ap_outbox.away_until

src/services/guardianship/handshake.js

  • Offer van shaer:Lapse (S2S en C2S), lapse-stemmen op Accept/Reject, one-answer op elke C2S-handeling

src/services/guardianship/gated.js

  • tally en voortgang over de beschikbare set; een stem is een antwoord

src/services/guardianship/notes.js

  • awayProps: shaer:away plus endTime op de uitgaande directe note

src/services/guardianship/delivery.js

  • away_until door het directe pad heen

src/services/guardianship/queues.js

  • guardiansCollection; offersCollection draagt de lapses

src/services/guardianship/index.js

  • exports

src/services/ActivityPubService.js

  • one-answer achter de handtekening-poort
  • away-ingest op het mention-pad en het C2S-directe pad
  • dormancy-bewijs op de follow-gating; quorum over de beschikbare set
  • de notificatieplicht van 3.6.2, een keer bedraad
  • buildReplyNote draagt awayProps

src/routes/activitypub.js

  • owner-only route /queues/guardians

src/routes/guardian.js

  • dashboard-besluit is een antwoord; quorum over de beschikbare set

src/services/guardianship/relations.js

  • guardians-queue aangekondigd in shaer:queues

test/activitypub-as2.test.js

  • guardians toegevoegd aan de queue-sleutels

New file:
src/services/guardianship/availability.js

  • de toestandsmachine, de lapse en de endTime-parser

test/availability.test.js

  • veertien spiegel-tests van de daemon, tot en met de volle lapse-flow over de S2S-draad en het vijf-guardians-rekenvoorbeeld

remarks: de PWA toont de beschikbaarheid nog niet (chips in het paneel per
kind en een lapse-kaart komen apart); de echte kruis-implementatie-testbank
blijft open op shaer-6d9. Klonkt heeft geen pinbare klok zoals de daemon; de
tests dateren bewijs terug in plaats van de tijd vooruit te zetten, en dat
staat er als kanttekening bij. Niet uitgerold.

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

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