Changeset 69bd747 in Klonkt for src/services/guardianship/handshake.js
- Timestamp:
- 08/03/2026 06:26:57 AM (5 weeks ago)
- Branches:
- main
- Children:
- 30d0e2c
- Parents:
- 5327324
- File:
-
- 1 edited
-
src/services/guardianship/handshake.js (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/guardianship/handshake.js
r5327324 r69bd747 168 168 reason: 'not_a_teapot', candidate: offer.candidate_uri, 169 169 }); 170 return { done: null, refused: 'not_a_teapot' };170 return { done: null, refused: 'not_a_teapot', offer }; 171 171 } 172 172 … … 352 352 offers.recordAccept(site.slug, offerId, me); 353 353 await fanout(site, others, { id: `${me}/answers/${Date.now().toString(36)}`, type: 'Accept', actor: me, to: others, object: offerId }); 354 const { done, refused } = await maybeCommit(site.slug, offerId);354 const { done, refused, offer: voided } = await maybeCommit(site.slug, offerId); 355 355 if (refused) { 356 // §4.2: the refusal travels as a `Reject` of the Offer, from whoever was 357 // about to commit — the same voice that just sent the Accept. A `Reject` 358 // is what §3 already understands, so a server that has never heard of §4 359 // still voids its copy correctly; the marker only adds the reason. 360 await fanout(site, others, { 356 // §4.2: the refusal travels as a `Reject` of the Offer (§3.2), which an 357 // implementation unaware of §4 still handles correctly. Who is told WHY is 358 // not uniform, and deliberately so. 359 const answer = (to, withReason) => ({ 361 360 id: `${me}/answers/${Date.now().toString(36)}`, 362 type: 'Reject', actor: me, to: others, object: offerId, 'shaer:notATeapot': true, 361 type: 'Reject', actor: me, to, object: offerId, 362 ...(withReason ? { 'shaer:notATeapot': true } : {}), 363 363 }); 364 const candidate = voided && voided.candidate_uri; 365 // The ward and its existing guardians MUST learn the reason: they are 366 // parties, the condition is public data (§2.1), and a bare void would 367 // leave a ward believing an adoption completed that did not. 368 const family = others.filter((u) => u !== candidate); 369 if (family.length) await fanout(site, family, answer(family, true)); 370 // The candidate gets a BARE Reject. Commit is the last step of §3.1, so a 371 // refusal that names itself technical also discloses that every human 372 // party already accepted and only the protocol objected — which, where a 373 // guardianship is contested, is not theirs to learn. The kind path for an 374 // merely misconfigured candidate is the check on the Offer, before anyone 375 // has consented to anything. 376 if (candidate && others.includes(candidate)) await fanout(site, [candidate], answer([candidate], false)); 364 377 return { status: 202, id: offerId, url: offerId, committed: false, refused }; 365 378 } … … 460 473 const existing = recipients.filter((u) => u !== rel.ward); 461 474 if (rel.ward !== me && !existing.includes(me)) return false; 475 // §4.2: check the candidate here too, and refuse before anyone accepts. 476 // At this point no party has consented, so saying why discloses nothing 477 // about anyone's position, and a candidate that is merely misconfigured 478 // can find that out and fix it. The commit-time check stays REQUIRED as 479 // the backstop for a candidate whose state changes in between. 480 if (await candidateFitness(rel.candidate) === 'malformed') { 481 notify(site.slug, { kind: 'offer_refused', offer: idOf(activity), reason: 'not_a_teapot', candidate: rel.candidate }); 482 await fanout(site, [rel.candidate], { 483 id: `${me}/answers/${Date.now().toString(36)}`, 484 type: 'Reject', actor: me, to: [rel.candidate], object: idOf(activity), 'shaer:notATeapot': true, 485 }); 486 return true; 487 } 462 488 offers.start(site.slug, { 463 489 offerId: idOf(activity), ward: rel.ward, candidate: rel.candidate, existingGuardians: existing,
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)