Changeset fa33214 in Klonkt for src/services
- Timestamp:
- 08/04/2026 12:55:55 PM (5 weeks ago)
- Branches:
- main
- Children:
- 04aca12, 12bed59
- Parents:
- 3d882bd
- Location:
- src/services
- Files:
-
- 1 added
- 4 edited
-
ActivityPubService.js (modified) (4 diffs)
-
guardianship/index.js (modified) (1 diff)
-
guardianship/outgoing.js (added)
-
guardianship/queues.js (modified) (3 diffs)
-
guardianship/relations.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
r3d882bd rfa33214 2487 2487 const actorUri = c2sIdOf(object); 2488 2488 if (!actorUri) return { status: 400, error: 'missing_object' }; 2489 // FEP-633c §5.3 outbound (shaer-p729): a ward asks its guardians first. 2490 // A held request is a THIRD outcome — not sent, not failed — and it 2491 // travels to the app as one, so Shaer can show "waiting" instead of a 2492 // tile that already looks followed. 2493 const held = await gateOutgoingFollow(site, actorUri); 2494 if (held) { 2495 return { 2496 status: 202, url: actorUri, id: held.id, 2497 state: held.status === 'denied' ? 'refused_by_guardian' : 'awaiting_guardian', 2498 }; 2499 } 2489 2500 // The error REACHES the app (Robins melding, 31-7): swallowing it 2490 2501 // made a failed follow look exactly like a successful one. … … 4024 4035 // Accept to the follower and record them, so delivery (incl. followers-only) 4025 4036 // begins. `pending` is a row from ap_pending_follows. 4037 /** 4038 * FEP-633c §5.3, the direction that was never gated (bead shaer-p729). 4039 * 4040 * A ward's OWN follow waited for nobody: it went straight out and the guardians 4041 * got a note afterwards (1a2f206). That is informing, not gating — the door is 4042 * already open when the message lands. Now it waits, with two exceptions that 4043 * are not favours but the same decision already taken: 4044 * 4045 * - the target is one of the ward's own guardians. Following the adult who 4046 * watches over you is not a question anyone needs to answer. 4047 * - the target already follows the ward THROUGH THE GATE. A guardian 4048 * approved that person by name; asking again about the same person only 4049 * teaches everyone to stop reading the question. 4050 * 4051 * Returns the held request, or null when the follow may go out now. 4052 * Deliberately not a boolean: a held follow must be distinguishable from a sent 4053 * one all the way up to the app, which is the lesson the error path already 4054 * learned (Robins melding, 31-7). 4055 */ 4056 export async function gateOutgoingFollow(site, targetUri) { 4057 const slug = site && site.slug; 4058 if (!slug || !targetUri) return null; 4059 const guardians = Guardianship.listGuardians(slug).map((g) => g.other_uri); 4060 if (!guardians.length) return null; // not a ward: nothing to gate 4061 if (guardians.includes(targetUri)) return null; // your own guardian 4062 if (Guardianship.outgoing.isMutual(slug, targetUri)) return null; // already vetted by name 4063 4064 const seen = Guardianship.outgoing.findFor(slug, targetUri); 4065 if (seen && seen.status === 'approved') return null; // the guardians said yes already 4066 if (seen && (seen.status === 'pending' || seen.status === 'denied')) return seen; 4067 4068 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); 4069 const wardActor = actorId(base, slug); 4070 const target = await fetchActor(targetUri).catch(() => null); 4071 const ti = actorInfo(target, targetUri); 4072 const id = `${wardActor}#outfollow-${Date.now()}-${rid()}`; 4073 const held = Guardianship.outgoing.recordPending(slug, { 4074 id, target: targetUri, 4075 inbox: target && ((target.endpoints && target.endpoints.sharedInbox) || target.inbox), 4076 name: ti.name, handle: ti.handle, icon: ti.icon, 4077 }); 4078 4079 // Same routing as the inbound gate: a guardian on this instance gets a push 4080 // and reads /guardian; one elsewhere gets an Offer delivered so its own 4081 // server holds a copy to answer from. 4082 const wardKeys = getOrCreateKeys(slug); 4083 const followObj = { id, type: 'Follow', actor: wardActor, object: targetUri }; 4084 for (const g of guardians) { 4085 try { Guardianship.availability.recordRequest(slug, g, id, Date.now()); } catch { /* never load-bearing */ } 4086 } 4087 for (const g of guardians) { 4088 const gslug = g.startsWith(`${base}/`) ? slugFromActorUrl(g) : null; 4089 const isLocal = gslug && db.prepare('SELECT 1 FROM sites WHERE slug = ?').get(gslug); 4090 if (isLocal) { 4091 const L = pushLang(gslug); 4092 pushEvent(gslug, { type: 'guardian', title: i18nT(L, 'push.n_guard_cog_t'), body: i18nT(L, 'push.n_guard_cog_b', { who: ti.name || ti.handle || i18nT(L, 'notif.someone') }), url: `${pushPrefix(gslug)}/guardian` }); 4093 } else { 4094 fetchActor(g).then((ga) => { 4095 const inbox = ga && ((ga.endpoints && ga.endpoints.sharedInbox) || ga.inbox); 4096 if (!inbox) return; 4097 const offer = { '@context': AP_CONTEXT, id: `${wardActor}#outfollowoffer-${Date.now()}-${rid()}`, type: 'Offer', actor: wardActor, to: [g], object: followObj, 'shaer:followApproval': true, 'shaer:direction': 'outgoing' }; 4098 deliverWithRetry(slug, inbox, offer, `${wardActor}#main-key`, wardKeys.private_pem).catch(() => {}); 4099 }).catch(() => {}); 4100 } 4101 } 4102 console.log('[AP] outgoing Follow', slug, '→', targetUri, '(gated, awaiting guardians)'); 4103 return held || { id, ward_slug: slug, target_uri: targetUri, status: 'pending' }; 4104 } 4105 4106 /** 4107 * The guardians said yes: send the ward's Follow for real (§5.3, shaer-p729). 4108 * 4109 * The row stays behind as `approved` rather than being deleted. It is the 4110 * record that these guardians vetted this target, so an unfollow-and-refollow 4111 * later does not put the same question in front of them again. 4112 */ 4113 export async function performApprovedFollow(pending) { 4114 const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(pending.ward_slug); 4115 if (!site) return { error: 'no_such_ward' }; 4116 const r = await followActor(site, pending.target_uri); 4117 if (r && r.error) return { error: r.error }; 4118 console.log('[AP] outgoing Follow approved', pending.ward_slug, '→', pending.target_uri); 4119 return { ok: true }; 4120 } 4121 4026 4122 export async function acceptGatedFollow(pending) { 4027 4123 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); … … 4030 4126 const keys = getOrCreateKeys(slug); 4031 4127 fStmts().ins.run(slug, pending.follower_uri, pending.follower_inbox, pending.follower_shared_inbox, pending.follower_name, pending.follower_handle, pending.follower_icon); 4128 // This follower came through the §5.3 gate: a guardian said yes to this 4129 // person by name. That is precisely what lets the ward follow them back later 4130 // without asking the same guardians the same question twice (shaer-p729). 4131 db.prepare('UPDATE ap_followers SET gate_approved = 1 WHERE slug = ? AND actor_uri = ?').run(slug, pending.follower_uri); 4032 4132 const original = pending.activity_json ? JSON.parse(pending.activity_json) : { type: 'Follow', actor: pending.follower_uri, object: me }; 4033 4133 const accept = { '@context': AP_CONTEXT, id: `${me}#accept-${Date.now()}-${rid()}`, type: 'Accept', actor: me, object: original }; … … 4512 4612 webfingerResolve, followActor, resolveRemoteActor, unfollowActor, handleMoveInbox, moveAccount, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, getDirectMessages, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll, 4513 4613 acceptGatedFollow, rejectGatedFollow, isWardGuardian, outboxAudience, sendFollowDecision, 4614 gateOutgoingFollow, performApprovedFollow, 4514 4615 parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs, 4515 4616 autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, getTimelineReaction, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline, -
src/services/guardianship/index.js
r3d882bd rfa33214 19 19 export { wireDelivery, c2sVisibility, deliverDirectNote } from './delivery.js'; 20 20 export { wireHandshake, handleOutbox as handleGuardianshipOutbox, handleInbox as handleGuardianshipInbox, parseRelationship, parseUndoRelationship, endGuardianship } from './handshake.js'; 21 export { offersCollection, followsCollection, wardsCollection, guardiansCollection } from './queues.js';21 export { offersCollection, followsCollection, outgoingFollowsCollection, wardsCollection, guardiansCollection } from './queues.js'; 22 22 export * as availability from './availability.js'; 23 23 export { wireAvailability } from './availability.js'; 24 24 export * as follows from './follows.js'; 25 export * as outgoing from './outgoing.js'; 25 26 export { listForParty as listOffersForParty, getOffer, findOfferAnywhere } from './offers.js'; 26 27 export { -
src/services/guardianship/queues.js
r3d882bd rfa33214 12 12 import * as relations from './relations.js'; 13 13 import * as availability from './availability.js'; 14 import * as outgoing from './outgoing.js'; 14 15 import * as handshake from './handshake.js'; 15 16 … … 39 40 } 40 41 42 /** §5.3 outbound: this ward's own follow requests, waiting for its guardians. */ 43 export function outgoingFollowsCollection(id, slug, me) { 44 return collection(id, outgoing.listForWard(slug).map((o) => outgoing.queueItem(o, me))); 45 } 46 41 47 /** The guardian's committed wards, with cached handle for display. */ 42 48 export function wardsCollection(id, slug) { … … 53 59 } 54 60 55 export default { offersCollection, followsCollection, wardsCollection, guardiansCollection };61 export default { offersCollection, followsCollection, outgoingFollowsCollection, wardsCollection, guardiansCollection }; -
src/services/guardianship/relations.js
r3d882bd rfa33214 73 73 offers: `${id}/queues/offers`, 74 74 follows: `${id}/queues/follows`, 75 // Both directions of §5.3, kept apart on purpose: a guardian must be able 76 // to tell "someone wants to follow your ward" from "your ward wants to 77 // follow someone". Same mechanism, opposite question, different words in 78 // the interface (shaer-p729). 79 outgoingFollows: `${id}/queues/outgoing-follows`, 75 80 wards: `${id}/queues/wards`, 76 81 guardians: `${id}/queues/guardians`,
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)