Changeset 6eab7e9 in Klonkt for src/services/ActivityPubService.js
- Timestamp:
- 07/28/2026 11:22:07 PM (6 weeks ago)
- Branches:
- main
- Children:
- 0202104
- Parents:
- 6c152a5
- File:
-
- 1 edited
-
src/services/ActivityPubService.js (modified) (7 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
r6c152a5 r6eab7e9 288 288 ...Guardianship.helpRequestProps(post), 289 289 ...Guardianship.waveProps(post), 290 ...Guardianship.awayProps(post), 290 291 // FEP-633c §2.2: object hint that the author is a ward. 291 292 ...Guardianship.hasGuardiansProps(site.slug), … … 1365 1366 return 401; 1366 1367 } 1368 // One answer restores everything (FEP-633c 3.6): any VERIFIED activity 1369 // from an actor that guards someone here restores it to active for those 1370 // wards and cancels any lapse running against it, before the activity is 1371 // even looked at. Signature-gated on purpose: an unverified claim of 1372 // being gran must not wake gran up. 1373 try { 1374 const ev = Guardianship.availability.oneAnswer(claimedActor, Date.now()); 1375 if (ev.restored.length) console.log('[AP] guardian restored (one answer, 3.6):', claimedActor, '→', ev.restored.join(', ')); 1376 for (const c of ev.cancelledLapses) console.log('[AP] lapse cancelled by an answer from its target:', c.id); 1377 } catch { /* availability is never load-bearing for delivery */ } 1367 1378 } 1368 1379 … … 1454 1465 const wardKeys = getOrCreateKeys(slug); 1455 1466 const followObj = { id: followId, type: 'Follow', actor: who, object: wardActor }; 1467 // Dormancy evidence (FEP-633c 3.6.2): this decision directly addresses 1468 // every guardian. The ONLY admissible evidence is a request like this 1469 // one going unanswered; recordRequest itself skips a declared absence. 1470 for (const g of wardGuardians) { 1471 try { Guardianship.availability.recordRequest(slug, g, followId, Date.now()); } catch { /* never load-bearing */ } 1472 } 1456 1473 for (const g of wardGuardians) { 1457 1474 // Local ONLY when the guardian lives on THIS instance: slugFromActorUrl … … 1637 1654 const wave = Guardianship.isWave(o); 1638 1655 const hasG = Guardianship.objectHasGuardians(o); // §2.2 hint, register-only 1656 // FEP-633c 3.6.1: a guardian declares itself away to its ward, on the 1657 // same direct note the mention below stores (so the kid also reads it 1658 // as an ordinary message). Recorded only from an actual guardian of 1659 // the addressed ward, and only with an end: an absence without an end 1660 // is logged and dropped, never guessed. 1661 if (Guardianship.availability.isAway(o)) { 1662 const until = Guardianship.availability.parseEndTime(o.endTime); 1663 for (const slug of slugs) { 1664 const isG = (() => { try { return Guardianship.listGuardians(slug).some((g) => g.other_uri === actorUri); } catch { return false; } })(); 1665 if (!isG) continue; 1666 if (!until || until <= Date.now()) { console.warn('[AP] away without a (future) end ignored (3.6.1):', actorUri, '→', slug); continue; } 1667 Guardianship.availability.declareAway(slug, actorUri, until); 1668 console.log('[AP] guardian declared away (3.6.1):', actorUri, '→', slug, 'until', new Date(until).toISOString()); 1669 } 1670 } 1639 1671 for (const slug of slugs) { 1640 1672 try { … … 2213 2245 .filter(Boolean); 2214 2246 const help = object['shaer:helpRequest'] === true || object.helpRequest === true; 2215 const r = await deliverDirectNote(site, { recipients, text: plain, language: object.language || null, inReplyTo: typeof object.inReplyTo === 'string' ? object.inReplyTo : null, attachments: atts, helpRequest: help }); 2247 // FEP-633c 3.6.1: a guardian here declaring itself away to its 2248 // wards. An away without a (future) end fails loudly, exactly as 2249 // the daemon refuses it: stored quietly it would be a nominal 2250 // guardian holding a seat. 2251 let awayUntil = null; 2252 if (Guardianship.availability.isAway(object)) { 2253 awayUntil = Guardianship.availability.parseEndTime(object.endTime); 2254 if (!awayUntil || awayUntil <= Date.now()) return { status: 400, error: 'away_needs_an_end' }; 2255 // A ward we host ourselves never receives its own delivery 2256 // (private ranges, loopback): apply locally, the way the 2257 // handshake commit does. 2258 const meUri = selfActorId(site.slug); 2259 for (const uri of recipients) { 2260 const wslug = uri.startsWith(`${base}/`) ? slugFromActorUrl(uri) : null; 2261 if (wslug && Guardianship.listGuardians(wslug).some((g) => g.other_uri === meUri)) { 2262 Guardianship.availability.declareAway(wslug, meUri, awayUntil); 2263 } 2264 } 2265 } 2266 const r = await deliverDirectNote(site, { recipients, text: plain, language: object.language || null, inReplyTo: typeof object.inReplyTo === 'string' ? object.inReplyTo : null, attachments: atts, helpRequest: help, awayUntil }); 2216 2267 if (!r || !r.id) return { status: 502, error: 'direct_failed' }; 2217 2268 return { status: 201, id: r.id, url: `${base}/ap/notes/${r.id}` }; … … 3439 3490 const pending = Guardianship.follows.getPending(followId); 3440 3491 if (!pending) return false; 3441 const guardians = Guardianship.listGuardians(pending.ward_slug).map((g) => g.other_uri);3442 if (! guardians.includes(actorUri)) return false; // only a real guardian of this ward decides3492 const allGuardians = Guardianship.listGuardians(pending.ward_slug).map((g) => g.other_uri); 3493 if (!allGuardians.includes(actorUri)) return false; // only a real guardian of this ward decides 3443 3494 const decision = type === 'Reject' ? 'reject' : 'approve'; 3495 // §3.5: the quorum runs over the AVAILABLE set. The voter itself was 3496 // restored by the one-answer rule when its activity arrived, so answering 3497 // is exactly what counts a guardian back in. 3498 const guardians = Guardianship.availability.availableSet(pending.ward_slug, allGuardians, Date.now()); 3444 3499 const r = Guardianship.follows.decide(followId, actorUri, decision, guardians); 3445 3500 try { … … 3784 3839 }); 3785 3840 3841 // The notification duty of FEP-633c 3.6.2, wired once for every place a 3842 // dormancy promotion can happen (queue reads, fan-outs, tallies): marking a 3843 // guardian dormant MUST notify it, in protocol AND over the §6 handle. The 3844 // one-answer rule is worthless to someone who does not know an answer is 3845 // wanted. The handle of a committed guardian is its inbox (§6 minimum), which 3846 // is the same door this delivery knocks on; both attempts are logged. 3847 Guardianship.wireAvailability({ 3848 onDormant: (wardSlug, guardianUri) => { 3849 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); 3850 const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(wardSlug); 3851 if (!base || !site) return; 3852 const me = selfActorId(wardSlug); 3853 const note = { 3854 id: `${me}/dormant/${Date.now().toString(36)}${rid()}`, 3855 type: 'Note', attributedTo: me, to: [guardianUri], 3856 'shaer:dormant': true, 3857 content: '<p>You have been observed dormant as a guardian. Nothing is wrong and nothing is held against you: one answer restores everything (FEP-633c 3.6.2).</p>', 3858 }; 3859 deliverToActor(site, guardianUri, { id: `${note.id}#create`, type: 'Create', actor: me, to: [guardianUri], object: note }) 3860 .catch(() => { /* retried by the queue */ }); 3861 console.log('[AP] guardian observed dormant (3.6.2):', guardianUri, 'ward', wardSlug, '(notified in protocol; the §6 handle is the same inbox)'); 3862 }, 3863 }); 3864 3786 3865 export default { 3787 3866 AP_CONTEXT, getOrCreateKeys, apWants, sendAP, actorId, noteId,
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)