Changeset 65abc85 in Klonkt for src/routes/guardian.js


Ignore:
Timestamp:
07/28/2026 10:40:50 AM (6 weeks ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
2708282
Parents:
2a76184
Message:

Gated settings federeren: guardians beslissen samen, ook van een andere server

Ik had de knop alleen voor het co-located geval gebouwd, en dat is precies het
uitzonderingsgeval. In de echte opstelling staat de ward op de ene server en zijn
drie guardians op twee andere: er was dus nergens een knop. Dat botst met onze
eigen regel dat co-locatie een optimalisatie is en nooit de aanname.

Nu volgens FEP-633c 5.6 (deze week aan de spec toegevoegd): een guardian stelt
een wijziging voor met een Offer van een shaer:GatedSetting aan de server van de
WARD; de andere guardians antwoorden met Accept/Reject; de server van de ward
telt en handhaaft, want die serveert de feed. Co-locatie neemt dezelfde weg: ook
daar wordt voorgesteld en geteld, anders zou een guardian naast de deur meer te
zeggen hebben dan een op afstand.

De tally is een 3.5-beslissing en staat als pure functie apart: gesnapshotte set,
strikte meerderheid, venster van een dag. Omkeerbaar, dus race naar de drempel in
BEIDE richtingen (settelt ook zodra een meerderheid onhaalbaar is) en faalt dicht
op de deadline. Een Reject is een stem voor de andere waarde, geen schouderophalen.

New file:
src/services/guardianship/gated.js

  • tallyGatedSetting (puur), thresholdFor, featureColumn (onbekende features geweigerd i.p.v. geraden), recordGatedVote, en de Offer-vorm

test/gated-settings.test.js

  • 9 tests: drempel, vroeg settelen in beide richtingen, dicht op de deadline, vreemden tellen niet mee, geen guardians = niets toegekend, van gedachten veranderen vervangt je stem, en een onbekende feature raakt geen kolom

Changed files:
src/config/database.js

  • ap_gated_offers + ap_gated_votes

src/services/guardianship/handshake.js

  • inbox: Offer(shaer:GatedSetting) en Accept/Reject erop, met de stem van de voorsteller meegeteld (one-step-clausule)

src/services/guardianship/index.js

  • gated geexporteerd

src/routes/guardian.js

  • de knop stuurt een voorstel, lokaal en remote langs dezelfde weg

src/assets/js/guardian.js

  • knop bij ELKE ward, ook remote; toont 'wacht op de andere guardians'

src/services/i18n.js

  • embeds_propose / embeds_waiting in nl, en, de

remarks: 228 tests groen (was 219).

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/guardian.js

    r2a76184 r65abc85  
    3939  const keys = ['sent', 'sent_retry', 'sending', 'not_found', 'failed', 'network',
    4040    'pending', 'active', 'retract', 'release', 'release_confirm', 'open', 'push_unavailable',
    41     'embeds_on', 'embeds_off',
     41    'embeds_on', 'embeds_off', 'embeds_propose', 'embeds_waiting',
    4242    'accept', 'reject', 'complete', 'awaiting_others', 'coguard'];
    4343  const s = Object.fromEntries(keys.map((k) => [k, i18nT(L, `guardian.${k}`)]));
     
    300300  const isMyWard = Guardianship.listWards(site.slug).some((w) => w.other_uri === uri);
    301301  if (!isMyWard) return res.status(403).json({ error: 'not_your_ward' });
    302   const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
    303   const slug = (base && uri.startsWith(`${base}/`)) ? uri.trim().replace(/\/+$/, '').split('/').pop() : null;
    304   const ward = slug ? db.prepare('SELECT id, slug FROM sites WHERE slug = ?').get(slug) : null;
    305   if (!ward) return res.status(400).json({ error: 'remote_ward_not_supported' });
    306   db.prepare('UPDATE sites SET external_embeds = ? WHERE id = ?').run(allow ? 1 : 0, ward.id);
    307   res.json({ ok: true, allow });
     302  // §5.6: propose it to the WARD'S server, wherever that is. The ward's server
     303  // tallies (a majority of its guardians, §3.5) and enforces. Co-location is
     304  // just the case where that server happens to be this one, so it takes the
     305  // same road: propose, then let the tally decide. Anything else would make a
     306  // guardian on the ward's own instance more powerful than one elsewhere.
     307  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     308  const me = AP.actorId(base, site.slug);
     309  const feature = 'shaer:externalEmbeds';
     310  const offerId = `${me}/gated/${Date.now().toString(36)}${Math.floor(Math.random() * 1e4).toString(36)}`;
     311  const offer = Guardianship.gated.buildGatedOffer(offerId, me, uri, feature, allow);
     312  const localSlug = (base && uri.startsWith(`${base}/`)) ? uri.replace(/\/+$/, '').split('/').pop() : null;
     313  const localWard = localSlug ? db.prepare('SELECT slug FROM sites WHERE slug = ?').get(localSlug) : null;
     314  if (localWard) {
     315    Guardianship.gated.rememberGatedOffer(offerId, localWard.slug, feature, allow);
     316    const r = Guardianship.gated.recordGatedVote(localWard.slug, feature, me, allow);
     317    return res.json({ ok: true, allow, state: r.state, need: r.need, of: r.of });
     318  }
     319  AP.deliverTo(site, uri, offer).catch(() => { /* queued, best-effort */ });
     320  res.json({ ok: true, allow, state: 'open', federated: true });
    308321});
    309322
Note: See TracChangeset for help on using the changeset viewer.