Changeset c66cbb4 in Klonkt for src/routes


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@…>

File:
1 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
Note: See TracChangeset for help on using the changeset viewer.