Changeset fbfd7a1 in Klonkt for src/routes/activitypub.js


Ignore:
Timestamp:
08/13/2026 10:34:36 PM (4 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
6c4ff7e
Parents:
4101c89
Message:

Je berichten verhuizen mee: FEP-1580 plus een webinterface ervoor

FEP-7628 verhuist je volgers en zegt zelf dat de inhoud een ander probleem is.
Dat probleem stond open: na een Move bleven je berichten op de oude instantie
staan, en elke reactie van een derde wees naar een URI die verdwijnt zodra dat
domein opgezegd wordt. FEP-1580 regelt dat, status DRAFT.

DE AUTORISATIE IS DE MOVE, NIET EEN CODE. De bronkant behandelt een ondertekend
verzoek namens de doel-actor alsof de bron-actor het zelf deed. Dat mag omdat
moveAccount() no_backreference weigert: moved_to staat er alleen als de
doel-actor ons al in alsoKnownAs had. Beide kanten hebben ooit ja gezegd, dus er
is geen tweede vertrouwensmechanisme nodig. Een typefout komt hier niet binnen,
want die haalt de move zelf niet. Dat dit veilig is leunt op de keyId-binding
uit shaer-xd8i: zonder die controle is "wie tekende dit" te zacht om je hele
geschiedenis aan af te geven.

NIEUWE IDS ZIJN GEEN BUG, DE VERTAALTABEL IS HET ANTWOORD. Een verhuisd bericht
krijgt een eigen URI, want het staat op een ander domein. De migration-collectie
mapt oud naar nieuw en derden lezen die om hun eigen verwijzingen bij te werken.
Zonder die collectie is de draad kapot, met die collectie is het een
verhuisbericht. Niet-publieke items staan er alleen in voor wie ze mocht zien:
een lijst met de URIs van je fan-only posts is een lek, ook zonder de inhoud.

Er gaat geen Create de deur uit. Je volgers hebben die berichten jaren geleden
al gezien; driehonderd posts die als nieuw de tijdlijn in klateren is geen
verhuizing maar spam.

Daarnaast /admin/migrate: exporteren, importeren en ophalen via de
webinterface, zodat verhuizen geen SSH-toegang meer vraagt. Importeren gaat
altijd eerst droog, met een verslag en pas daarna een knop die het echt doet.

Getest op twee draaiende instanties, A verhuisd naar B via de echte
moveAccount. Anoniem zag A 3 van de 4 berichten; ondertekend als de doel-actor
kwamen alle 4 mee, inclusief de fan-only. Titel, webadres en publicatiedatum
blijven staan. Media komt echt over: gedownload, in de mediatabel, B serveert
het. Migration-collectie 4 rijen totaal, 3 publiek.

Changed files:
src/services/ActivityPubService.js

  • isMoveTarget: het hele autorisatiepredicaat van de bronkant
  • outboxAudience en mayReadNote: de doel-actor krijgt onze eigen kijkrechten
  • buildActor adverteert migration en moves, ook leeg (de FEP wijst er apart op dat "niets verhuisd" anders niet te onderscheiden is van "kent dit niet")
  • signedGetJson geexporteerd, de ingest heeft hem nodig
  • isMoveTarget en signedGetJson in de default export (movedLock verstopte zich een dag eerder precies zo)

src/services/ap-core.js

  • FEP-1580-termen in de JSON-LD-context

src/services/ArchiveImportService.js

  • een import uit een zip vult dezelfde vertaaltabel; de spec wil dat een geexporteerde collectie identiek behandeld wordt

src/routes/activitypub.js

  • /ap/users/:slug/migration en /moves
  • de blocked-collectie gaat open voor de doel-actor, want zichtbaarheidsvoorkeuren moeten meeverhuizen

src/config/database.js

  • ap_migration en ap_moves, plus sites.migration_complete

src/server.js

  • /admin/migrate aangesloten

src/views/pages/admin.ejs

  • knop naar Migreren

src/services/i18n.js

  • mig.* in nl/en/de

New file:
src/services/MigrationService.js

  • de doelkant: ingest-routine, migration- en moves-collectie, statusvlag

src/routes/admin-migrate.js

  • exporteren, droog importeren, echt importeren, ophalen bij de oude Klonkt

src/views/pages/admin-migrate.ejs

  • de pagina

test/fep1580-migration.test.js

  • 22 tests over beide rollen, plus de regressietest bij 4101c89

remarks: FEP-8b32 ontbreekt volledig (shaer-j1v0), dus er staat geen
handtekening onder de moves-collectie en we zijn niet naleveringsklaar. Bewust
geen leeg proof-veld: een derde die het controleert wordt dan misleid. De DERDE
rol zit er ook niet in, Klonkt leest nog geen migration-collecties van anderen,
dus andermans verhuizing repareert onze verwijzingen nog niet. Alle betrokken
FEPs zijn DRAFT, ook 7628 die we al volgden; 1580 is vers en de auteur schrijft
zelf dat het een audit verdient.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    r4101c89 rfbfd7a1  
    2020import OAuth from '../services/OAuthService.js';
    2121import * as Guardianship from '../services/guardianship/index.js';
     22import * as Migration from '../services/MigrationService.js';
    2223import { getPrimarySite } from '../middleware/site.js';
    2324import multer from 'multer';
     
    655656// clients read it here instead of keeping their own state. Actor-kind
    656657// blocks only (domain blocks are instance policy, not an Orbit member).
    657 router.get('/ap/users/:slug/blocked', (req, res) => {
     658//
     659// FEP-1580 zet deze deur één spleet verder open: de bronkant MOET de blokkades
     660// beschikbaar maken voor de instantie waar je NAARTOE verhuist, zodat je
     661// zichtbaarheidsvoorkeuren meeverhuizen. De doelkant haalt ze als eerste op,
     662// want ze bepalen wat de rest te zien krijgt. Geen nieuwe collectie: deze
     663// bestond al en staat al op de actor, alleen de toegang verbreedt.
     664router.get('/ap/users/:slug/blocked', async (req, res) => {
    658665  const auth = OAuth.verifyBearer(req.headers.authorization);
    659   if (!auth || auth.site.slug !== req.params.slug) return res.status(403).end();
     666  let slug = (auth && auth.site.slug === req.params.slug) ? auth.site.slug : null;
     667  if (!slug && req.headers['signature']) {
     668    const verified = await AP.verifyRequest(req).catch(() => null);
     669    if (verified && verified.id && AP.isMoveTarget(req.params.slug, verified.id)) slug = req.params.slug;
     670  }
     671  if (!slug) return res.status(403).end();
    660672  const base = baseUrl(req);
    661   const items = AP.listBlocks(auth.site.slug)
     673  const items = AP.listBlocks(slug)
    662674    .filter((b) => b.kind === 'actor')
    663675    .map((b) => b.target);
    664676  AP.sendAP(res, {
    665677    '@context': AP.AP_CONTEXT,
    666     id: `${base}/ap/users/${auth.site.slug}/blocked`,
     678    id: `${base}/ap/users/${slug}/blocked`,
    667679    type: 'OrderedCollection',
    668680    totalItems: items.length,
    669681    orderedItems: items,
    670   });
     682  }, 'private, no-store');
    671683});
    672684
     
    10311043});
    10321044
     1045// ── FEP-1580: de vertaaltabel van een verhuizing ──────────────────
     1046//
     1047// Publiek leesbaar, want dat is het hele doel: een derde die een oude URI in
     1048// zijn database heeft leest hier wat de nieuwe is. Zonder deze collectie blijft
     1049// elke reactie op een verhuisd bericht naar een dood adres wijzen.
     1050//
     1051// Niet-publieke items komen er alleen in voor een lezer die ze mocht zien. De
     1052// spec: Moves voor objecten die niet aan as:Public gericht zijn MOGEN NIET
     1053// publiek getoond worden. Een lijst met de URIs van je fan-only posts is een
     1054// lek, ook al staat de inhoud er niet bij.
     1055router.get('/ap/users/:slug/migration', async (req, res) => {
     1056  const site = publicSite(req.params.slug);
     1057  if (!site) return res.status(404).end();
     1058  let alles = false;
     1059  const auth = OAuth.verifyBearer(req.headers.authorization);
     1060  if (auth && auth.site.slug === site.slug) alles = true;
     1061  else if (req.headers['signature']) {
     1062    const v = await AP.verifyRequest(req).catch(() => null);
     1063    // Een geverifieerde volger zat in het publiek van de fan-only posts, dus
     1064    // die mag ook weten waar ze heen zijn.
     1065    if (v && v.id && AP.outboxAudience(site.slug, { verifiedActor: v.id }) === 'friend') alles = true;
     1066  }
     1067  AP.sendAP(res, Migration.buildMigration(baseUrl(req), site, { page: paginaNr(req), alles }),
     1068    alles ? 'private, no-store' : undefined);
     1069});
     1070
     1071// De Moves die de vertaaltabel rechtvaardigen. Altijd publiek: een bewijs dat
     1072// je moet kunnen nakijken heeft niets aan een slot.
     1073//
     1074// LET OP: zonder FEP-8b32 (shaer-j1v0) staat hier geen handtekening onder. De
     1075// collectie is structureel goed en niet verifieerbaar, en een derde die de spec
     1076// streng volgt mag hem daarom weigeren. Bewust geen leeg proof-veld erbij.
     1077router.get('/ap/users/:slug/moves', (req, res) => {
     1078  const site = publicSite(req.params.slug);
     1079  if (!site) return res.status(404).end();
     1080  AP.sendAP(res, Migration.buildMoves(baseUrl(req), site));
     1081});
     1082
    10331083// ── Featured (pinned posts → Mastodon "Featured" tab) ─────────────
    10341084router.get('/ap/users/:slug/featured', (req, res) => {
Note: See TracChangeset for help on using the changeset viewer.