Changeset 2bfe26c in Klonkt
- Timestamp:
- 08/08/2026 07:00:17 AM (5 weeks ago)
- 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)
- Files:
-
- 3 added
- 6 edited
-
src/config/database.js (modified) (1 diff)
-
src/routes/guardian.js (modified) (1 diff)
-
src/services/ActivityPubService.js (modified) (6 diffs)
-
src/services/guardianship/delivery.js (modified) (2 diffs)
-
src/services/guardianship/gatereq.js (added)
-
src/services/guardianship/index.js (modified) (1 diff)
-
src/services/guardianship/queues.js (modified) (2 diffs)
-
test/gate-request.test.js (added)
-
test/gated-propose-c2s.test.js (added)
Legend:
- Unmodified
- Added
- Removed
-
src/config/database.js
rf3a2556 r2bfe26c 776 776 )`); 777 777 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)'); 778 793 ensureColumn('ap_mentions', 'help_request', 'INTEGER'); // inbound ward call-for-help (Guardian PWA message centre) 779 794 ensureColumn('ap_outbox', 'wave', 'INTEGER'); // FEP-633c shaer:wave (guardian -> ward nudge) -
src/routes/guardian.js
rf3a2556 r2bfe26c 620 620 const site = siteForUser(req); 621 621 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); 653 627 } 654 628 -
src/services/ActivityPubService.js
rf3a2556 r2bfe26c 2632 2632 Guardianship.availability.declareAway(slug, actorUri, until); 2633 2633 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 } 2634 2652 } 2635 2653 } … … 3237 3255 try { 3238 3256 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 } 3239 3268 case 'Create': { 3240 3269 if (!object || typeof object !== 'object') return { status: 400, error: 'missing_object' }; … … 3248 3277 const isWard = (() => { try { return Guardianship.listGuardians(site.slug).length > 0; } catch { return false; } })(); 3249 3278 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); 3250 3286 const direct = c2sVisibility(object) === 'direct'; 3251 if (!isHelp ) {3287 if (!isHelp && !isGateReq) { 3252 3288 if (direct && !Guardianship.wardGateAllowed(site.gate_messages, isWard)) { 3253 3289 return { status: 403, error: 'gated_messages' }; … … 3307 3343 // absence like it does for a ward anywhere else. One path. 3308 3344 } 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 }); 3310 3347 if (!r || !r.id) return { status: 502, error: 'direct_failed' }; 3311 3348 return { status: 201, id: r.id, url: `${base}/ap/notes/${r.id}` }; … … 3538 3575 // Send a reply FROM this site to a remote actor (in reply to their inbound reply). 3539 3576 // `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 */ 3592 export 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 3540 3616 export async function deliverReply(site, { postId, postSlug, parent, text, html, language, attachments, mentions, visibility }) { 3541 3617 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); … … 6113 6189 getNotifications, listBlocks, isBlockedAny, blockTarget, unblock, 6114 6190 deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker, 6115 getReplyUris, getThread, filterThreadToCircle, gateAttachments, stripEmojiTags, markNotificationsSeen, countUnseenNotifications, hasPlayableAudio,6191 proposeGate, getReplyUris, getThread, filterThreadToCircle, gateAttachments, stripEmojiTags, markNotificationsSeen, countUnseenNotifications, hasPlayableAudio, 6116 6192 linkifyBody, bakePostContent, bakePostContentWithMentions, listFollowers, removeFollower, listConnections, 6117 6193 noteVisibility, belongsInTimeline, playerUrlFor, isRejectedObject, rejectInteraction, interactionReportTarget, -
src/services/guardianship/delivery.js
rf3a2556 r2bfe26c 41 41 // guardian on any instance receives it as a private mention (the ward 42 42 // call-for-help path). 43 export async function deliverDirectNote(site, { recipients, text, html, language, inReplyTo, attachments, helpRequest, wave, awayUntil, helpMark }) {43 export async function deliverDirectNote(site, { recipients, text, html, language, inReplyTo, attachments, helpRequest, wave, awayUntil, helpMark, gateRequest }) { 44 44 const { actorId, fetchActor, localActor, deliverTo, deriveHandle, escHtml, linkUrls, linkHashtags, 45 45 getOutboxRow, buildReplyNote, AP_CONTEXT, getOrCreateKeys, deliver, enqueueDelivery } = deps; … … 116 116 note[helpMark.kind === 'handled' ? 'shaer:helpHandled' : 'shaer:helpPickup'] = helpMark.noteUri; 117 117 } 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); 118 121 const create = { 119 122 '@context': AP_CONTEXT, -
src/services/guardianship/index.js
rf3a2556 r2bfe26c 35 35 // 5.2.1: wie er op een hulpvraag af is en wanneer hij is afgesloten (shaer-lgo) 36 36 export * as help from './help.js'; 37 export * as gatereq from './gatereq.js'; -
src/services/guardianship/queues.js
rf3a2556 r2bfe26c 15 15 import * as follows from './follows.js'; 16 16 import * as gated from './gated.js'; 17 import * as gatereq from './gatereq.js'; 17 18 import * as handshake from './handshake.js'; 18 19 … … 169 170 feature: p.feature, value: !!p.value, status: gated.sentStatus(p, Date.now()), 170 171 })), 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 }, 172 177 }); 173 178 }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)