| 1 | /**
|
|---|
| 2 | * Guardianship (FEP-633c) — the ward ↔ guardian relations (ap_guardianships).
|
|---|
| 3 | *
|
|---|
| 4 | * Every row is one relation seen from a LOCAL site: role 'guardian' means the
|
|---|
| 5 | * site guards `other_uri` (a ward, possibly remote); role 'ward' means
|
|---|
| 6 | * `other_uri` guards the site. A local ward with a local guardian yields two
|
|---|
| 7 | * rows, one per perspective — intentional, each side reads its own.
|
|---|
| 8 | *
|
|---|
| 9 | * The handshake (spec §3): the guardian-candidate — and only the candidate —
|
|---|
| 10 | * Offers a Relationship {subject: ward, relationship: shaer:Guardian,
|
|---|
| 11 | * object: candidate}; the ward Accepts (or Rejects). Status walks
|
|---|
| 12 | * 'offered' → 'accepted'; a Reject deletes the row.
|
|---|
| 13 | */
|
|---|
| 14 | import db from '../../config/database.js';
|
|---|
| 15 |
|
|---|
| 16 | let _s = null;
|
|---|
| 17 | function stmts() {
|
|---|
| 18 | if (!_s) {
|
|---|
| 19 | _s = {
|
|---|
| 20 | ins: db.prepare(`INSERT OR IGNORE INTO ap_guardianships (slug, role, other_uri, other_handle, status, offer_id, created_at)
|
|---|
| 21 | VALUES (?,?,?,?,?,?,CURRENT_TIMESTAMP)`),
|
|---|
| 22 | accept: db.prepare(`UPDATE ap_guardianships SET status='accepted' WHERE slug=? AND role=? AND other_uri=?`),
|
|---|
| 23 | del: db.prepare('DELETE FROM ap_guardianships WHERE slug=? AND role=? AND other_uri=?'),
|
|---|
| 24 | bySlugRole: db.prepare('SELECT * FROM ap_guardianships WHERE slug=? AND role=? ORDER BY created_at DESC'),
|
|---|
| 25 | one: db.prepare('SELECT * FROM ap_guardianships WHERE slug=? AND role=? AND other_uri=?'),
|
|---|
| 26 | byOffer: db.prepare('SELECT * FROM ap_guardianships WHERE offer_id=?'),
|
|---|
| 27 | };
|
|---|
| 28 | }
|
|---|
| 29 | return _s;
|
|---|
| 30 | }
|
|---|
| 31 |
|
|---|
| 32 | // ── Reads ────────────────────────────────────────────────────────────────
|
|---|
| 33 |
|
|---|
| 34 | /** Accepted guardian URIs of a local ward (feeds shaer:guardians). */
|
|---|
| 35 | export function listGuardians(slug) {
|
|---|
| 36 | return stmts().bySlugRole.all(slug, 'ward').filter((r) => r.status === 'accepted');
|
|---|
| 37 | }
|
|---|
| 38 |
|
|---|
| 39 | /** All ward relations of a local guardian (accepted + pending offers). */
|
|---|
| 40 | export function listWards(slug) {
|
|---|
| 41 | return stmts().bySlugRole.all(slug, 'guardian');
|
|---|
| 42 | }
|
|---|
| 43 |
|
|---|
| 44 | /** Pending offers where the local site is a party (either side). */
|
|---|
| 45 | export function listOffers(slug) {
|
|---|
| 46 | return [...stmts().bySlugRole.all(slug, 'guardian'), ...stmts().bySlugRole.all(slug, 'ward')]
|
|---|
| 47 | .filter((r) => r.status === 'offered');
|
|---|
| 48 | }
|
|---|
| 49 |
|
|---|
| 50 | /** A site is a guardian once it stands in any guardian-side relation. */
|
|---|
| 51 | export function isGuardian(slug) {
|
|---|
| 52 | return stmts().bySlugRole.all(slug, 'guardian').length > 0;
|
|---|
| 53 | }
|
|---|
| 54 |
|
|---|
| 55 | export function getRelation(slug, role, otherUri) { return stmts().one.get(slug, role, otherUri); }
|
|---|
| 56 | export function findByOfferId(offerId) { return offerId ? stmts().byOffer.all(offerId) : []; }
|
|---|
| 57 |
|
|---|
| 58 | // ── Writes (the handshake walks through these) ───────────────────────────
|
|---|
| 59 |
|
|---|
| 60 | /** Record an outgoing/incoming Offer on the local side with `role`. */
|
|---|
| 61 | export function recordOffer(slug, role, otherUri, { handle = null, offerId = null } = {}) {
|
|---|
| 62 | stmts().ins.run(slug, role, otherUri, handle, 'offered', offerId);
|
|---|
| 63 | return stmts().one.get(slug, role, otherUri);
|
|---|
| 64 | }
|
|---|
| 65 |
|
|---|
| 66 | /** The ward said yes (or our own offer was accepted): relation becomes real. */
|
|---|
| 67 | export function acceptRelation(slug, role, otherUri) {
|
|---|
| 68 | stmts().accept.run(slug, role, otherUri);
|
|---|
| 69 | return stmts().one.get(slug, role, otherUri);
|
|---|
| 70 | }
|
|---|
| 71 |
|
|---|
| 72 | /** Reject / retract / end a relation: the row disappears. */
|
|---|
| 73 | export function removeRelation(slug, role, otherUri) {
|
|---|
| 74 | stmts().del.run(slug, role, otherUri);
|
|---|
| 75 | return { ok: true };
|
|---|
| 76 | }
|
|---|
| 77 |
|
|---|
| 78 | // ── Actor document (FEP-633c §2) ─────────────────────────────────────────
|
|---|
| 79 |
|
|---|
| 80 | /**
|
|---|
| 81 | * The guardianship properties for a local actor doc. `id` is the actor URI.
|
|---|
| 82 | * - shaer:guardians: accepted guardians of this ward (omitted when none)
|
|---|
| 83 | * - shaer:isGuardian: true once the site guards anyone
|
|---|
| 84 | * - shaer:queues: the owner-only dashboard collections (always advertised,
|
|---|
| 85 | * like `blocked`: clients discover, the routes enforce auth)
|
|---|
| 86 | */
|
|---|
| 87 | export function actorProps(id, slug) {
|
|---|
| 88 | const props = {
|
|---|
| 89 | 'shaer:queues': {
|
|---|
| 90 | offers: `${id}/queues/offers`,
|
|---|
| 91 | follows: `${id}/queues/follows`,
|
|---|
| 92 | wards: `${id}/queues/wards`,
|
|---|
| 93 | },
|
|---|
| 94 | };
|
|---|
| 95 | const guardians = listGuardians(slug).map((r) => r.other_uri);
|
|---|
| 96 | if (guardians.length) props['shaer:guardians'] = guardians;
|
|---|
| 97 | if (isGuardian(slug)) props['shaer:isGuardian'] = true;
|
|---|
| 98 | return props;
|
|---|
| 99 | }
|
|---|
| 100 |
|
|---|
| 101 | export default {
|
|---|
| 102 | listGuardians, listWards, listOffers, isGuardian, getRelation, findByOfferId,
|
|---|
| 103 | recordOffer, acceptRelation, removeRelation, actorProps,
|
|---|
| 104 | };
|
|---|