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

main
Last change on this file since d9ad6c5 was 780a7c6, checked in by Robin Genis <roboburr@…>, 7 weeks ago

Guardianship Fase 0+1: de echte multi-party handshake (FEP-633c §3)

De eerste versie committeerde na één accept. Nu de spec: geen enkele partij
maakt een voogdij alleen, en een nieuwe guardian erbij kan niet zonder
toestemming van de bestaande. Daemon als blauwdruk, zodat Klonkt en de
test-daemon exact hetzelfde gedragen en de Shaer-clients één contract lezen.

Fase 0 (datamodel): ap_guardian_offers (per lokale partij een kopie van de
handshake, PK slug+offer_id) + ap_guardian_offer_accepts (de accept-tally).
ap_guardianships houdt alleen nog de GECOMMITTE relaties.

Fase 1 (state-machine): offers.js is een getrouwe port van de daemon-Handshake
(accepts over ward+candidate+existing; ready = ward && candidate && (geen
existing OF >=1 existing); een Reject voidt). handshake.js orchestreert het
gedistribueerd: de kandidaat adresseert de Offer aan ward + alle bestaande
guardians (§3.1.1); elke Accept wordt aan alle andere partijen gebroadcast, dus
elke instance-kopie convergeert; zodra een kopie compleet is committeert die
lokaal (ward schrijft shaer:guardians, guardian schrijft z'n ward), met de
kandidaat-inbox als handle (§6). Volgorde-onafhankelijk.

Ook: §1 wederzijdse uitsluiting (een ward is nooit ook guardian in het
actor-doc), de queues vullen nu de echte accept-tally (needsMyAccept/
readyToCommit/acceptedBy/existingGuardians), en de PWA + Berichten beantwoorden
via de C2S Accept/Reject-pijplijn per offer-id. De co-guardian ziet een
mede-voogdij-aanvraag met accepteer/weiger in de PWA.

Changed files:
src/config/database.js

  • tabellen ap_guardian_offers + ap_guardian_offer_accepts

src/services/guardianship/offers.js (NEW)

  • de handshake-state-machine (daemon-port), per-instance in SQLite

src/services/guardianship/relations.js

  • alleen commit-writers + actor-props (§1 uitsluiting)

src/services/guardianship/handshake.js

  • gedistribueerde multi-party C2S/S2S orchestratie

src/services/guardianship/queues.js

  • offers-queue uit de state-machine

src/services/guardianship/index.js

  • exports bijgewerkt

src/services/ActivityPubService.js

  • wire localSlug + fetchActor; inbound-routing naar alle lokale partijen

src/routes/guardian.js

  • dashboard toont offers met tally; POST /guardian/offer (accept/reject)

src/routes/posts.js

  • Berichten toont ward-offers uit de state-machine; accept via offer-id

src/views/pages/messages.ejs, src/assets/js/guardian.js, src/assets/css/guardian.css

  • offer-kaarten per state (mijn aanvraag / mede-voogdij / wachten)

src/services/i18n.js

  • accept/reject/complete/coguard + co-guardian push (nl/en/de)

test/guardianship.test.js

  • multi-party: eerste guardian, co-approval bestaande guardian, reject voidt, ward-mag-niet-guarden, vaste initiator

remarks: Fase 2 (follow-gating), 3 (hasGuardians + Not-a-Teapot), 4 (Undo/
emancipatie) volgen. 164 tests groen.

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

  • Property mode set to 100644
File size: 4.0 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 wards: `${id}/queues/wards`,
76 },
77 };
78 const guardians = listGuardians(slug).map((r) => r.other_uri);
79 if (guardians.length) {
80 props['shaer:guardians'] = guardians; // a ward
81 } else if (isGuardian(slug)) {
82 props['shaer:isGuardian'] = true; // a guardian (never both, §1)
83 }
84 return props;
85}
86
87export default {
88 listGuardians, listWards, isGuardian, getRelation,
89 commitGuardianForWard, commitWardForGuardian, removeRelation, actorProps,
90};
Note: See TracBrowser for help on using the repository browser.