Changeset aae5881 in Klonkt


Ignore:
Timestamp:
08/10/2026 08:14:13 AM (4 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
86ec01b
Parents:
75f2897
Message:

De thread-melding wijst nu naar de juiste partij

Bart opende een post van boiert.eu en las "Replies could not be loaded from
your server" terwijl onze server het prima deed: de BRON weigerde. Het was een
friends-only post van een account dat hij 's ochtends nog volgde en nu niet
meer, en Klonkt geeft een niet-publieke note alleen aan followers -- boiert
weigerde dus terecht, met 404, ook op het ondertekende verzoek.

signedGetJson slikte die status ("if (!r.ok) return null"), dus getThread wist
niet WAAROM het misging en de route kon het niet zeggen. Nu geeft hij de status
door aan wie erom vraagt, en getThread draagt hem als sourceStatus.

De route splitst daarop: 401/403/404/410 is een besluit van die server (niet
gedeeld, of weg) en blijft 404 'not shared by source'; al het andere, inclusief
een status die we niet eens kregen, wordt 502 'source unreachable'. Zonder dat
onderscheid vervang je een verkeerde schuldige door een andere gok.

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

Files:
3 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    r75f2897 raae5881  
    938938  const isWard = (() => { try { return Guardianship.listGuardians(auth.site.slug).length > 0; } catch { return false; } })();
    939939  const uit = await AP.getThread(auth.site.slug, objectUri);
    940   if (!uit.found) return res.status(404).json({ error: 'note not reachable' });
     940  if (!uit.found) {
     941    // WIENS schuld is dit? De oude melding zei "jouw server kon het niet
     942    // laden" terwijl onze server het prima deed en de BRON weigerde -- dat
     943    // wees naar de verkeerde partij (Barts melding, 10-8: een post van een
     944    // account dat hij vanochtend nog volgde, en dat nu niet meer).
     945    // 401/403/404/410 is een besluit van die server; al het andere, inclusief
     946    // een status die we niet eens kregen, is een storing.
     947    const geweigerd = [401, 403, 404, 410].includes(uit.sourceStatus);
     948    return res.status(geweigerd ? 404 : 502)
     949      .json({ error: geweigerd ? 'not shared by source' : 'source unreachable', sourceStatus: uit.sourceStatus || undefined });
     950  }
    941951  // De poortstand komt uit de kolom (shaer-9y2): expliciete 0/1 van de
    942952  // guardians wint, de automatiek is dicht-voor-een-ward. Dicht is de KRING,
  • src/services/ActivityPubService.js

    r75f2897 raae5881  
    36983698  if (hit && Date.now() - hit.at < THREAD_VIEW_TTL_MS) return hit.out;
    36993699
    3700   const get = (u) => localNoteObject(u, slug) || signedGetJson(slug, u);
     3700  // De status waarmee de BRON antwoordde op de note zelf. 401/403/404/410 is
     3701  // een besluit van die server (niet gedeeld, of weg); alles daarbuiten -- ook
     3702  // een stuk netwerk dat wegviel -- is een storing. De route moet dat verschil
     3703  // kunnen zeggen, anders wijst de melding naar de verkeerde partij.
     3704  let sourceStatus = 0;
     3705  const get = (u) => localNoteObject(u, slug) || signedGetJson(slug, u, (st) => { sourceStatus = st; });
    37013706  const note = await get(objectUri);
    37023707  const repliesRef = note && note.replies;
     
    37813786  })).sort((a, b) => String(a.published || '').localeCompare(String(b.published || '')));
    37823787
    3783   const out = { notes, found: !!note };
     3788  const out = { notes, found: !!note, sourceStatus };
    37843789  threadViewCache.set(key, { at: Date.now(), out });
    37853790  if (threadViewCache.size > THREAD_VIEW_CACHE_MAX) {
     
    50015006 * (request-target) host date, the set verifyRequest checks.
    50025007 */
    5003 async function signedGetJson(slug, url) {
     5008async function signedGetJson(slug, url, onStatus) {
    50045009  try {
    50055010    const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     
    50145019    const sig = `keyId="${me}#main-key",algorithm="rsa-sha256",headers="(request-target) host date",signature="${signature}"`;
    50155020    const r = await safeFetch(url, { headers: { Accept: 'application/activity+json', Date: date, Signature: sig } });
     5021    // De status doorgeven aan wie erom vroeg: null alleen zegt "het lukte
     5022    // niet", en dat is te weinig om een WEIGERING van een STORING te
     5023    // onderscheiden. Wie geen callback meegeeft merkt hier niets van.
     5024    if (typeof onStatus === 'function') onStatus(r.status);
    50165025    if (!r.ok) return null;
    50175026    const len = Number(r.headers.get('content-length') || 0);
  • test/thread.test.js

    r75f2897 raae5881  
    193193  assert.ok(!na.attributedTo.tag, 'ook niet in de byline');
    194194});
     195
     196// ── Wiens schuld is het? ─────────────────────────────────────────────
     197//
     198// "Replies could not be loaded from your server" wees naar de verkeerde
     199// partij: onze server deed het prima en de BRON weigerde. Een weigering
     200// (401/403/404/410 -- niet gedeeld, of weg) is iets anders dan een storing, en
     201// alleen de server weet welke van de twee het was.
     202test('een weigering van de bron is 404, een storing is 502', async (t) => {
     203  const crypto = await import('crypto');
     204  const express = (await import('express')).default;
     205  const routes = (await import('../src/routes/activitypub.js')).default;
     206
     207  const bearer = 'test-token-' + 'd'.repeat(24);
     208  db.prepare('INSERT INTO oauth_tokens (token_hash, client_id, user_id, site_slug, scope) VALUES (?,?,?,?,?)')
     209    .run(crypto.createHash('sha256').update(bearer).digest('base64url'), 'c', 'u1', 'kind', 'read write');
     210
     211  const app = express();
     212  app.use(routes);
     213  const server = app.listen(0);
     214  t.after(() => server.close());
     215  await new Promise((r) => server.once('listening', r));
     216
     217  const vorige = globalThis.fetch;
     218  t.after(() => { globalThis.fetch = vorige; });
     219  const vraag = async (uri) => {
     220    const r = await echteFetch(
     221      `http://127.0.0.1:${server.address().port}/ap/users/kind/thread?object=${encodeURIComponent(uri)}`,
     222      { headers: { Authorization: `Bearer ${bearer}` } });
     223    return { status: r.status, body: await r.json() };
     224  };
     225
     226  // De bron zegt nee -- precies wat boiert.eu doet met een friends-only post
     227  // voor iemand die de auteur niet (meer) volgt.
     228  const DICHT = 'https://203.0.113.50/notes/dicht';
     229  globalThis.fetch = async () => new Response('nope', { status: 404 });
     230  const nee = await vraag(DICHT);
     231  assert.equal(nee.status, 404);
     232  assert.equal(nee.body.error, 'not shared by source');
     233  assert.equal(nee.body.sourceStatus, 404);
     234
     235  // De bron ligt eruit. Dezelfde lege uitkomst, een andere waarheid.
     236  const STUK = 'https://203.0.113.51/notes/stuk';
     237  globalThis.fetch = async () => new Response('boem', { status: 500 });
     238  const stuk = await vraag(STUK);
     239  assert.equal(stuk.status, 502);
     240  assert.equal(stuk.body.error, 'source unreachable');
     241});
Note: See TracChangeset for help on using the changeset viewer.