Changeset 1a2f206 in Klonkt for src


Ignore:
Timestamp:
07/30/2026 11:17:05 PM (6 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
ef519a3
Parents:
55eca8b
Message:

Follow: gesigneerd resolven, fouten naar de app, guardians ingelicht, volg-QR

Robins meldingen (31-7). Vier dingen aan de volg-kant.

Een: followActor haalde het actor-document anoniem op; een
authorized-fetch-instance weigert dat, waardoor volgen vanaf een boost
faalde. De fetch is nu gesigneerd als de eigen actor.

Twee: de C2S-ingest slikte followActor-fouten in en gaf altijd 202,
dus een mislukte follow zag er in de app uit als een gelukte. Fouten
komen nu als 502 follow_failed (met detail) bij de app aan, met test.

Drie: de guardians worden ingelicht als hun ward iemand gaat volgen:
een follow brengt nieuwe content het kind binnen, en het dorp hoort te
weten dat de deur openging. Een directe note per guardian,
best-effort. FEP-633c 5.3 gate't inkomende follows; deze uitgaande
melding is Shaer-beleid (spec-vraag als bead).

Vier: GET /ap/users/:slug/follow-qr.png serveert een QR-PNG van
share:social/follow/AP/@slug@host (npm qrcode, puur JS). Publiek met
opzet: er staat alleen de publieke handle in, en de plain image-loaders
van de apps dragen geen bearer.

Changed files:
src/services/ActivityPubService.js

  • followActor: signedGetJson voor het actor-doc; guardian-notice
  • C2S Follow-case: fouten door naar de app

src/routes/activitypub.js

  • follow-qr.png-route (cache 1 dag)

package.json / package-lock.json

  • qrcode-dependency

test/c2s-compose.test.js

  • onbereikbare follow geeft 502 follow_failed/unreachable

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

Location:
src
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    r55eca8b r1a2f206  
    121121  }
    122122  AP.sendAP(res, ob, audience === 'friend' ? 'private, no-store' : undefined);
     123});
     124
     125// ── Follow-QR (Robins verzoek, 31-7) ──────────────────────────────
     126// A PNG QR of share:social/follow/AP/@slug@host: the ward shows it in
     127// Account, a friend scans the SCREEN with the ordinary camera app and their
     128// Shaer opens with the follow question. Public on purpose: it encodes only
     129// the public handle, and the app's plain image loaders carry no bearer.
     130router.get('/ap/users/:slug/follow-qr.png', async (req, res) => {
     131  const site = db.prepare('SELECT slug FROM sites WHERE slug = ?').get(req.params.slug);
     132  if (!site) return res.status(404).end();
     133  try {
     134    const host = new URL(baseUrl(req)).host;
     135    const { default: QRCode } = await import('qrcode');
     136    const png = await QRCode.toBuffer(`share:social/follow/AP/@${site.slug}@${host}`, { width: 600, margin: 1 });
     137    res.set('Content-Type', 'image/png');
     138    res.set('Cache-Control', 'public, max-age=86400');
     139    res.send(png);
     140  } catch (e) {
     141    console.warn('[AP] follow-qr failed:', e && e.message);
     142    res.status(500).end();
     143  }
    123144});
    124145
  • src/services/ActivityPubService.js

    r55eca8b r1a2f206  
    23832383        const actorUri = c2sIdOf(object);
    23842384        if (!actorUri) return { status: 400, error: 'missing_object' };
    2385         await followActor(site, actorUri);
     2385        // The error REACHES the app (Robins melding, 31-7): swallowing it
     2386        // made a failed follow look exactly like a successful one.
     2387        const r = await followActor(site, actorUri);
     2388        if (r && r.error) return { status: 502, error: 'follow_failed', detail: r.error };
    23862389        return { status: 202, url: actorUri };
    23872390      }
     
    36453648  else actorUrl = null;
    36463649  if (!actorUrl) return { error: 'not_found' };
    3647   const actor = await fetchActor(actorUrl).catch(() => null);
     3650  // SIGNED, as this actor: an authorized-fetch instance refuses an anonymous
     3651  // GET of the actor doc, which made following from a boost silently fail
     3652  // (Robins melding, 31-7). Signed, the other side sees who asks.
     3653  const actor = await signedGetJson(site.slug, actorUrl);
    36483654  if (!actor || !actor.id || !actor.inbox) return { error: 'unreachable' };
    36493655  const ai = actorInfo(actor, actor.id);
     
    36603666  // Follow + feature in one step → backfill their recent posts into the Cirkel right away.
    36613667  if (autoBoost) backfillFromOutbox(site.slug, actor.id).catch(() => {});
     3668  // A ward's guardians are TOLD about a new follow (Robins verzoek, 31-7):
     3669  // a follow brings new content into the child's feed, and the village
     3670  // should know the door opened. A direct note per guardian, best-effort;
     3671  // FEP-633c 5.3 gates inbound follows, the outbound notice is Shaer policy
     3672  // for now (bead: spec-vraag).
     3673  try {
     3674    const guardians = Guardianship.listGuardians(site.slug);
     3675    if (guardians.length) {
     3676      const meRef = actorId(base, site.slug);
     3677      const esc = (t) => String(t).replace(/[<>&]/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;' }[c]));
     3678      const label = esc(ai.name || ai.handle || actor.id);
     3679      for (const g of guardians) {
     3680        const note = {
     3681          id: `${meRef}/follow-notice/${Date.now().toString(36)}${rid()}`,
     3682          type: 'Note', attributedTo: meRef, to: [g.other_uri],
     3683          tag: [{ type: 'Mention', href: g.other_uri }],
     3684          content: `<p>👀 ${esc(site.title || site.slug)} is now following ${label}.</p>`,
     3685        };
     3686        deliverToActor(site, g.other_uri, { id: `${note.id}#create`, type: 'Create', actor: meRef, to: [g.other_uri], object: note })
     3687          .catch(() => { /* retried by the queue */ });
     3688      }
     3689      console.log('[AP] follow notice →', guardians.length, 'guardian(s) of', site.slug);
     3690    }
     3691  } catch { /* geen guardians is geen fout */ }
    36623692  return { ok: true, name: ai.name, handle: ai.handle, actor: actor.id };
    36633693}
Note: See TracChangeset for help on using the changeset viewer.