Changeset 6d5ce0c in Klonkt for src/services/guardianship


Ignore:
Timestamp:
07/29/2026 10:50:32 AM (6 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
08ab8ad
Parents:
c8e03c6
Message:

Op deze machine gedraagt elke Klonkt zich alsof hij ergens anders staat

Robins regel, en de reden staat in de logs van deze week. Twee bugs kwamen uit
hetzelfde patroon: een tweede, lokale route die een kapotte externe route
verborg. De Undo bereikte een kind op dezelfde machine nooit, en het gated
voorstel was een maand stuk over de lijn terwijl de sluiproute de stem hier
direct opschreef en het dashboard er prima uitzag.

Samenlokatie is nu een kwestie van TRANSPORT, geen beslispad. deliverToActor
geeft een activiteit voor een lokale ontvanger door aan dezelfde inbox-handler
die de lijn zou bereiken, inclusief de controle of de ondertekenaar de afzender
is. Alles daarboven weet het verschil niet meer, en dus draait elke deployment
dezelfde code.

Directe berichten deden dat nog niet. Die zochten een inbox op en POSTten
erheen, dus een bericht aan een kind op deze machine ging naar onze eigen
hostnaam en terug, of nergens heen. Nu nemen ze dezelfde loopback.

En de kern van het probleem zat in de inbox zelf: "van onze eigen actor" werd
gelezen als "van wie dan ook op deze machine". Daardoor werd elk bericht tussen
twee sites op een instantie met een 202 aangenomen en daarna weggegooid: geen
vermelding, geen afwezigheid, geen hulpvraag. Buren zijn niet wij.

Daarmee konden twee met de hand geschreven sluiproutes weg: het lokaal
wegschrijven van een afwezigheid in de C2S-outbox en in de Guardian PWA. Die
bestonden alleen omdat de echte weg niet aankwam.

Changed files:
src/services/ActivityPubService.js

  • isLocalActor is nu "de eigenaar van deze inbox", niet "iemand op deze host"
  • localActor(): het actordocument van een site die wij hosten, uit onze eigen database in plaats van via een verzoek aan onszelf
  • de lokale sluiproute voor afwezigheid in de C2S-outbox is weg

src/services/guardianship/delivery.js

  • een lokale ontvanger krijgt het bericht via de loopback, de rest per inbox
  • een lokale ontvanger wordt lokaal opgezocht, dus hij valt niet stilletjes uit de ontvangerslijst als het verzoek aan onszelf mislukt

src/routes/guardian.js

  • /api/away schrijft niets meer zelf weg: het bericht doet het werk

src/services/guardianship/handshake.js

  • commentaar bijgewerkt bij de plekken die wel lokaal mogen schrijven

New file:
test/co-location.test.js

  • hetzelfde scenario twee keer, alles-lokaal en alles-extern, met de eis dat de eindtoestand gelijk is
  • een afwezigheid via de loopback en dezelfde brief van de lijn gelezen
  • de loopback weigert nog steeds een afzender die niet klopt
  • een bewaking die faalt zodra er een nieuwe lokale sluiproute in een beslispad verschijnt, met de legitieme uitzonderingen bij naam

remarks: 329 tests groen, en de server start. Twee dingen om te weten voor de
uitrol: sites op een instantie die elkaar volgen zien elkaars berichten nu wel
(dat is wat volgen betekent, maar het is zichtbaar anders), en gated follows
lopen voor een lokale guardian nog steeds via de gedeelde database. Die laatste
staat met naam en toenaam in de bewakingstest, zodat hij niet vergeten wordt.

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

Location:
src/services/guardianship
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • src/services/guardianship/delivery.js

    rc8e03c6 r6d5ce0c  
    4141// call-for-help path).
    4242export async function deliverDirectNote(site, { recipients, text, language, inReplyTo, attachments, helpRequest, wave, awayUntil }) {
    43   const { actorId, fetchActor, deriveHandle, escHtml, linkUrls, linkHashtags,
     43  const { actorId, fetchActor, localActor, deliverTo, deriveHandle, escHtml, linkUrls, linkHashtags,
    4444          getOutboxRow, buildReplyNote, AP_CONTEXT, getOrCreateKeys, deliver, enqueueDelivery } = deps;
    4545  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     
    5050  const resolved = [];
    5151  for (const uri of list) {
    52     const a = await fetchActor(uri).catch(() => null);
     52    // An actor we host is read from our own database, not fetched from our own
     53    // hostname: that request has to leave the machine and come back, and when
     54    // it does not, the recipient is silently dropped from the note. Everything
     55    // that decides anything still runs below, for local and remote alike.
     56    const a = (localActor && localActor(uri)) || await fetchActor(uri).catch(() => null);
    5357    if (!a || !(a.inbox || (a.endpoints && a.endpoints.sharedInbox))) continue;
    54     resolved.push({ uri, inbox: (a.endpoints && a.endpoints.sharedInbox) || a.inbox, handle: deriveHandle(uri), url: a.url || uri });
     58    resolved.push({ uri, inbox: (a.endpoints && a.endpoints.sharedInbox) || a.inbox, local: !!a.local, handle: deriveHandle(uri), url: a.url || uri });
    5559  }
    5660  if (!resolved.length) return null;
     
    8387  const keyId = `${me}#main-key`;
    8488  let delivered = 0;
    85   for (const inbox of [...new Set(resolved.map((r) => r.inbox))]) {
     89  // A recipient on this machine takes the loopback (deliverToActor), which
     90  // hands the Create to the same inbox handler an HTTP POST would reach: the
     91  // note is stored, the mention is stored, and a shaer:away on it is applied,
     92  // all by the code that does it for everyone else. A hairpin POST to our own
     93  // hostname is not that code path, it is a second one that only appears to be.
     94  for (const r of resolved.filter((x) => x.local)) {
     95    const res = await deliverTo(site, r.uri, create).catch(() => null);
     96    if (res && res.delivered) delivered++;
     97  }
     98  // Remote: one POST per inbox, so two guardians on the same server share it.
     99  for (const inbox of [...new Set(resolved.filter((x) => !x.local).map((r) => r.inbox))]) {
    86100    let ok = false;
    87101    try { const st = await deliver(inbox, create, keyId, keys.private_pem); ok = st >= 200 && st < 300; } catch { ok = false; }
  • src/services/guardianship/handshake.js

    rc8e03c6 r6d5ce0c  
    219219    const lp = availability.parseLapse(activity.object);
    220220    if (lp) {
     221      // ONE path (Robins regel, 29-7): the ward's server opens, tallies and
     222      // enforces, wherever it lives. A local ward is reached by the same
     223      // deliverTo, which loops back into the inbox handler; co-location is a
     224      // transport detail and never a shortcut past the decision.
    221225      const id = `${me}/lapses/${Date.now().toString(36)}${Math.floor(Math.random() * 1e4).toString(36)}`;
    222       const wardSlug = deps.localSlug(lp.ward);
    223       if (wardSlug) {
    224         const r = availability.openLapse({ id, wardSlug, wardUri: lp.ward, target: lp.target, openedBy: me, now: Date.now() });
    225         if (r.error) return { status: r.error === 'not_in_available_set' ? 403 : 409, error: r.error };
    226         deps.deliverTo(site, lp.target, { id, type: 'Offer', actor: me, to: [lp.target], object: { type: 'shaer:Lapse', 'shaer:ward': lp.ward, object: lp.target } }).catch(() => { /* best-effort */ });
    227         notify(wardSlug, { kind: 'lapse_opened', lapse: id, target: lp.target, set: r.set });
    228         return { status: 202, id, url: id, 'shaer:set': r.set, 'shaer:threshold': r.threshold };
    229       }
    230226      const offer = { id, type: 'Offer', actor: me, to: [lp.ward], object: { type: 'shaer:Lapse', 'shaer:ward': lp.ward, object: lp.target } };
    231227      const delivered = await fanout(site, [lp.ward], offer);
Note: See TracChangeset for help on using the changeset viewer.