Changeset 0261730 in Klonkt


Ignore:
Timestamp:
08/08/2026 04:18:37 PM (4 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
abcf51a
Parents:
66d97b8
Message:

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

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 zou zich bovendien niet melden. 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. Een ward die
denkt bewaakt te zijn en het niet is, stil aan beide kanten.

Alleen de accept van de ward kan die toestand maken, dus de kandidaat en de
bestaande guardians lopen er ongemoeid langs.

721 tests groen. 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.

Refs shaer-gk2; stable (1.6.0) heeft dezelfde omissie, zie shaer-pmo.

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

Files:
2 edited

Legend:

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

    r66d97b8 r0261730  
    341341    notify(site.slug, { kind: 'offer_rejected', offer: offerId });
    342342    return { status: 202, id: offerId, url: offerId };
     343  }
     344
     345  // §1 is flat in BOTH directions. The Offer path above bars a ward from
     346  // offering to guard; this is the mirror: an actor that already guards wards
     347  // must not become a ward itself. Only the ward's own accept can create that
     348  // state, so the candidate and the existing guardians pass through untouched.
     349  //
     350  // Without it the inconsistency would also be invisible. actorProps() picks
     351  // one role with an if/else and would publish shaer:guardians while dropping
     352  // shaer:isGuardian, so this account keeps routing its wards' escalations
     353  // locally while every remote §4 check reads it as malformed and drops it —
     354  // a ward believing it is watched over when it is not, silent on both sides.
     355  if (me === offer.ward_uri && relations.listWards(site.slug).length) {
     356    return { status: 403, error: 'a_guardian_cannot_be_guarded' };
    343357  }
    344358
  • test/guardianship.test.js

    r66d97b8 r0261730  
    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.