source: Klonkt/src/services/guardianship/relations.js@ fa33214

main
Last change on this file since fa33214 was fa33214, checked in by Bart <bart@…>, 5 weeks ago

FEP-633c §5.3 andersom: een ward vraagt eerst of het iemand mag volgen

Uitgaande follows gingen ongehinderd de deur uit; de guardians kregen achteraf
een bericht (1a2f206). Dat is informeren, niet gaten — de deur staat al open als
het bericht aankomt. Bead shaer-p729, ontwerp in
docs/ward-outbound-follows-design.md.

De regel: per geval goedkeuring, met twee uitzonderingen die geen gunst zijn
maar dezelfde beslissing die al genomen is. Je eigen guardian volgen is geen
vraag. En iemand die de ward al volgt DOOR DE POORT heen is door een guardian
bij naam goedgekeurd; die vraag nog eens stellen leert mensen alleen om de vraag
niet meer te lezen.

Daarvoor moet je weten wie er door de poort kwam, dus ap_followers krijgt
gate_approved, gezet bij acceptGatedFollow. Iedereen die al volgde toen die
kolom erbij kwam wordt eenmalig gegrandfatherd (Barts besluit): exact vanaf nu,
in plaats van met terugwerkende kracht wantrouwig tegen wat er al was.

Eigen tabel, want ap_pending_follows is gesleuteld met de ward als DOEL. Eigen
wachtrij (outgoingFollows), want een guardian moet "iemand wil je ward volgen"
kunnen onderscheiden van "je ward wil iemand volgen" — de AS2-test ving netjes
dat de nieuwe term aangemeld moest worden. En een tegengehouden follow reist als
derde uitkomst naar de app (state: awaiting_guardian), zodat Shaer "wacht op
toestemming" kan tonen in plaats van een tegel die er al volgend uitziet.

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

  • Property mode set to 100644
File size: 4.3 KB
Line 
1/**
2 * Guardianship (FEP-633c) — the COMMITTED ward ↔ guardian relations
3 * (ap_guardianships). Pending offers live in offers.js; a row here means the
4 * handshake committed (§3.1.4). Every row is one relation seen from a LOCAL
5 * site: role 'guardian' = the site guards other_uri; role 'ward' = other_uri
6 * guards the site.
7 */
8import db from '../../config/database.js';
9
10let _s = null;
11function stmts() {
12 if (!_s) {
13 _s = {
14 commit: db.prepare(`INSERT INTO ap_guardianships (slug, role, other_uri, other_handle, status, offer_id, created_at)
15 VALUES (?,?,?,?, 'accepted', ?, CURRENT_TIMESTAMP)
16 ON CONFLICT(slug, role, other_uri) DO UPDATE SET status='accepted', offer_id=excluded.offer_id`),
17 del: db.prepare('DELETE FROM ap_guardianships WHERE slug=? AND role=? AND other_uri=?'),
18 bySlugRole: db.prepare("SELECT * FROM ap_guardianships WHERE slug=? AND role=? AND status='accepted' ORDER BY created_at DESC"),
19 one: db.prepare('SELECT * FROM ap_guardianships WHERE slug=? AND role=? AND other_uri=?'),
20 };
21 }
22 return _s;
23}
24
25// ── Reads ────────────────────────────────────────────────────────────────
26
27/** Accepted guardian URIs of a local ward (feeds shaer:guardians). */
28export function listGuardians(slug) { return stmts().bySlugRole.all(slug, 'ward'); }
29
30/** Accepted wards of a local guardian (the wards queue). */
31export function listWards(slug) { return stmts().bySlugRole.all(slug, 'guardian'); }
32
33/** A site is a guardian once it stands in any accepted guardian relation. */
34export function isGuardian(slug) { return listWards(slug).length > 0; }
35
36export function getRelation(slug, role, otherUri) { return stmts().one.get(slug, role, otherUri); }
37
38// ── Writes (only the handshake commit lands here) ────────────────────────
39
40/** The local ward gains a guardian (commit, §3.1.4). */
41export function commitGuardianForWard(wardSlug, guardianUri, { handle = null, offerId = null } = {}) {
42 stmts().commit.run(wardSlug, 'ward', guardianUri, handle, offerId);
43 return stmts().one.get(wardSlug, 'ward', guardianUri);
44}
45
46/** The local guardian gains a ward (commit, §3.1.4). */
47export function commitWardForGuardian(guardianSlug, wardUri, { handle = null, offerId = null } = {}) {
48 stmts().commit.run(guardianSlug, 'guardian', wardUri, handle, offerId);
49 return stmts().one.get(guardianSlug, 'guardian', wardUri);
50}
51
52/** End a relation locally (Undo, §3.2 — federation of the Undo is Fase 4). */
53export function removeRelation(slug, role, otherUri) {
54 stmts().del.run(slug, role, otherUri);
55 return { ok: true };
56}
57
58// ── Actor document (FEP-633c §2) ─────────────────────────────────────────
59
60/**
61 * Guardianship props for a local actor doc. `id` is the actor URI.
62 * - shaer:guardians: accepted guardians of this ward (omitted when none, §2.1)
63 * - shaer:isGuardian: true once the site guards anyone
64 * - shaer:queues: the owner-only dashboard collections
65 *
66 * §1 mutual exclusion: a ward (has guardians) is never a guardian, so
67 * shaer:isGuardian is suppressed if guardians exist; the offer path already
68 * bars a ward from offering.
69 */
70export function actorProps(id, slug) {
71 const props = {
72 'shaer:queues': {
73 offers: `${id}/queues/offers`,
74 follows: `${id}/queues/follows`,
75 // Both directions of §5.3, kept apart on purpose: a guardian must be able
76 // to tell "someone wants to follow your ward" from "your ward wants to
77 // follow someone". Same mechanism, opposite question, different words in
78 // the interface (shaer-p729).
79 outgoingFollows: `${id}/queues/outgoing-follows`,
80 wards: `${id}/queues/wards`,
81 guardians: `${id}/queues/guardians`,
82 },
83 };
84 const guardians = listGuardians(slug).map((r) => r.other_uri);
85 if (guardians.length) {
86 props['shaer:guardians'] = guardians; // a ward
87 } else if (isGuardian(slug)) {
88 props['shaer:isGuardian'] = true; // a guardian (never both, §1)
89 }
90 return props;
91}
92
93export default {
94 listGuardians, listWards, isGuardian, getRelation,
95 commitGuardianForWard, commitWardForGuardian, removeRelation, actorProps,
96};
Note: See TracBrowser for help on using the repository browser.