Changeset c66cbb4 in Klonkt


Ignore:
Timestamp:
07/25/2026 08:22:15 AM (7 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
8f520e0
Parents:
ad6f62a
Message:

Authorized fetch: gecommitte guardian leest ward-outbox gesigneerd (FEP-633c §5.3 noot)

De optionele verfijning uit de spec: een guardian die de outbox van z'n ward met
een HTTP-signature opvraagt, krijgt ook de fan_only (prive) posts mee, zonder als
follower te verschijnen en inclusief historie van voor een eventuele follow.
Ongesigneerde of niet-guardian callers krijgen zoals altijd alleen de publieke
collectie. De verbrede view gaat met Cache-Control private,no-store zodat een
gedeelde cache 'm nooit aan het publiek serveert.

De publieke shaer:guardians-lijst maakt dit mogelijk: de ward-server herkent de
signer als huidige guardian (getRelation) zonder extra coordinatie.

Changed files:
src/routes/activitypub.js

  • outbox GET: signed-fetch check; guardian ziet fan_only; no-store op de verbrede view

src/services/ActivityPubService.js

  • isWardGuardian(wardSlug, actorUri); sendAP krijgt optionele cache-control

test/authorized-fetch.test.js

  • guardian herkend, vreemde niet

remarks: npm test 168/168. verifyRequest deed GET al aan (request-target/host/date,
digest alleen bij body). v1 en niet-guardian-verkeer ongewijzigd.

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

Files:
1 added
2 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    rad6f62a rc66cbb4  
    7979
    8080// ── Outbox ────────────────────────────────────────────────────────
    81 router.get('/ap/users/:slug/outbox', (req, res) => {
     81router.get('/ap/users/:slug/outbox', async (req, res) => {
    8282  const site = publicSite(req.params.slug);
    8383  if (!site) return res.status(404).end();
     84  // Authorized fetch (FEP-633c §5.3 note): a committed guardian doing a SIGNED
     85  // GET may read the ward's fan-only history too, without appearing as a
     86  // follower. Unsigned / non-guardian callers get the public collection only.
     87  let asGuardian = false;
     88  if (req.headers['signature']) {
     89    const verified = await AP.verifyRequest(req).catch(() => null);
     90    asGuardian = !!(verified && AP.isWardGuardian(req.params.slug, verified.id));
     91  }
     92  const fanClause = asGuardian ? '' : "AND (fan_only IS NULL OR fan_only = 0)";
    8493  const posts = db.prepare(
    8594    `SELECT id, slug, title, content, cover_image_url, cover_video_url, nsfw, content_warning, published_at, created_at
    86      FROM posts WHERE site_id = ? AND status = 'published' AND (fan_only IS NULL OR fan_only = 0)
     95     FROM posts WHERE site_id = ? AND status = 'published' ${fanClause}
    8796     ORDER BY COALESCE(published_at, created_at) DESC LIMIT 20`
    8897  ).all(site.id);
    89   AP.sendAP(res, AP.buildOutbox(baseUrl(req), site, posts));
     98  AP.sendAP(res, AP.buildOutbox(baseUrl(req), site, posts), asGuardian ? 'private, no-store' : undefined);
    9099});
    91100
  • src/services/ActivityPubService.js

    rad6f62a rc66cbb4  
    144144
    145145const AP_CONTENT_TYPE = 'application/activity+json; charset=utf-8';
    146 export function sendAP(res, obj) {
     146export function sendAP(res, obj, cacheControl) {
    147147  res.type(AP_CONTENT_TYPE);
    148   res.set('Cache-Control', 'public, max-age=120');
     148  // A per-caller (e.g. guardian-widened) view must not be publicly cached.
     149  res.set('Cache-Control', cacheControl || 'public, max-age=120');
    149150  res.send(JSON.stringify(obj));
    150151}
     
    29012902}
    29022903
     2904// FEP-633c §5.3 note (authorized fetch): true when `actorUri` is a committed
     2905// guardian of the local ward `wardSlug` — so a signed GET from it may read the
     2906// ward's non-public history without the guardian appearing as a follower.
     2907export function isWardGuardian(wardSlug, actorUri) {
     2908  try { return !!Guardianship.getRelation(wardSlug, 'ward', actorUri); } catch { return false; }
     2909}
     2910
    29032911// FEP-633c §5.3: the guardians approved a gated follow of their ward. Send the
    29042912// Accept to the follower and record them, so delivery (incl. followers-only)
     
    32483256  listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote,
    32493257  webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, timelineAttachments, sendInteraction, voteOnPoll, voteOnRemotePoll,
    3250   acceptGatedFollow, rejectGatedFollow,
     3258  acceptGatedFollow, rejectGatedFollow, isWardGuardian,
    32513259  parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs,
    32523260  autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, getTimelineReaction, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline,
Note: See TracChangeset for help on using the changeset viewer.