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/services/ActivityPubService.js

    r4101c89 rfbfd7a1  
    391391    // separate state.
    392392    blocked: `${id}/blocked`,
     393    // FEP-1580: de vertaaltabel van een verhuizing plus de Moves die hem
     394    // rechtvaardigen. Deze twee staan er ALTIJD, ook leeg, en dat is met opzet:
     395    // de FEP wijst er apart op dat "een verhuizing zonder objecten" en "een
     396    // server die dit niet kent" anders niet uit elkaar te houden zijn.
     397    migration: `${id}/migration`,
     398    moves: `${id}/moves`,
    393399    // FEP-633c §2: shaer:guardians / shaer:isGuardian / shaer:queues
    394400    // (guardianship module owns these).
     
    18391845  if (aud === 'public') return true;
    18401846  if (aud === 'direct' || !site || !actorUri) return false;
     1847  // FEP-1580: dezelfde regel als in outboxAudience, en hier net zo hard nodig.
     1848  // De outbox geeft de LIJST vrij; zonder deze tak strandt de doelinstantie
     1849  // alsnog op elke losse Note die niet publiek is.
     1850  if (isMoveTarget(site.slug, actorUri)) return true;
    18411851  try {
    18421852    const blocked = db.prepare("SELECT 1 FROM ap_blocks WHERE slug = ? AND kind = 'actor' AND target = ?").get(site.slug, actorUri);
     
    54065416 * (request-target) host date, the set verifyRequest checks.
    54075417 */
    5408 async function signedGetJson(slug, url, onStatus) {
     5418export async function signedGetJson(slug, url, onStatus) {
    54095419  try {
    54105420    const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     
    59825992  if (!verifiedActor) return 'public';
    59835993  if (isBlockedAny(verifiedActor)) return 'blocked';
     5994  // FEP-1580, Source Instance: wie ondertekend vraagt namens de actor waar wij
     5995  // NAARTOE verhuisd zijn, moet behandeld worden alsof wij het zelf vragen.
     5996  // Anders kan de nieuwe instantie alleen het publieke deel ophalen en verhuist
     5997  // je fan-only geschiedenis niet mee.
     5998  if (isMoveTarget(slug, verifiedActor)) return 'friend';
    59845999  try {
    59856000    if (db.prepare('SELECT 1 FROM ap_followers WHERE slug = ? AND actor_uri = ?').get(slug, verifiedActor)) return 'friend';
     
    59876002  if (isWardGuardian(slug, verifiedActor)) return 'friend';
    59886003  return 'public';
     6004}
     6005
     6006/**
     6007 * FEP-1580, de hele autorisatie van de bronkant in één predicaat.
     6008 *
     6009 * De spec zegt: behandel een verzoek dat namens de DOEL-actor getekend is alsof
     6010 * de BRON-actor het deed, voor zichtbaarheid en toegang. Wij hangen dat aan
     6011 * `moved_to`, en dat mag omdat moveAccount() `no_backreference` weigert: het
     6012 * veld komt er alleen te staan als de doel-actor ons al in `alsoKnownAs` had.
     6013 * Dus staat er iets, dan heeft iemand met beheer op BEIDE kanten dat gewild.
     6014 * Een typefout kan hier niet binnenkomen, want die haalt de move zelf niet.
     6015 *
     6016 * Dat dit veilig is leunt op de keyId-binding in verifyRequest (shaer-xd8i):
     6017 * zonder die controle kon een actor tekenen met de sleutel van een buurman op
     6018 * dezelfde host, en dan is "wie tekende dit" te zacht om je hele geschiedenis
     6019 * aan af te geven.
     6020 */
     6021export function isMoveTarget(slug, actorUri) {
     6022  if (!slug || !actorUri) return false;
     6023  try {
     6024    const row = db.prepare('SELECT moved_to FROM sites WHERE slug = ?').get(slug);
     6025    return !!(row && row.moved_to && row.moved_to === actorUri);
     6026  } catch { return false; }
    59896027}
    59906028
     
    67166754export default {
    67176755  movedLock,
     6756  // FEP-1580 bronkant. Vergeet je hem hier, dan werpt elke route die hem
     6757  // aanroept een 500 en lijkt het alsof de poort dicht staat terwijl hij
     6758  // ontbreekt (precies hoe movedLock zich een dag eerder verstopte).
     6759  isMoveTarget, signedGetJson,
    67186760  AP_CONTEXT, getOrCreateKeys, apWants, sendAP, actorId, noteId, stripLeadingMentions, pagedCollection,
    67196761  deriveHandle, localSlugOf, outboxSlice, PAGINA_GROOTTE,
Note: See TracChangeset for help on using the changeset viewer.