source: Klonkt/src/services/guardianship/outgoing.js@ 3a0ca0f

main
Last change on this file since 3a0ca0f was 3a0ca0f, checked in by roboburr <roboburr@…>, 4 weeks ago

De volgpoort zat alleen in de app, en volgverzoeken beslissen bij meerderheid

Barts twee meldingen (8-8), en de eerste is een echte vondst.

  1. WAAROM ESMEE'S VOLGVERZOEKEN NOOIT AANKWAMEN

Niet wat Bart vreesde -- het was niet zo dat alleen een intern account ze kon
beantwoorden. Die weg werkt: gateOutgoingFollow levert een Offer af bij een
guardian elders, en dat is precies wat dev als guardian van @mee had moeten
krijgen.

Er kwam alleen nooit iets om af te leveren. De poort stond in case 'Follow' van
de C2S-outbox, dus alleen als je via Shaer volgt. Volgde het kind vanuit Klonkts
eigen webinterface, dan werd er geen verzoek aangemaakt, ging er niets naar de
guardians, en was er dus ook niets te beantwoorden. Op dev staat geen enkele rij
in ap_follow_reviews en nergens een follow-approval-regel in de log.

Exact dezelfde deur-naast-de-poort als bij de antwoordpoort vanmiddag: de gate in
C2S, het webpad eromheen. De controle staat nu in followActor zelf, NA het
oplossen van de handle -- zou hij alleen naar de ruwe invoer kijken, dan is elke
@naam@server een sluiproute. approved is de enige doorlaat, voor
performApprovedFollow; zonder dat stuit een goedgekeurd verzoek opnieuw op de
poort en wacht het voor eeuwig.

Drie webroutes zeiden "Je volgt nu X" terwijl het verzoek bij de guardians lag.
Dat is de leugen die de poort waardeloos maakt: het kind denkt dat het gebeurd
is. Er is nu een derde uitkomst.

DE MUTATIE GAF EERST NUL FOUTEN -- er stond niets op deze poort. Nu vier toetsen,
inclusief het handle-geval en het goedgekeurde pad.

  1. VOLGVERZOEKEN BIJ EENVOUDIGE MEERDERHEID

Barts besluit: 1 van 2 is voldoende. followThreshold = ceil(n/2), voor beide
richtingen, want "mag dit kind met deze persoon te maken hebben" is dezelfde
vraag en twee drempels zou een guardian nooit kunnen uitleggen.

BEWUST SOEPELER DAN DE POORTDREMPEL (2 van 2): een gate opent een deur voor alles
wat daarna komt, een volgverzoek gaat over een persoon en is met ontvolgen terug
te draaien. En het is geen versoepeling maar een AANSCHERPING -- dit stond op
'any', een enkele ja hoeveel guardians er ook waren.

De 'all'-stand is weg: hij werd nergens gezet, dus was het een keuze die niemand
kon maken. De toets die hem bewaakte is herschreven, niet verwijderd.

Race naar de drempel zoals de poorttelling, met dezelfde TODO erbij (shaer-8vt):
wie antwoordt weet niet dat hij de doorslag geeft.

Suite 742/742; beide mutaties rood.

  • Property mode set to 100644
