Ignore:
Timestamp:
07/31/2026 05:43:16 PM (6 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
bfa6fa1
Parents:
f434294
Message:

Account-verhuizingen slice 2: de uitgaande Move (FEP-7628)

De vertrekkende helft van shaer-0j2: deze Klonkt als het oude huis. Twee
eisen voordat er iets de deur uit gaat. Een: geen guardians; een warded
account verhuizen zonder de guardianship te hertargeten zou het vangnet
van het kind stil breken, dus tot shaer-tge er is weigert een bewaakt
account met een heldere melding. Twee: het nieuwe profiel claimt dit
adres in alsoKnownAs, dezelfde back-reference die elke ontvangende
server (onze slice 1 incluis) eist; zonder die claim is de Move overal
dood bij aankomst.

De Move gaat duurzaam (retry-queue) naar elke volger-inbox; hun servers
doen de re-follow. moved_to wordt vastgelegd; het serveren ervan op de
actor en het beleid van de oude site zijn slice 3. In Beheer > Sites >
bewerken zit de aankondiging als eigen formulier met bevestiging, nooit
als bijeffect van Opslaan: een verhuizing is een deur die je achter je
dichttrekt.

Changed files:
src/services/ActivityPubService.js

  • moveAccount(site, target, {fetchActorFn, deliverFn}): guardian- weigering, resolutie (URL/handle), back-reference-check, moved_to, Move naar alle volger-inboxen (shared inbox de-dupe)

src/config/database.js

  • sites.moved_to (TEXT)

src/routes/admin-sites.js

  • POST /:slug/move met per-fout een heldere redirect-melding

src/views/pages/admin-site-edit.ejs

  • Verhuizen-sectie (eigen form + confirm), toont moved_to

src/services/i18n.js

  • asite.move* in nl/en/de

test/move-actor.test.js

  • guarded weigert (niets vastgelegd), zonder back-reference weigert, happy path: moved_to + een Move per volger-inbox (shared voorop)

remarks: gebouwd op vps/main (f434294) in een worktree, los van de
lopende boost-toggle-sessie in de hoofd-clone. Suite op 370 groen.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    rf434294 r0ca7e9a4  
    38433843}
    38443844
     3845/**
     3846 * Slice 2 van shaer-0j2 (FEP-7628, DRAFT): de UITGAANDE helft — deze Klonkt
     3847 * is het oude huis en kondigt het vertrek aan. Twee eisen voordat er iets
     3848 * de deur uit gaat:
     3849 *  1. Geen guardians: een warded account verhuizen zonder de guardianship
     3850 *     te hertargeten zou het vangnet van het kind stil breken; dat is
     3851 *     shaer-tge's gated beslissing, dus tot die er is weigert een bewaakt
     3852 *     account de verhuizing.
     3853 *  2. De NIEUWE actor claimt ons in alsoKnownAs — dezelfde back-reference
     3854 *     die elke ontvangende server (onze eigen slice 1 incluis) eist. Zonder
     3855 *     die claim is de Move overal dood bij aankomst.
     3856 * De Move gaat duurzaam naar elke volger-inbox; hun servers doen de
     3857 * re-follow. `moved_to` wordt hier vastgelegd; het SERVEREN ervan op de
     3858 * actor (en het beleid van de oude site) is slice 3.
     3859 * Deps injecteerbaar voor tests (geen netwerk in node:test).
     3860 */
     3861export async function moveAccount(site, targetRaw, { fetchActorFn = null, deliverFn = null } = {}) {
     3862  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     3863  if (!base || !site || !site.slug) return { error: 'config' };
     3864  try {
     3865    const guardians = Guardianship.listGuardians(site.slug);
     3866    if (guardians.length) {
     3867      console.warn('[AP] move refused: guarded account (shaer-tge):', site.slug, '→', String(targetRaw || ''));
     3868      return { error: 'guarded_account' };
     3869    }
     3870  } catch { /* geen guardianship-tabellen = geen guardians */ }
     3871  const s = String(targetRaw || '').trim();
     3872  let targetUri = null;
     3873  if (/^https?:\/\//i.test(s)) targetUri = safeUrl(s);
     3874  else if (s.includes('@')) targetUri = await webfingerResolve(s);
     3875  if (!targetUri) return { error: 'not_found' };
     3876  const me = actorId(base, site.slug);
     3877  if (targetUri === me) return { error: 'self' };
     3878  const target = await (fetchActorFn ? fetchActorFn(targetUri) : signedGetJson(site.slug, targetUri));
     3879  if (!target || !target.id || !target.inbox) return { error: 'unreachable' };
     3880  const aka = [].concat(target.alsoKnownAs || [])
     3881    .map((a) => (typeof a === 'string' ? a : (a && a.id))).filter(Boolean);
     3882  if (!aka.includes(me)) return { error: 'no_backreference' };
     3883  db.prepare('UPDATE sites SET moved_to = ? WHERE slug = ?').run(target.id, site.slug);
     3884  const keys = getOrCreateKeys(site.slug);
     3885  const move = {
     3886    '@context': AP_CONTEXT,
     3887    id: `${me}#move-${Date.now()}-${rid()}`,
     3888    type: 'Move',
     3889    actor: me,
     3890    object: me,
     3891    target: target.id,
     3892    to: [`${me}/followers`],
     3893  };
     3894  const inboxes = [...new Set(fStmts().list.all(site.slug).map((f) => f.shared_inbox || f.inbox).filter(Boolean))];
     3895  for (const inbox of inboxes) {
     3896    await (deliverFn || deliverWithRetry)(site.slug, inbox, move, `${me}#main-key`, keys.private_pem);
     3897  }
     3898  console.log('[AP] MOVE announced:', site.slug, '→', target.id, 'naar', inboxes.length, 'inbox(en)');
     3899  return { ok: true, target: target.id, inboxes: inboxes.length };
     3900}
     3901
    38453902// FEP-633c §5.3 note (authorized fetch): true when `actorUri` is a committed
    38463903/**
     
    43594416  getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, setMyReaction, getMyReactions, buildReplyNote, getOutboxNote, getSentNotes, deliverReply, resolveRemoteNote,
    43604417  listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote,
    4361   webfingerResolve, followActor, resolveRemoteActor, unfollowActor, handleMoveInbox, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, getDirectMessages, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,
     4418  webfingerResolve, followActor, resolveRemoteActor, unfollowActor, handleMoveInbox, moveAccount, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, getDirectMessages, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,
    43624419  acceptGatedFollow, rejectGatedFollow, isWardGuardian, outboxAudience, sendFollowDecision,
    43634420  parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs,
Note: See TracChangeset for help on using the changeset viewer.