Changeset 6eab7e9 in Klonkt for src/services/guardianship/handshake.js
- Timestamp:
- 07/28/2026 11:22:07 PM (6 weeks ago)
- Branches:
- main
- Children:
- 0202104
- Parents:
- 6c152a5
- File:
-
- 1 edited
-
src/services/guardianship/handshake.js (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/guardianship/handshake.js
r6c152a5 r6eab7e9 20 20 import * as relations from './relations.js'; 21 21 import * as gated from './gated.js'; 22 import * as availability from './availability.js'; 22 23 23 24 let deps = null; … … 194 195 if (!['Offer', 'Accept', 'Reject', 'Undo'].includes(type)) return null; 195 196 const me = deps.selfId(site.slug); 197 // One answer restores everything (§3.6): any C2S activity from this actor 198 // is that answer, for every local ward it guards. Runs before anything is 199 // even looked at, so the target of a running lapse cancels it by doing 200 // anything at all — including trying to vote on it. 201 try { availability.oneAnswer(me, Date.now()); } catch { /* never load-bearing */ } 196 202 197 203 // ── Undo: a guardian ends its own guardianship (§3.2). Same path as the … … 206 212 // ── Offer: the local site is the guardian-candidate. ─────────────────── 207 213 if (type === 'Offer') { 214 // §3.6.3 over C2S: a guardian here proposes releasing a dormant 215 // co-guardian. A ward we host opens locally; a remote ward gets the 216 // proposal delivered, because the ward's server is the one that tallies 217 // and enforces (the §5.6 line: a guardian next door must not have more 218 // say than one far away). 219 const lp = availability.parseLapse(activity.object); 220 if (lp) { 221 const id = `${me}/lapses/${Date.now().toString(36)}${Math.floor(Math.random() * 1e4).toString(36)}`; 222 const wardSlug = deps.localSlug(lp.ward); 223 if (wardSlug) { 224 const r = availability.openLapse({ id, wardSlug, wardUri: lp.ward, target: lp.target, openedBy: me, now: Date.now() }); 225 if (r.error) return { status: r.error === 'not_in_available_set' ? 403 : 409, error: r.error }; 226 deps.deliverTo(site, lp.target, { id, type: 'Offer', actor: me, to: [lp.target], object: { type: 'shaer:Lapse', 'shaer:ward': lp.ward, object: lp.target } }).catch(() => { /* best-effort */ }); 227 notify(wardSlug, { kind: 'lapse_opened', lapse: id, target: lp.target, set: r.set }); 228 return { status: 202, id, url: id, 'shaer:set': r.set, 'shaer:threshold': r.threshold }; 229 } 230 const offer = { id, type: 'Offer', actor: me, to: [lp.ward], object: { type: 'shaer:Lapse', 'shaer:ward': lp.ward, object: lp.target } }; 231 const delivered = await fanout(site, [lp.ward], offer); 232 return { status: 202, id, url: id, delivered }; 233 } 208 234 const rel = parseRelationship(activity.object); 209 235 if (!rel) return null; … … 231 257 const offerId = idOf(activity.object); 232 258 if (!offerId) return { status: 400, error: 'missing_offer' }; 259 // A lapse vote over C2S (§3.6.3): the same Accept/Reject wire the offers 260 // and gated follows use, which is exactly why the Shaer clients need no 261 // new verbs for it. 262 if (availability.getLapse(offerId)) { 263 const r = availability.lapseVote(offerId, me, type === 'Accept', Date.now()); 264 if (r && r.error) return { status: r.error === 'not_in_set' ? 403 : 409, error: r.error }; 265 return { status: 202, id: offerId, url: offerId, 'shaer:outcome': 'open', 'shaer:accepts': r.accepts, 'shaer:threshold': r.threshold }; 266 } 233 267 let offer = offers.getOffer(site.slug, offerId); 234 268 if (!offer) return { status: 404, error: 'no_such_offer' }; … … 273 307 const r = gated.recordGatedVote(site.slug, gs.feature, actor, gs.value); 274 308 notify(site.slug, { kind: 'gated_setting', feature: gs.feature, value: gs.value, state: r.state }); 309 return true; 310 } 311 // §3.6.3: a co-guardian proposes releasing a dormant guardian of THIS 312 // ward. The ward's server opens, tallies and (after the full window) 313 // executes, exactly as it does for the gated settings above. 314 const lp = availability.parseLapse(activity.object); 315 if (lp) { 316 if (lp.ward !== me) return false; // not our ward 317 const id = idOf(activity) || `${me}/lapses/${Date.now().toString(36)}${Math.floor(Math.random() * 1e4).toString(36)}`; 318 const r = availability.openLapse({ id, wardSlug: site.slug, wardUri: me, target: lp.target, openedBy: actor, now: Date.now() }); 319 if (r.error) { 320 notify(site.slug, { kind: 'lapse_refused', reason: r.error, target: lp.target }); 321 return true; // consumed: the refusal is the answer 322 } 323 // The target is notified like any dormancy marking (§3.6.2): in 324 // protocol (a copy of the Offer, so one answer can cancel it) AND the 325 // §6 handle, which for a committed guardian is its inbox — the same 326 // door this delivery knocks on. 327 deps.deliverTo(site, lp.target, activity).catch(() => { /* best-effort */ }); 328 notify(site.slug, { kind: 'lapse_opened', lapse: id, target: lp.target, set: r.set }); 275 329 return true; 276 330 } … … 304 358 return true; 305 359 } 360 // §3.6.3: a set member answering a running lapse. Irreversible, so even a 361 // full tally leaves it open until the window closes (§3.5); the completion 362 // happens lazily on reads (queues) once the window has run. 363 if (availability.getLapse(offerId)) { 364 const r = availability.lapseVote(offerId, actor, type === 'Accept', Date.now()); 365 notify(site.slug, { kind: 'lapse_vote', lapse: offerId, by: actor, state: r && !r.error ? 'recorded' : (r && r.error) || 'refused' }); 366 return true; 367 } 306 368 let offer = offers.getOffer(site.slug, offerId); 307 369 if (!offer) return false;
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)