Ignore:
Timestamp:
08/13/2026 08:50:19 PM (4 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
34a0053
Parents:
44dfbd2
Message:

Een verhuisd account gaat op slot aan de uitgaande kant

Robins vraag: "nu kan je er nog alles mee". Klopt. moved_to deed maar twee dingen,
de actor-wegwijzer en een banner op de homepage. Posten, volgen, liken, boosten en
reageren kon gewoon door, en dat federeerde de wereld in.

Drie dingen gingen daar mis. Nieuwe posts kregen een object-URI op een adres dat je
hebt opgezegd, en die URI's overleven het domein niet. Je volgers waren al verhuisd,
dus je postte in het niets terwijl het lijkt of je post. En een server die je
movedTo ziet én tegelijk verse activiteit van dat adres krijgt, krijgt
tegenstrijdige signalen over de verhuizing.

DE POORT STAAT IN DE SERVICE, NIET OP DE KNOPPEN. Een C2S-client (Shaer) praat
rechtstreeks met deze functies en zou langs een verborgen knop lopen. De UI volgt
de poort.

Dicht: posten, reageren, volgen, liken, boosten, stemmen, en een tweede verhuizing.
Open: de actor met movedTo, webfinger, bestaande posts, de outbox, alles inkomend
(reacties op oude posts blijven binnenkomen en leesbaar), je eigen beheer (archief
en volglijst exporteren), ontvolgen, en rapporteren. Dat laatste is een
veiligheidsklep, geen inhoud maken.

Omkeerbaar: moved_to leegmaken heft het slot op. Een verhuizing kan mislukken.

setReaction is APART gepoort, want die schrijft alleen de lokale vlag; het
versturen gebeurt elders. Zonder die tweede poort zag je een like staan die nooit
de deur uit was: dezelfde halve toestand die vandaag al twee keer opdook.

Changed files:
src/services/ActivityPubService.js

  • movedLock() en movedRefusal(), met de scheidslijn in het commentaar
  • poorten in deliverCreate, deliverReply, followActor, sendInteraction, voteOnRemotePoll, setReaction en moveAccount

src/routes/posts.js

  • 'moved' krijgt een eigen melding: het is een weigering met een reden, niet een mislukking, en "Volgen mislukt" laat je zoeken naar een storing die er niet is
  • connect krijgt movedTo mee

src/views/pages/connect.ejs

  • banner in plaats van het volgformulier; het volglijst-blok BLIJFT, want je archief meenemen moet je juist kunnen op een verhuisd account

src/services/i18n.js

  • drie teksten in nl, en en de

New file:
test/moved-lockdown.test.js

  • 11 tests die per soort vastleggen wat dicht gaat en wat open blijft
  • inclusief: een half ingevuld moved_to zet niets op slot, ontvolgen mag nog, en de wegwijzer plus de inbox blijven staan

remarks: tegenproef gedaan. Zonder de poort vallen 8 van de 11 om en blijven de
drie "blijft open"-tests groen. Suite 949 in UTC en Europe/Amsterdam. De pagina in
beide standen gerenderd: formulier weg, banner er, exportblok blijft, geen
onvertaalde sleutels.

Doorsturen van reacties op oude posts is bewust NIET gebouwd. Zo'n reply wijst naar
een object-URI met de oude base, en findThreadTarget matcht alleen op onze eigen
base: hij zou als wees binnenkomen. Om het te laten werken zou de nieuwe instance
objecten moeten claimen op een domein dat hij niet beheert, en dat is precies waar
de sleutelbinding van vanmiddag tegen beschermt. Antwoorden doe je vanaf het nieuwe
account via de bestaande bladwijzerroute; dan blijft elke URI bij wie hem hoort.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r44dfbd2 r4766720  
    431431  // and publishers should stop delivering here.
    432432  if (site.moved_to && /^https?:\/\//i.test(String(site.moved_to))) actor.movedTo = String(site.moved_to);
     433  // Zie movedLock() verderop: het serveren van movedTo is de ENE helft, het
     434  // stilzetten van de uitgaande kant de andere.
    433435  // De MusicBrainz-koppeling van de artiest (shaer-mbz). Alleen als hij ZELF
    434436  // gekozen heeft -- er staat niets als er niets gekoppeld is, want een lege
     
    28162818}
    28172819
     2820// ── Op slot na een verhuizing (FEP-7628) ──────────────────────────
     2821//
     2822// Een verhuisd account serveert `movedTo` en is daarmee dood verklaard. Toch kon
     2823// je er gewoon op posten, volgen, liken en reageren, en dat federeerde vrolijk
     2824// de wereld in. Drie dingen gaan daar mis:
     2825//
     2826//   - Nieuwe posts krijgen een object-URI op een adres dat je hebt opgezegd. Die
     2827//     URI's overleven het domein niet, en de reacties erop ook niet.
     2828//   - Je volgers zijn al verhuisd, dus je post in het niets terwijl het lijkt of
     2829//     je post.
     2830//   - Een server die je movedTo ziet EN tegelijk verse activiteit van dat adres
     2831//     krijgt, krijgt tegenstrijdige signalen over de verhuizing.
     2832//
     2833// Daarom staat de poort op de UITGAANDE kant en niet op de knoppen: een
     2834// C2S-client (Shaer) praat rechtstreeks met deze functies en zou anders zo langs
     2835// een verborgen knop lopen. De UI volgt de poort, niet andersom.
     2836//
     2837// WAT DICHT GAAT: posten, reageren, volgen, liken, boosten, stemmen, en een
     2838// tweede verhuizing.
     2839// WAT OPEN BLIJFT: alles wat de wegwijzer draagt (de actor, webfinger, je
     2840// bestaande posts, de outbox), alles inkomend (reacties op oude posts blijven
     2841// binnenkomen en leesbaar), je eigen beheer (archief exporteren, volglijst
     2842// downloaden), en ontvolgen -- opruimen mag altijd.
     2843// Rapporteren blijft OOK open: dat is een veiligheidsklep, geen inhoud maken.
     2844//
     2845// OMKEERBAAR: `moved_to` leegmaken heft het slot op. Een verhuizing kan mislukken
     2846// en dan moet je terug kunnen.
     2847export function movedLock(site) {
     2848  const to = site && site.moved_to && /^https?:\/\//i.test(String(site.moved_to))
     2849    ? String(site.moved_to) : null;
     2850  return to ? { locked: true, movedTo: to } : { locked: false, movedTo: null };
     2851}
     2852
     2853/** Weigering in de vorm die de aanroepers al kennen: een object met `error`. */
     2854function movedRefusal(site, wat) {
     2855  const l = movedLock(site);
     2856  if (!l.locked) return null;
     2857  console.warn('[AP] geweigerd, dit account is verhuisd:', wat, '→', l.movedTo);
     2858  return { error: 'moved', movedTo: l.movedTo };
     2859}
     2860
    28182861// Deliver a new post as Create(Note) to all followers' inboxes (fire-and-forget).
    28192862// Needs PUBLIC_BASE_URL (absolute URLs); no-op without followers or base.
    28202863export async function deliverCreate(site, post) {
     2864  if (movedLock(site).locked) { console.warn('[AP] Create niet bezorgd, account verhuisd:', site && site.slug); return; }
    28212865  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
    28222866  if (!base || !site || !site.slug) return;
     
    35923636
    35933637export async function deliverReply(site, { postId, postSlug, parent, text, html, language, attachments, mentions, visibility }) {
     3638  const _mv = movedRefusal(site, 'reply'); if (_mv) return _mv;
    35943639  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
    35953640  // Rich replies: `html` is the reply editor's HTML (sanitized here); `text` is
     
    49454990export function setReaction(slug, uri, kind, on, opts = {}) {
    49464991  if (!slug || !uri || (kind !== 'like' && kind !== 'boost')) return;
     4992  // Ook hier, en niet alleen bij sendInteraction. Deze functie schrijft ALLEEN de
     4993  // lokale vlag; het versturen gebeurt elders. Zonder deze poort zou je op een
     4994  // verhuisd account een like zien staan die nooit de deur uit is gegaan, en dat
     4995  // is de halve toestand die erger is dan een duidelijke weigering.
     4996  try {
     4997    const s = db.prepare('SELECT moved_to FROM sites WHERE slug = ?').get(slug);
     4998    if (movedLock(s).locked) { console.warn('[AP] reactie geweigerd, account verhuisd:', slug, kind); return; }
     4999  } catch { /* geen sites-tabel = geen verhuizing */ }
    49475000  // EEN sleutel voor beide bronnen. opts.flagUri is de opgeloste object-URI van
    49485001  // de aanroeper (het C2S-pad kent die uit resolveRemoteNote en dat is
     
    56275680// Follow a fediverse account by @handle (WebFinger → actor → signed Follow).
    56285681export async function followActor(site, handle, autoBoost = false, { approved = false } = {}) {
     5682  const _mv = movedRefusal(site, 'follow'); if (_mv) return _mv;
    56295683  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
    56305684  if (!base || !site || !site.slug) return { error: 'config' };
     
    58465900 */
    58475901export async function moveAccount(site, targetRaw, { fetchActorFn = null, deliverFn = null } = {}) {
     5902  // Al verhuisd? Dan eerst het slot eraf (moved_to leegmaken). Anders stapel je
     5903  // wegwijzers op elkaar en weet niemand meer waar de keten eindigt.
    58485904  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
    58495905  if (!base || !site || !site.slug) return { error: 'config' };
     5906  if (movedLock(site).locked) return { error: 'already_moved', movedTo: movedLock(site).movedTo };
    58505907  try {
    58515908    // Was een harde weigering voor elk bewaakt account (shaer-tge); sinds 8-8
     
    61646221// Send a Like or Announce (boost) on a remote note FROM this site.
    61656222export async function sendInteraction(site, kind, targetNoteId, authorUri) {
     6223  const _mv = movedRefusal(site, `interaction:${kind}`); if (_mv) return _mv;
    61666224  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
    61676225  if (!base || !site || !site.slug || !targetNoteId) return { error: 'config' };
     
    63476405// by URL, not just ones from accounts you follow (which go through voteOnPoll via /news).
    63486406export async function voteOnRemotePoll(site, questionUrl, choices) {
     6407  const _mv = movedRefusal(site, 'poll-vote'); if (_mv) return _mv;
    63496408  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
    63506409  if (!base || !site || !site.slug || !/^https?:\/\//i.test(String(questionUrl || ''))) return { error: 'config' };
Note: See TracChangeset for help on using the changeset viewer.