Changeset c8e03c6 in Klonkt


Ignore:
Timestamp:
07/29/2026 10:29:55 AM (6 weeks ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
6d5ce0c
Parents:
329873e
Message:

Het doorgestuurde voorstel werd geweigerd: verkeerde afzender

Robin meldde dat stap 2 niet gebeurt, en beta's log zegt waarom:

[AP] guardianship Offer got 401 from https://boiert.eu/ap/users/opie/inbox
[AP] guardianship Offer got 401 from https://boiert.eu/ap/users/boiert/inbox

Het doorsturen werkt dus wel, maar de ontvangers weigeren het, en terecht. Ik
stuurde door met de voorsteller in actor terwijl de sleutel van het kind
ondertekent. Het lichaam beweerde de ene afzender, de handtekening bewees de
andere: signer mismatch, 401, precies wat die controle hoort te doen.

5.3 doet het al goed en had het voorbeeld moeten zijn: een gated follow wordt
doorgestuurd ALS HET KIND. Nu deze ook, met de voorsteller in shaer:proposer
zodat het scherm van de guardian nog steeds de juiste naam toont.

En de test die dit had moeten vangen: die controleerde DAT er doorgestuurd
werd, niet namens wie. Hij stond groen terwijl er niets aankwam. Nu controleert
hij de afzender en de proposer, en hij faalt op de oude code.

Changed files:
src/services/guardianship/handshake.js

  • doorsturen met actor = het kind, plus shaer:proposer
  • de ontvanger leest de voorsteller uit shaer:proposer

src/routes/guardian.js

  • hetzelfde op het lokale pad: ondertekenen en beweren moeten kloppen

test/gated-settings.test.js

  • de afzender van het doorgestuurde voorstel is het kind; de proposer reist apart mee en komt op het scherm terecht

remarks: 322 tests groen. De 401's die in de bezorgwachtrij staan dragen nog de
oude vorm en blijven falen tot ze opgeven; een nieuw voorstel is de weg vooruit.

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

Files:
3 edited

Legend:

Unmodified
Added
Removed
  • src/routes/guardian.js

    r329873e rc8e03c6  
    516516    // the proposal exists and a threshold of two can never be met.
    517517    if (r.state === 'open') {
     518      const wardActor = AP.actorId(base, localWard.slug);
    518519      for (const g of Guardianship.listGuardians(localWard.slug).map((x) => x.other_uri)) {
    519520        if (g === me) continue;
    520         AP.deliverToActor(site, g, { ...offer, to: [g] }).catch(() => { /* queued */ });
     521        // Signed by the ward, so the body must say the ward: anything else is
     522        // a signer mismatch and the receiver answers 401 (as it should).
     523        AP.deliverToActor(
     524          db.prepare('SELECT * FROM sites WHERE slug = ?').get(localWard.slug),
     525          g,
     526          { ...offer, actor: wardActor, to: [g], 'shaer:proposer': me },
     527        ).catch(() => { /* queued */ });
    521528      }
    522529    }
  • src/services/guardianship/handshake.js

    r329873e rc8e03c6  
    317317          for (const g of relations.listGuardians(site.slug).map((x) => x.other_uri)) {
    318318            if (g === actor) continue;   // the proposer already answered
     319            // The forward goes out AS THE WARD, because the ward's key signs
     320            // it. Keeping the proposer in `actor` made every receiver answer
     321            // 401 signer mismatch, and rightly so: the body claimed one author
     322            // and the signature proved another. §5.3 forwards a gated follow
     323            // the same way. Who proposed it rides along separately, for the
     324            // guardian's screen.
    319325            deps.deliverTo(site, g, {
    320               id: offerId, type: 'Offer', actor, to: [g], object: activity.object,
     326              id: offerId, type: 'Offer', actor: me, to: [g], object: activity.object,
     327              'shaer:proposer': actor,
    321328            }).catch(() => { /* the delivery queue retries */ });
    322329          }
     
    333340        gated.recordGatedReview(site.slug, {
    334341          id: offerId, wardUri: gs.ward, wardInbox: wardDoc && wardDoc.inbox,
    335           proposer: actor, feature: gs.feature, value: gs.value,
     342          // A forward is signed by the ward, so `actor` is the ward; the
     343          // guardian who opened it travels in shaer:proposer.
     344          proposer: (typeof activity['shaer:proposer'] === 'string' ? activity['shaer:proposer'] : actor),
     345          feature: gs.feature, value: gs.value,
    336346        });
    337347        notify(site.slug, { kind: 'gated_review', feature: gs.feature, value: gs.value, ward: gs.ward });
  • test/gated-settings.test.js

    r329873e rc8e03c6  
    116116
    117117  // The forward: B and C are told, A is not asked twice.
    118   const told = sent.filter((x) => x.act.object && x.act.object['shaer:feature']).map((x) => x.to).sort();
    119   assert.deepEqual(told, [B, C].sort(), 'both other guardians must receive the proposal');
     118  const fwd = sent.filter((x) => x.act.object && x.act.object['shaer:feature']);
     119  assert.deepEqual(fwd.map((x) => x.to).sort(), [B, C].sort(), 'both other guardians must receive the proposal');
     120  // And it must go out AS THE WARD, because the ward's key signs it. Sending
     121  // it with the proposer still in `actor` is a signer mismatch: every receiver
     122  // answers 401 and the proposal silently never arrives. Live proof, from
     123  // beta's log: "guardianship Offer got 401 from boiert.eu/.../inbox". The
     124  // first version of this test checked THAT a forward happened and not on
     125  // whose behalf, so it passed while nothing worked.
     126  for (const x of fwd) {
     127    assert.equal(x.act.actor, WARD, 'the forward is signed by the ward, so it must say the ward');
     128    assert.equal(x.act['shaer:proposer'], A, 'and it carries who actually proposed it');
     129  }
    120130
    121131  // B receives its copy on its own server and can answer it.
     
    123133  database.prepare("INSERT OR IGNORE INTO ap_guardianships (slug, role, other_uri, status, offer_id) VALUES ('gb','guardian',?, 'accepted','o')").run(WARD);
    124134  const gbSite = database.prepare('SELECT * FROM sites WHERE slug = ?').get('gb');
    125   assert.equal(await G.handleGuardianshipInbox(gbSite, { ...offer, actor: A, to: ['https://test.example/ap/users/gb'] }), true);
     135  // Exactly the shape the ward sends: actor = the ward, proposer alongside.
     136  assert.equal(await G.handleGuardianshipInbox(gbSite, {
     137    ...offer, actor: WARD, 'shaer:proposer': A, to: ['https://test.example/ap/users/gb'],
     138  }), true);
    126139  const review = G.gated.listGatedReviews('gb')[0];
    127140  assert.ok(review, 'the guardian stores a copy it can answer');
     141  assert.equal(review.proposer, A, 'the screen names who proposed it, not the ward that relayed it');
    128142  assert.equal(review.feature, 'shaer:externalEmbeds');
    129143  assert.equal(review.ward_uri, WARD);
Note: See TracChangeset for help on using the changeset viewer.