Changeset 2bfe26c in Klonkt


Ignore:
Timestamp:
08/08/2026 07:00:17 AM (5 weeks ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
0b0cd54
Parents:
f3a2556
git-author:
Robin <roboburr@…> (08/08/2026 07:00:15 AM)
git-committer:
roboburr <roboburr@…> (08/08/2026 07:00:17 AM)
Message:

Voorstellen vanuit de app, en een kind dat zelf kan vragen (shaer-8ru)

Barts opdracht: bouw shaer-8ru zodat de apps ook kunnen voorstellen, en maak
daarnaast het pad voor de ward om zelf een voorstel aan te zwengelen als hij
tegen een gated feature aanloopt.

DEEL 1 -- DE APPS KUNNEN VOORSTELLEN. De outbox kent nu een Offer van een
shaer:GatedSetting, precies de vorm die 5.6 al beschrijft en die de inbox al las.
De afweging zelf is verhuisd naar AP.proposeGate, waar de PWA-route nu ook
doorheen loopt. Twee implementaties naast elkaar zou een tweede weg naar
hetzelfde besluit zijn -- exact de fout die we vanmiddag bij de antwoordpoort
rechtzetten, toen de innamepoort alleen in C2S bleek te zitten.

DEEL 2 -- HET KIND KRIJGT WOORDEN. Tot nu toe liep alles over de guardians: zij
zien de catalogus, zij stellen voor, zij tellen. Het kind liep tegen een dichte
deur en kon niets. Nu stuurt het een shaer:gateRequest, en die landt bij zijn
guardians BIJ DE POORT waar hij over gaat -- niet in een aparte lijst die je
apart moet openen en dus vergeet.

EEN VRAAG IS GEEN STEM, en dat is de hele grens. Het verzoek is geen voorstel en
telt nergens mee; pas als een guardian het oppakt wordt het een voorstel dat
langs de gewone tally gaat. Anders opent een kind zijn eigen poort door hard
genoeg te vragen. Er staat een toets op die kijkt of er echt geen voorstel en
geen open poort uit ontstaat.

NIET DE REDDINGSBOEI. Een hulpvraag is een noodgeval en gaat door elke dichte
deur; dit is een wens. Ze door elkaar laten lopen zou de boei devalueren tot "het
kind wil iets", en dan kijkt er op een dag niemand meer op als hij afgaat.

GEEN VRIJE TEKST, en dat is geen gierigheid maar de reden dat het verzoek langs
de messages-poort MAG. Een kind met berichten dicht kan nog steeds om iets
vragen, zonder dat er een kanaal ontstaat om omheen die poort te praten. Wil het
uitleggen waarom, dan is dat een gesprek, en gesprekken hebben hun eigen poort.

Alleen van een eigen ward: een verzoek van een vreemde is geen vraag maar een
onbekende die iets over jouw instellingen wil zeggen.

Twaalf toetsen. Twee mutaties gecontroleerd (1 en 6 rood). Suite 712/712.

Files:
3 added
6 edited

Legend:

Unmodified
Added
Removed
  • src/config/database.js

    rf3a2556 r2bfe26c  
    776776  )`);
    777777  db.exec('CREATE INDEX IF NOT EXISTS idx_ap_help_state_note ON ap_help_state(note_uri)');
     778  // Een kind dat zelf om een poort vraagt (shaer-8ru). Een VRAAG, geen stem:
     779  // pas als een guardian hem oppakt wordt het een voorstel dat langs de tally
     780  // gaat. handled_at in plaats van verwijderen -- wat een kind gevraagd heeft
     781  // hoort terug te vinden te zijn, ook als het antwoord nee was.
     782  db.exec(`CREATE TABLE IF NOT EXISTS ap_gate_requests (
     783    id INTEGER PRIMARY KEY AUTOINCREMENT,
     784    slug TEXT NOT NULL,                 -- de guardian die hem ontving
     785    ward_uri TEXT NOT NULL,
     786    feature TEXT NOT NULL,
     787    note_uri TEXT,
     788    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
     789    handled_at TEXT,
     790    UNIQUE (slug, ward_uri, feature, handled_at)
     791  )`);
     792  db.exec('CREATE INDEX IF NOT EXISTS idx_ap_gate_requests_slug ON ap_gate_requests(slug, handled_at)');
    778793  ensureColumn('ap_mentions', 'help_request', 'INTEGER'); // inbound ward call-for-help (Guardian PWA message centre)
    779794  ensureColumn('ap_outbox', 'wave', 'INTEGER');    // FEP-633c shaer:wave (guardian -> ward nudge)
  • src/routes/guardian.js

    rf3a2556 r2bfe26c  
    620620  const site = siteForUser(req);
    621621  if (!site) return res.status(404).json({ error: 'no_site' });
    622   const uri = String(req.body?.uri || '').trim();
    623   const allow = req.body?.allow === true;
    624   if (!uri) return res.status(400).json({ error: 'empty_uri' });
    625   // Only a guardian of this ward, and only for a ward we host: a setting on a
    626   // remote ward belongs to that ward's own server (federating it is Fase 4).
    627   const isMyWard = Guardianship.listWards(site.slug).some((w) => w.other_uri === uri);
    628   if (!isMyWard) return res.status(403).json({ error: 'not_your_ward' });
    629   // §5.6: propose it to the WARD'S server, wherever that is. The ward's server
    630   // tallies (a majority of its guardians, §3.5) and enforces. Co-location is
    631   // just the case where that server happens to be this one, so it takes the
    632   // same road: propose, then let the tally decide. Anything else would make a
    633   // guardian on the ward's own instance more powerful than one elsewhere.
    634   const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
    635   const me = AP.actorId(base, site.slug);
    636   const feature = req.body.feature;   // normalised by the route above
    637   const offerId = `${me}/gated/${Date.now().toString(36)}${Math.floor(Math.random() * 1e4).toString(36)}`;
    638   const offer = Guardianship.gated.buildGatedOffer(offerId, me, uri, feature, allow);
    639   // ONE path, whether the ward lives here or on the other side of the world
    640   // (Robins regel, 29-7): propose over the wire and let the ward's server do
    641   // what it does for everyone. deliverToActor loops a local recipient back
    642   // into the same inbox handler, so co-location changes the transport and
    643   // nothing else. The old shortcut recorded the vote here directly, which is
    644   // how the remote path stayed broken for a month without anyone noticing.
    645   // Our own record of what we sent (5.6): the ward's server answers this Offer
    646   // once the decision settles, and that answer needs a row to land in. It is
    647   // also the only way the proposer's screen can say more than a button caption.
    648   Guardianship.gated.recordSent(offerId, site.slug, uri, feature, allow);
    649   AP.deliverToActor(site, uri, offer).catch(() => { /* queued, best-effort */ });
    650   const localSlug = (base && uri.startsWith(`${base}/`)) ? uri.replace(/\/+$/, '').split('/').pop() : null;
    651   const progress = localSlug ? Guardianship.gated.gatedProgress(localSlug, feature) : null;
    652   res.json({ ok: true, allow, state: 'open', ...(progress || { federated: true }) });
     622  // De hele afweging staat in AP.proposeGate, zodat de apps langs dezelfde weg
     623  // kunnen voorstellen (shaer-8ru). Deze route is nog maar de PWA-deur ernaartoe.
     624  const uit = AP.proposeGate(site, req.body?.uri, req.body?.feature, req.body?.allow === true);
     625  const { status, ...rest } = uit;
     626  return res.status(status === 200 ? 200 : status).json(rest);
    653627}
    654628
  • src/services/ActivityPubService.js

    rf3a2556 r2bfe26c  
    26322632            Guardianship.availability.declareAway(slug, actorUri, until);
    26332633            console.log('[AP] guardian declared away (3.6.1):', actorUri, '→', slug, 'until', new Date(until).toISOString());
     2634          }
     2635        }
     2636        // Een kind dat zelf om een poort vraagt (shaer-8ru). Zelfde weg als de
     2637        // afwezigheidsmelding: een gewone directe note met een shaer:-markering,
     2638        // per genoemde ontvanger afgehandeld.
     2639        //
     2640        // ALLEEN VAN EEN EIGEN WARD. Een verzoek van een vreemde is geen vraag
     2641        // maar een onbekende die iets over jouw instellingen wil zeggen -- dat
     2642        // hoort in geen enkele lijst te belanden waar een guardian op afgaat.
     2643        {
     2644          const req = Guardianship.gatereq.parseRequest(o);
     2645          if (req) {
     2646            for (const slug of slugs) {
     2647              const mijn = (() => { try { return Guardianship.listWards(slug).some((w) => w.other_uri === actorUri); } catch { return false; } })();
     2648              if (!mijn) { console.warn('[AP] gate request from someone who is not our ward, ignored:', actorUri, '→', slug); continue; }
     2649              Guardianship.gatereq.record(slug, actorUri, req.feature, o.id);
     2650              console.log('[AP] gate request', req.feature, actorUri, '→', slug);
     2651            }
    26342652          }
    26352653        }
     
    32373255  try {
    32383256    switch (type) {
     3257      // Een gate-voorstel uit de app (5.6, shaer-8ru). De PWA deed dit al over
     3258      // een eigen route; hier komt het binnen als wat het in AP IS -- een Offer
     3259      // van een shaer:GatedSetting. Dezelfde functie erachter, want twee wegen
     3260      // naar hetzelfde besluit is precies wat we vandaag hebben rechtgezet.
     3261      case 'Offer': {
     3262        const gs = Guardianship.gated.parseGatedSetting(object);
     3263        if (!gs) return { status: 400, error: 'unsupported_offer' };
     3264        const uit = proposeGate(site, gs.ward, gs.feature, gs.value);
     3265        if (uit.status !== 200) return uit;
     3266        return { ...uit, status: 201, id: uit.offerId };
     3267      }
    32393268      case 'Create': {
    32403269        if (!object || typeof object !== 'object') return { status: 400, error: 'missing_object' };
     
    32483277          const isWard = (() => { try { return Guardianship.listGuardians(site.slug).length > 0; } catch { return false; } })();
    32493278          const isHelp = object['shaer:helpRequest'] === true || object.helpRequest === true;
     3279          // Een poortverzoek van het kind zelf (shaer-8ru) gaat langs de
     3280          // messages-poort. Dat lijkt een gat en is het niet: het verzoek draagt
     3281          // ALLEEN de naam van de feature, geen vrije tekst, dus er ontstaat geen
     3282          // kanaal om omheen die poort te praten. Zonder deze uitzondering kan
     3283          // een kind met berichten dicht nergens meer om vragen -- en dan is de
     3284          // hele weg dood op precies het moment dat hij nodig is.
     3285          const isGateReq = !!Guardianship.gatereq.parseRequest(object);
    32503286          const direct = c2sVisibility(object) === 'direct';
    3251           if (!isHelp) {
     3287          if (!isHelp && !isGateReq) {
    32523288            if (direct && !Guardianship.wardGateAllowed(site.gate_messages, isWard)) {
    32533289              return { status: 403, error: 'gated_messages' };
     
    33073343            // absence like it does for a ward anywhere else. One path.
    33083344          }
    3309           const r = await deliverDirectNote(site, { recipients, text: plain, language: object.language || null, inReplyTo: typeof object.inReplyTo === 'string' ? object.inReplyTo : null, attachments: atts, helpRequest: help, awayUntil });
     3345          const gateReq = Guardianship.gatereq.parseRequest(object);
     3346          const r = await deliverDirectNote(site, { recipients, text: plain, language: object.language || null, inReplyTo: typeof object.inReplyTo === 'string' ? object.inReplyTo : null, attachments: atts, helpRequest: help, awayUntil, gateRequest: gateReq && gateReq.feature });
    33103347          if (!r || !r.id) return { status: 502, error: 'direct_failed' };
    33113348          return { status: 201, id: r.id, url: `${base}/ap/notes/${r.id}` };
     
    35383575// Send a reply FROM this site to a remote actor (in reply to their inbound reply).
    35393576// `parent` = an ap_interactions row (actor_uri, actor_url, actor_handle, object_uri).
     3577/**
     3578 * Een gate-voorstel de deur uit (FEP-633c 5.6, shaer-8ru).
     3579 *
     3580 * STOND IN routes/guardian.js en kon daar alleen door de PWA aangeroepen worden.
     3581 * De apps moeten hetzelfde kunnen, en een tweede implementatie ernaast zou een
     3582 * tweede weg naar hetzelfde besluit zijn -- precies de fout die we vandaag bij
     3583 * de antwoordpoort hebben rechtgezet, toen de innamepoort alleen in C2S bleek te
     3584 * zitten en het webpad eromheen liep. Een pad dus.
     3585 *
     3586 * EEN WEG, waar de ward ook woont (Robins regel, 29-7): voorstellen over de
     3587 * lijn en de server van de ward laat tellen. Co-locatie verandert alleen het
     3588 * transport -- deliverToActor lust een lokale ontvanger terug door dezelfde
     3589 * inbox. De oude kortsluiting boekte de stem hier meteen, en zo bleef het
     3590 * remote-pad een maand stuk zonder dat iemand het merkte.
     3591 */
     3592export function proposeGate(site, wardUri, feature, allow) {
     3593  const uri = String(wardUri || '').trim();
     3594  if (!uri) return { status: 400, error: 'empty_uri' };
     3595  if (!Guardianship.gated.featureColumn(feature)) return { status: 400, error: 'unknown_feature' };
     3596  // Alleen een guardian van dit kind. Zonder deze regel zou iedereen met een
     3597  // token een instelling van een vreemd kind kunnen aanvragen.
     3598  if (!Guardianship.listWards(site.slug).some((w) => w.other_uri === uri)) {
     3599    return { status: 403, error: 'not_your_ward' };
     3600  }
     3601  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     3602  const me = actorId(base, site.slug);
     3603  const offerId = `${me}/gated/${Date.now().toString(36)}${Math.floor(Math.random() * 1e4).toString(36)}`;
     3604  const offer = Guardianship.gated.buildGatedOffer(offerId, me, uri, feature, allow);
     3605  // Ons eigen spoor van wat we stuurden: de server van de ward antwoordt op deze
     3606  // Offer zodra het besluit valt, en dat antwoord heeft een rij nodig om in te
     3607  // landen. Het is ook het enige waardoor het scherm van de voorsteller meer kan
     3608  // zeggen dan een knoptekst.
     3609  Guardianship.gated.recordSent(offerId, site.slug, uri, feature, allow);
     3610  deliverToActor(site, uri, offer).catch(() => { /* queued, best-effort */ });
     3611  const localSlug = (base && uri.startsWith(`${base}/`)) ? uri.replace(/\/+$/, '').split('/').pop() : null;
     3612  const progress = localSlug ? Guardianship.gated.gatedProgress(localSlug, feature) : null;
     3613  return { status: 200, ok: true, allow, state: 'open', offerId, ...(progress || { federated: true }) };
     3614}
     3615
    35403616export async function deliverReply(site, { postId, postSlug, parent, text, html, language, attachments, mentions, visibility }) {
    35413617  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     
    61136189  getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
    61146190  deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker,
    6115   getReplyUris, getThread, filterThreadToCircle, gateAttachments, stripEmojiTags, markNotificationsSeen, countUnseenNotifications, hasPlayableAudio,
     6191  proposeGate, getReplyUris, getThread, filterThreadToCircle, gateAttachments, stripEmojiTags, markNotificationsSeen, countUnseenNotifications, hasPlayableAudio,
    61166192  linkifyBody, bakePostContent, bakePostContentWithMentions, listFollowers, removeFollower, listConnections,
    61176193  noteVisibility, belongsInTimeline, playerUrlFor, isRejectedObject, rejectInteraction, interactionReportTarget,
  • src/services/guardianship/delivery.js

    rf3a2556 r2bfe26c  
    4141// guardian on any instance receives it as a private mention (the ward
    4242// call-for-help path).
    43 export async function deliverDirectNote(site, { recipients, text, html, language, inReplyTo, attachments, helpRequest, wave, awayUntil, helpMark }) {
     43export async function deliverDirectNote(site, { recipients, text, html, language, inReplyTo, attachments, helpRequest, wave, awayUntil, helpMark, gateRequest }) {
    4444  const { actorId, fetchActor, localActor, deliverTo, deriveHandle, escHtml, linkUrls, linkHashtags,
    4545          getOutboxRow, buildReplyNote, AP_CONTEXT, getOrCreateKeys, deliver, enqueueDelivery } = deps;
     
    116116    note[helpMark.kind === 'handled' ? 'shaer:helpHandled' : 'shaer:helpPickup'] = helpMark.noteUri;
    117117  }
     118  // Een kind dat zelf om een poort vraagt (shaer-8ru). Alleen de naam van de
     119  // feature reist mee -- geen vrije tekst, zie gatereq.js.
     120  if (gateRequest) note['shaer:gateRequest'] = String(gateRequest);
    118121  const create = {
    119122    '@context': AP_CONTEXT,
  • src/services/guardianship/index.js

    rf3a2556 r2bfe26c  
    3535// 5.2.1: wie er op een hulpvraag af is en wanneer hij is afgesloten (shaer-lgo)
    3636export * as help from './help.js';
     37export * as gatereq from './gatereq.js';
  • src/services/guardianship/queues.js

    rf3a2556 r2bfe26c  
    1515import * as follows from './follows.js';
    1616import * as gated from './gated.js';
     17import * as gatereq from './gatereq.js';
    1718import * as handshake from './handshake.js';
    1819
     
    169170      feature: p.feature, value: !!p.value, status: gated.sentStatus(p, Date.now()),
    170171    })),
    171     waiting: { 'shaer:follows': wachtend || undefined },
     172    // Wat er op deze poort wacht: volgverzoeken bij de volgpoort, en de vraag
     173    // van het kind zelf bij de poort waar hij over gaat (shaer-8ru). Zo staat
     174    // hij waar je hem nodig hebt, en niet in een aparte lijst die je apart moet
     175    // openen -- en die je dus vergeet.
     176    waiting: { ...gatereq.waitingFor(mySlug, wardUri), 'shaer:follows': wachtend || undefined },
    172177  });
    173178}
Note: See TracChangeset for help on using the changeset viewer.