File size: 5.5 KB
Line 
1/**
2 * Guardianship (FEP-633c §5.3, the other direction) — gating a ward's OWN
3 * follows. Bead shaer-p729; the design is in docs/ward-outbound-follows-design.md,
4 * and the spec question it answers is shaer-yeo5.
5 *
6 * The inbound gate in `follows.js` decides who may follow a ward. This one
7 * decides who a ward may follow. Until now that went out unchecked: the
8 * guardians got a note afterwards (1a2f206), which is informing, not gating —
9 * the door is already open by the time the message arrives.
10 *
11 * The rule (Barts besluit): every outgoing follow waits for a guardian, EXCEPT
12 * where the target already follows the ward through the gate. A guardian
13 * already said yes to that person; asking the same question twice only teaches
14 * people to stop reading the question.
15 */
16import db from '../../config/database.js';
17import { followThreshold } from './follows.js';
18
19let _s = null;
20function stmts() {
21 if (!_s) {
22 _s = {
23 ins: db.prepare(`INSERT OR IGNORE INTO ap_pending_outgoing_follows
24 (id, ward_slug, target_uri, target_inbox, target_name, target_handle, target_icon, quorum, created_at)
25 VALUES (?,?,?,?,?,?,?,?, CURRENT_TIMESTAMP)`),
26 get: db.prepare('SELECT * FROM ap_pending_outgoing_follows WHERE id = ?'),
27 byTarget: db.prepare('SELECT * FROM ap_pending_outgoing_follows WHERE ward_slug = ? AND target_uri = ?'),
28 byWard: db.prepare("SELECT * FROM ap_pending_outgoing_follows WHERE ward_slug = ? AND status = 'pending' ORDER BY created_at DESC"),
29 approve: db.prepare('INSERT OR IGNORE INTO ap_outgoing_follow_approvals (follow_id, guardian_uri, decision, created_at) VALUES (?,?,?,CURRENT_TIMESTAMP)'),
30 answers: db.prepare('SELECT guardian_uri, decision FROM ap_outgoing_follow_approvals WHERE follow_id = ?'),
31 setStatus: db.prepare('UPDATE ap_pending_outgoing_follows SET status = ? WHERE id = ?'),
32 del: db.prepare('DELETE FROM ap_pending_outgoing_follows WHERE id = ?'),
33 delByTarget: db.prepare('DELETE FROM ap_pending_outgoing_follows WHERE ward_slug = ? AND target_uri = ?'),
34 gateApproved: db.prepare('SELECT 1 FROM ap_followers WHERE slug = ? AND actor_uri = ? AND gate_approved = 1'),
35 };
36 }
37 return _s;
38}
39
40/**
41 * Does this target already follow the ward, with a guardian's blessing?
42 *
43 * Only a gate-approved follower counts. A follower a free actor picked up
44 * before it was ever a ward was never seen by a guardian, so following them
45 * back is a new question, not a settled one. (Rows that predate the marker are
46 * grandfathered at migration; see config/database.js.)
47 */
48export function isMutual(wardSlug, targetUri) {
49 return !!stmts().gateApproved.get(wardSlug, targetUri);
50}
51
52/** Record an outgoing follow awaiting guardian approval. */
53export function recordPending(wardSlug, f) {
54 stmts().ins.run(
55 f.id, wardSlug, f.target, f.inbox || null,
56 f.name || null, f.handle || null, f.icon || null, f.quorum || 'any',
57 );
58 return stmts().byTarget.get(wardSlug, f.target);
59}
60
61export function getPending(id) { return stmts().get.get(id); }
62export function findFor(wardSlug, targetUri) { return stmts().byTarget.get(wardSlug, targetUri); }
63
64/** Outgoing follows this ward is waiting on — the guardian's queue. */
65export function listForWard(wardSlug) { return stmts().byWard.all(wardSlug); }
66
67/**
68 * A guardian's answer. Same shape and the same quorum arithmetic as the
69 * inbound gate, so the two directions cannot drift apart in how they count:
70 * a single reject denies outright, approvals accumulate toward the quorum.
71 */
72export function decide(id, guardianUri, decision, guardiansOfWard) {
73 const follow = stmts().get.get(id);
74 if (!follow || follow.status !== 'pending') return { outcome: 'gone', follow };
75 stmts().approve.run(id, guardianUri, decision === 'reject' ? 'reject' : 'approve');
76 const rows = stmts().answers.all(id);
77 if (rows.some((r) => r.decision === 'reject')) {
78 stmts().setStatus.run('denied', id);
79 return { outcome: 'rejected', follow };
80 }
81 const approvers = new Set(rows.filter((r) => r.decision === 'approve').map((r) => r.guardian_uri));
82 const guardians = (guardiansOfWard || []).filter(Boolean);
83 // Dezelfde eenvoudige meerderheid als bij een inkomend volgverzoek
84 // (followThreshold): het is dezelfde vraag, alleen omgedraaid. Twee
85 // verschillende drempels voor "mag dit kind met deze persoon te maken hebben"
86 // zou een guardian nooit kunnen uitleggen.
87 const enough = approvers.size >= followThreshold(guardians.length);
88 if (enough) {
89 stmts().setStatus.run('approved', id);
90 return { outcome: 'approved', follow };
91 }
92 return { outcome: 'waiting', follow };
93}
94
95/** The ward changed its mind, or blocked the target: the request is gone. */
96export function withdraw(wardSlug, targetUri) { stmts().delByTarget.run(wardSlug, targetUri); }
97export function remove(id) { stmts().del.run(id); }
98
99/** One request as the queue item the Shaer clients parse, mirroring the
100 * inbound gated-follow item so a dashboard can render both side by side. */
101export function queueItem(o, me) {
102 const rows = stmts().answers.all(o.id);
103 return {
104 id: o.id,
105 type: 'Follow',
106 actor: o.ward_slug,
107 object: o.target_uri,
108 'shaer:direction': 'outgoing',
109 'shaer:target': o.target_uri,
110 'shaer:targetHandle': o.target_handle || undefined,
111 'shaer:quorum': o.quorum || 'any',
112 'shaer:approvals': rows.filter((r) => r.decision === 'approve').length,
113 'shaer:myVote': rows.some((r) => r.guardian_uri === me),
114 published: o.created_at,
115 };
116}
117
118export default {
119 isMutual, recordPending, getPending, findFor, listForWard, decide, withdraw, remove, queueItem,
120};
Note: See TracBrowser for help on using the repository browser.