Changeset 61d52ed in Klonkt for src/routes/activitypub.js


Ignore:
Timestamp:
10/14/2026 04:25:21 PM (6 hours ago)
Author:
Bart <bart@…>
Branches:
main
Parents:
6e9bc70
git-author:
Bart <bart@…> (10/14/2026 04:23:29 PM)
git-committer:
Bart <bart@…> (10/14/2026 04:25:21 PM)
Message:

Eén note ophalen, voor wie een gesprek naar boven volgt

De thread-route (shaer-tqz) geeft wat ONDER een post hangt. Een lezer die een
antwoord kreeg en de ouder niet, kon die ouder bij een server in secure mode
niet halen: daar telt alleen een ondertekend verzoek, en de sleutel woont hier.
R9999 haalt ouders voortaan op zodra een antwoord binnenkomt, en heeft daar
deze route voor nodig -- zonder anonieme terugval, om dezelfde reden als bij de
thread.

GET /ap/users/:slug/object?uri= -- bearer-only en niets bewaren, dezelfde
weigering-of-storing (404/502) en dezelfde poorten als de thread. Een
geblokkeerde auteur is er niet. Voor een ward met een dichte kring is een note
van buiten de kring een 404 'outside the circle', met shaer:hidden erbij in
plaats van verzwegen.

inReplyTo alleen als de bron hem gaf. De thread vult een ontbrekende inReplyTo
aan met de post waaronder het antwoord in de replies-collectie stond, en daar is
dat bewijs. Voor een losse note is er geen bewijs, en een verzonnen ouder is
erger dan geen.

De vorm van één note (readerNote) en de poorten (readerGates) zijn uit de
thread getild, zodat de twee niet uit elkaar kunnen lopen. De thread-route
gedraagt zich hetzelfde.

Tests: getObject (de ouder van de bron, geen verzonnen ouder voor een wortel,
blokkade, weigering) en de route (zonder bearer, andermans bearer, alleen
https, weigering is 404).

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    r6e9bc70 r61d52ed  
    14111411});
    14121412
     1413// De poorten op één vreemde note, voor de thread en voor de losse note:
     1414// beeld, muziek, video en emoji, per verzoek en buiten elke cache om. De
     1415// emoji-poort knipt in de byline zelf: FEP-9098 zit in de tag van de
     1416// ingesloten actor, niet meer in een eigen emoji-kaart ernaast.
     1417function readerGates(site, isWard) {
     1418  const gate = (col) => Guardianship.wardGateAllowed(site[col], isWard);
     1419  const images = gate('gate_images'), audio = gate('gate_music'), video = gate('gate_video');
     1420  const emoji = gate('gate_custom_emoji');
     1421  return (n) => ({
     1422    ...n,
     1423    attachment: AP.gateAttachments(n.attachment, { images, audio, video }),
     1424    tag: emoji ? n.tag : AP.stripEmojiTags(n.tag),
     1425    attributedTo: (!emoji && n.attributedTo && typeof n.attributedTo === 'object')
     1426      ? { ...n.attributedTo, tag: undefined } : n.attributedTo,
     1427  });
     1428}
     1429
    14131430// ── De thread onder een post (shaer-tqz): ophalen, niet bewaren ────
    14141431//
    … …  
    14461463  // poorten als de tijdlijn -- per verzoek, buiten de threadcache om.
    14471464  const threadsOpen = Guardianship.wardGateAllowed(auth.site.external_threads, isWard);
    1448   const gate2 = (col) => Guardianship.wardGateAllowed(auth.site[col], isWard);
    14491465  const kring = threadsOpen ? { notes: uit.notes, hidden: 0 } : AP.filterThreadToCircle(auth.site.slug, uit.notes);
    1450   const imagesOk = gate2('gate_images'), musicOk = gate2('gate_music'), emojiOk = gate2('gate_custom_emoji');
    1451   const videoOk = gate2('gate_video');
    1452   uit.notes = kring.notes.map((n) => ({
    1453     ...n,
    1454     attachment: AP.gateAttachments(n.attachment, { images: imagesOk, audio: musicOk, video: videoOk }),
    1455     tag: emojiOk ? n.tag : AP.stripEmojiTags(n.tag),
    1456     // De emoji-poort knipt in de byline zelf: FEP-9098 zit in de tag van de
    1457     // ingesloten actor, niet meer in een eigen emoji-kaart ernaast.
    1458     attributedTo: (!emojiOk && n.attributedTo && typeof n.attributedTo === 'object')
    1459       ? { ...n.attributedTo, tag: undefined } : n.attributedTo,
    1460   }));
     1466  uit.notes = kring.notes.map(readerGates(auth.site, isWard));
    14611467  uit.hidden = kring.hidden;
    14621468  // Liked/boosted per antwoord, BUITEN de cache om: de genormaliseerde notes
    … …  
    14811487});
    14821488
     1489// ── Eén note, voor wie een gesprek naar boven volgt ────────────────
     1490//
     1491// De thread hierboven geeft wat onder een post hangt; dit geeft de post zelf.
     1492// Een lezer die een antwoord kreeg en de ouder niet, kan die ouder bij een
     1493// server in secure mode alleen ondertekend halen, en de sleutel staat hier.
     1494// Bearer-only en niets bewaren, net als de thread. Dezelfde weigering-of-
     1495// storing en dezelfde poorten; voor een ward met een dichte kring is een note
     1496// van buiten de kring er niet, en dat wordt gezegd in plaats van verzwegen.
     1497router.get('/ap/users/:slug/object', async (req, res) => {
     1498  const auth = OAuth.verifyBearer(req.headers.authorization);
     1499  if (!auth || auth.site.slug !== req.params.slug) return res.status(403).end();
     1500  const objectUri = String(req.query.uri || '');
     1501  if (!/^https:\/\//i.test(objectUri)) return res.status(400).json({ error: 'uri must be an https URI' });
     1502  const uit = await AP.getObject(auth.site.slug, objectUri);
     1503  if (!uit.found) {
     1504    const geweigerd = [401, 403, 404, 410].includes(uit.sourceStatus);
     1505    return res.status(geweigerd ? 404 : 502)
     1506      .json({ error: geweigerd ? 'not shared by source' : 'source unreachable', sourceStatus: uit.sourceStatus || undefined });
     1507  }
     1508  const isWard = (() => { try { return Guardianship.listGuardians(auth.site.slug).length > 0; } catch { return false; } })();
     1509  const threadsOpen = Guardianship.wardGateAllowed(auth.site.external_threads, isWard);
     1510  if (!threadsOpen && !AP.filterThreadToCircle(auth.site.slug, [uit.note]).notes.length) {
     1511    return res.status(404).json({ error: 'outside the circle', 'shaer:hidden': 1 });
     1512  }
     1513  AP.sendAP(res, { '@context': AP.AP_CONTEXT, ...readerGates(auth.site, isWard)(uit.note) }, 'private, no-store');
     1514});
     1515
    14831516router.get('/ap/notes/:id/replies', (req, res) => {
    14841517  const base = baseUrl(req);
Note: See TracChangeset for help on using the changeset viewer.