Changeset b60cf25 in Klonkt


Ignore:
Timestamp:
08/11/2026 04:24:25 PM (3 months ago)
Author:
Robin <roboburr@…>
Parents:
859b1707
Message:

guardianship: §1 is flat in beide richtingen, ook bij accept

Backport van 0261730 op main. Het Offer-pad belet een ward om te offeren
(a_ward_cannot_guard), maar het Accept-pad kende geen spiegelbeeld: een actor
die al wards heeft kon een guardianship-Offer accepteren en werd zo ward én
guardian tegelijk. §1 eist flatheid in beide richtingen.

De fout meldt zich niet. actorProps() kiest met een if/else één rol en
publiceert dan shaer:guardians terwijl shaer:isGuardian wegvalt: lokaal
blijven escalaties van de eigen wards binnenkomen, terwijl elke remote
§4-check de actor als malformed leest en laat vallen.

357 tests groen in een echte 1.6-omgeving (eigen npm ci, nodemailer 6.10.1
zoals stable vraagt, niet main's 9.x). Controleproef gedraaid: zonder de
controle wordt de nieuwe test rood en valt ook 'a guardian leaving sends an
Undo (§3.2)' om, doordat de actor dan werkelijk ward wordt.

Closes shaer-pmo.

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

Files:
2 edited

Legend:

Unmodified
Added
Removed
  • src/services/guardianship/handshake.js

    r859b1707 rb60cf25  
    290290    notify(site.slug, { kind: 'offer_rejected', offer: offerId });
    291291    return { status: 202, id: offerId, url: offerId };
     292  }
     293
     294  // §1 is flat in BOTH directions. The Offer path above bars a ward from
     295  // offering to guard; this is the mirror: an actor that already guards wards
     296  // must not become a ward itself. Only the ward's own accept can create that
     297  // state, so the candidate and the existing guardians pass through untouched.
     298  //
     299  // Without it the inconsistency would also be invisible. actorProps() picks
     300  // one role with an if/else and would publish shaer:guardians while dropping
     301  // shaer:isGuardian, so this account keeps routing its wards' escalations
     302  // locally while every remote §4 check reads it as malformed and drops it —
     303  // a ward believing it is watched over when it is not, silent on both sides.
     304  if (me === offer.ward_uri && relations.listWards(site.slug).length) {
     305    return { status: 403, error: 'a_guardian_cannot_be_guarded' };
    292306  }
    293307
  • test/guardianship.test.js

    r859b1707 rb60cf25  
    119119  assert.equal(r.status, 403);
    120120  assert.equal(r.error, 'a_ward_cannot_guard');
     121});
     122
     123test('a guardian cannot be guarded either: §1 is flat in both directions', async () => {
     124  // parent guards kid by now, and gran (a guardian, so free to offer) offers
     125  // to guard parent. The offer itself is fine; only parent's accept would
     126  // make one actor ward and guardian at once.
     127  assert.ok(G.listWards('parent').length, 'parent already guards someone');
     128  const off = await G.handleGuardianshipOutbox(gran, {
     129    type: 'Offer', object: { type: 'Relationship', subject: ME, relationship: 'shaer:Guardian', object: GRAN },
     130  });
     131  assert.equal(off.status, 202, 'a guardian may still offer to guard');
     132  const id = offerIdFrom(off);
     133
     134  const r = await G.handleGuardianshipOutbox(parent, { type: 'Accept', object: id });
     135  assert.equal(r.status, 403);
     136  assert.equal(r.error, 'a_guardian_cannot_be_guarded');
     137
     138  // Nothing recorded, and the actor document still reads as a pure guardian.
     139  // Without the check actorProps() would have flipped it to a ward silently.
     140  assert.deepEqual(G.listGuardians('parent'), []);
     141  const doc = AP.buildActor('https://test.example', parent);
     142  assert.equal(doc['shaer:isGuardian'], true);
     143  assert.equal(doc['shaer:guardians'], undefined);
     144
     145  // The candidate and the existing guardians are untouched by the check, so
     146  // the offer is refusable in the ordinary way rather than stuck.
     147  await G.handleGuardianshipOutbox(parent, { type: 'Reject', object: id });
    121148});
    122149
Note: See TracChangeset for help on using the changeset viewer.