Changeset ccaa530 in Klonkt for src/routes/admin-sites.js


Ignore:
Timestamp:
07/31/2026 01:34:46 PM (6 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
5462bab
Parents:
2dd1dc4
Message:

Account-verhuizingen: inkomende Move plus alsoKnownAs-aliassen (FEP-7628)

De ontvangende helft van accountverhuizingen, in twee delen die samen een
verhuizing NAAR Klonkt mogelijk maken (shaer-0j2, slice 1).

Deel 1: een account dat onze sites volgen verhuist. Op een geldige Move volgt
elke lokale volger automatisch mee naar het nieuwe adres, met behoud van de
eigen auto-boost-keuze. Geldigheid heeft twee onafhankelijke benen en beide
zijn verplicht: de ondertekenaar is zelf partij in de verhuizing (oude actor
in push-modus of nieuwe actor in pull-modus), en de nieuwe actor claimt de
oude identiteit in alsoKnownAs. Zonder het eerste kan elke ondertekende
vreemde onze follows ompointen; zonder het tweede kan wie een kant beheerst
de volgers van de andere kant kapen. Herbezorging is idempotent, een
geblokkeerde bestemming wordt geweigerd, en guardianship-relaties worden
bewust NIET geretarget: een guardian is een beveiligingsanker en verhuist
pas onder de regels van shaer-tge.

Deel 2: eigen aliassen instellen via Beheer, Sites, site bewerken. Een regel
per oud account, als @naam@server of als actor-URL. Handles resolven via
WebFinger op het moment van opslaan, zodat een typfout direct zichtbaar is
in plaats van pas bij een mislukkende Move op de oude server. De aliassen
verschijnen als alsoKnownAs op de publieke actor; dat is de claim die de
oude server controleert voor hij volgers hierheen verhuist.

Let op: FEP-7628 heeft status DRAFT. De vorm is sinds 2019 de facto
Mastodon-standaard, maar de spec kan nog wijzigen.

Changed files:
src/services/ActivityPubService.js

  • Move toegevoegd aan de GATED-lijst (handtekening verplicht)
  • dispatch-blok voor Move, voor de Follow-afhandeling (geen race met de re-Follow-golf van de nieuwe actor)
  • handleMoveInbox met injecteerbare afhankelijkheden voor de tests
  • alsoKnownAs en movedTo als JSON-LD-termen in de context
  • buildActor publiceert alsoKnownAs uit sites.ap_aliases, eigen id en niet-URLs gefilterd

src/config/database.js

  • kolom sites.ap_aliases (JSON-array van actor-URIs)

src/routes/admin-sites.js

  • parseApAliases: max 5, dedupe, WebFinger-resolutie bij opslaan, fout met de betreffende regel terug naar het formulier
  • save-route asynchroon; ap_aliases in de UPDATE
  • actor-Update naar volgers wanneer de lijst wijzigt (best-effort)

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

  • veld Fediverse-aliassen onder de profiel-links

src/services/i18n.js

  • asite.aliases en asite.aliases_hint in nl, en en de

New file:
test/move-actor.test.js

  • 11 tests: derde-partij-weigering, ontbrekend aliasbewijs, push- en pull-modus, idempotentie, al-volgend overslaan, geblokkeerd doel, misvormde activiteiten, ongesigneerd, actor-publicatie met filtering

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/admin-sites.js

    r2dd1dc4 rccaa530  
    2626import { toWebp } from '../services/ImageWebpService.js';
    2727import { mediaDir } from '../config/paths.js';
     28import AP from '../services/ActivityPubService.js';
    2829
    2930
     
    7172  }
    7273  return arr.length ? JSON.stringify(arr) : null;
     74}
     75
     76/**
     77 * FEP-7628 aliases (alsoKnownAs): one former identity per line, as an actor
     78 * URL or an @user@host handle. Handles resolve via WebFinger AT SAVE TIME on
     79 * purpose — a typo'd alias that silently lands on the actor would make a later
     80 * Move fail at the old server with no hint why. Throws the offending line.
     81 */
     82async function parseApAliases(raw, ownActorUri) {
     83  const lines = String(raw || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
     84  if (lines.length > 5) throw new Error(lines[5] + ' (max 5)');
     85  const out = [];
     86  for (const line of lines) {
     87    let uri = null;
     88    if (/^https?:\/\//i.test(line)) uri = line;
     89    else if (line.includes('@')) uri = await AP.webfingerResolve(line).catch(() => null);
     90    if (!uri) throw new Error(line);
     91    if (uri === ownActorUri) continue; // claiming yourself adds nothing
     92    if (!out.includes(uri)) out.push(uri);
     93  }
     94  return out;
    7395}
    7496
     
    256278  }
    257279
     280  let apAliases = '';
     281  try { apAliases = (JSON.parse(site.ap_aliases || '[]') || []).join('\n'); } catch { /* show empty on malformed */ }
     282
    258283  renderPage(req, res, 'pages/admin-site-edit', {
    259284    pageTitleKey: 'admin.t_editsite', pageTitleVars: { title: site.title },
     
    266291    platforms: listPlatforms(),
    267292    parsedLinks,
     293    apAliases,
    268294    success: req.query.success || null,
    269295    error: req.query.error || null,
     
    272298
    273299// ==================== SAVE ====================
    274 router.post('/:slug/save', requireSiteManagerBySlug, (req, res) => {
    275   const site = db.prepare('SELECT id FROM sites WHERE slug = ?').get(req.params.slug);
     300router.post('/:slug/save', requireSiteManagerBySlug, async (req, res) => {
     301  const site = db.prepare('SELECT id, ap_aliases FROM sites WHERE slug = ?').get(req.params.slug);
    276302  if (!site) return res.redirect('/admin/sites?error=Not+found');
    277303
     
    279305  const feedViewDef = f.feed_view_default === 'grid' ? 'grid' : 'timeline';
    280306  const profileLinksJson = buildProfileLinks(f);
     307
     308  // FEP-7628 aliases — validated/resolved before anything is written.
     309  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     310  let apAliasesJson = null;
     311  try {
     312    const arr = await parseApAliases(f.ap_aliases, AP.actorId(base, req.params.slug));
     313    apAliasesJson = arr.length ? JSON.stringify(arr) : null;
     314  } catch (e) {
     315    return res.redirect(`/admin/sites/${req.params.slug}/edit?error=` + encodeURIComponent(`Alias niet herkend of niet vindbaar: ${e.message}`));
     316  }
    281317
    282318  // theme_override: only accept the three legal values. Empty string means
     
    294330      profile_enabled = ?,
    295331      profile_links = ?,
     332      ap_aliases = ?,
    296333      is_public = ?, robots_index = ?, require_login_to_comment = ?,
    297334      enable_audio_player = ?,
     
    312349    f.profile_enabled ? 1 : 0,
    313350    profileLinksJson,
     351    apAliasesJson,
    314352    f.is_public ? 1 : 0,
    315353    f.robots_index ? 1 : 0,
     
    336374  }
    337375
     376  // Alias change → broadcast an actor Update so remote caches refresh. The old
     377  // server re-fetches the actor live during a Move anyway; this is freshness,
     378  // not correctness, hence best-effort.
     379  if ((site.ap_aliases || null) !== apAliasesJson) {
     380    try {
     381      const fresh = db.prepare('SELECT * FROM sites WHERE id = ?').get(site.id);
     382      AP.deliverActorUpdate(fresh).catch(() => {});
     383    } catch { /* never blocks the save */ }
     384  }
     385
    338386  res.redirect(`/admin/sites/${req.params.slug}/edit?success=` + encodeURIComponent('Opgeslagen'));
    339387});
Note: See TracChangeset for help on using the changeset viewer.