Changeset 1e172f3 in Klonkt for src/services/guardianship


Ignore:
Timestamp:
08/07/2026 12:00:22 PM (5 weeks ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
4c1e327
Parents:
ea138c0
git-author:
Robin <roboburr@…> (08/07/2026 12:00:20 PM)
git-committer:
roboburr <roboburr@…> (08/07/2026 12:00:22 PM)
Message:

Modules laden vanuit de shell in plaats van inline script (shaer-bqr, stap 1)

Het mechanisme uit optie C, met de bottom-tab als eerste geval zodat het ook
te bewijzen is.

WAAROM. De CSP-nonce rouleert per verzoek (shaer-0i6). Een script dat via htmx
binnenkomt draagt dus een nonce die het document niet kent en wordt geweigerd.
De chrome komt bij ELKE navigatie out-of-band opnieuw binnen, dus daar valt de
JS bij de eerste klik binnen de site al weg.

HOE. Een bootstrap in shell.ejs -- die komt alleen bij een volledige laadbeurt
binnen en heeft dus wel de goede nonce. Hij leest body[data-js], een lijst
modulenamen, en importeert ze uit /assets/js/mod/. Een dynamische import vanuit
een vertrouwd script is precies waar strict-dynamic voor bedoeld is, dus de
module zelf heeft geen nonce nodig.

Bij een htmx-navigatie zet de pcmsNav-trigger data-js opnieuw en haalt de
bootstrap op wat er nieuw bij staat. 'chrome' staat er altijd bij.

De naam wordt een PAD, dus hij moet door /[a-z0-9-]+$/ -- geen punt, geen
schuine streep.

EERSTE GEVAL: de zoekknop van de bottom-tab. Geen servergegevens erin, al
gedelegeerd, al voorzien van een slot -- dus de verhuizing verandert niets aan de
logica en het mechanisme is er echt mee te toetsen.

WAT DIT BLOOTLEGT VOOR DE VOLGENDE STAP: het topnav-script interpoleert
vertalingen (<%= t('search.section_posts') %>) en kan dus niet zomaar een
statisch bestand worden. Servergegevens horen via een data-attribuut naar een
module, niet via interpolatie in de code. Dat is een eigen stap en staat als
zodanig in mod/chrome.js opgeschreven.

Templates compileren, suite 551/551. Het echte bewijs is een klik BINNEN de site:
na een herlading werkt alles toch al.

Location:
src/services/guardianship
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • src/services/guardianship/follows.js

    rea138c0 r1e172f3  
    8181    _r = {
    8282      ins: db.prepare(`INSERT OR IGNORE INTO ap_follow_reviews
    83         (id, guardian_slug, ward_uri, ward_inbox, follower_uri, follower_handle, follower_icon, follow_json, created_at)
    84         VALUES (?,?,?,?,?,?,?,?, CURRENT_TIMESTAMP)`),
     83        (id, guardian_slug, ward_uri, ward_inbox, follower_uri, follower_handle, follower_icon, follow_json,
     84         direction, target_uri, target_handle, created_at)
     85        VALUES (?,?,?,?,?,?,?,?,?,?,?, CURRENT_TIMESTAMP)`),
    8586      get: db.prepare('SELECT * FROM ap_follow_reviews WHERE guardian_slug = ? AND id = ?'),
    8687      bySlug: db.prepare("SELECT * FROM ap_follow_reviews WHERE guardian_slug = ? AND status = 'pending' ORDER BY created_at DESC"),
     
    9192}
    9293
     94/**
     95 * De guardian-zijdige kopie van een gate-verzoek op een REMOTE ward.
     96 *
     97 * `direction` is niet cosmetisch (shaer-jdb). Bij een INKOMENDE is de follower
     98 * iemand anders en de ward het doel. Bij een UITGAANDE is de ward zelf de
     99 * follower en staat het doel in het Follow-object -- die werd hiervoor
     100 * opgeslagen als "deze ward wil deze ward volgen", met het doel weggegooid.
     101 */
    93102export function recordReview(guardianSlug, r) {
    94   rstmts().ins.run(r.id, guardianSlug, r.wardUri, r.wardInbox || null, r.follower, r.followerHandle || null, r.followerIcon || null, r.followJson || null);
     103  const richting = r.direction === 'outgoing' ? 'outgoing' : 'incoming';
     104  rstmts().ins.run(r.id, guardianSlug, r.wardUri, r.wardInbox || null, r.follower, r.followerHandle || null,
     105    r.followerIcon || null, r.followJson || null, richting, r.target || null, r.targetHandle || null);
    95106  return rstmts().get.get(guardianSlug, r.id);
     107}
     108
     109/**
     110 * Een openstaande review als wachtrij-item, in dezelfde vorm die de clients al
     111 * lezen (offers en outgoing-follows doen het net zo).
     112 */
     113export function reviewQueueItem(r, me, guardianCount) {
     114  // guardianCount blijft WEG als we hem niet kennen. Bij een remote ward wordt
     115  // de guardian-set op diens eigen server bijgehouden, en 0 sturen zou lezen als
     116  // "dit kind heeft geen guardians" -- het tegenovergestelde van onbekend.
     117  const stemmen = (() => {
     118    try { return db.prepare('SELECT guardian_uri, decision FROM ap_pending_follow_approvals WHERE follow_id = ?').all(r.id); }
     119    catch { return []; }
     120  })();
     121  const uitgaand = r.direction === 'outgoing';
     122  return {
     123    id: r.id,
     124    type: 'Follow',
     125    // Bij een uitgaande is de WARD de volger; bij een inkomende is dat de vreemde.
     126    actor: uitgaand ? r.ward_uri : r.follower_uri,
     127    object: uitgaand ? (r.target_uri || '') : r.ward_uri,
     128    'shaer:direction': uitgaand ? 'outgoing' : 'incoming',
     129    'shaer:ward': r.ward_uri,
     130    'shaer:target': uitgaand ? (r.target_uri || undefined) : undefined,
     131    'shaer:targetHandle': uitgaand ? (r.target_handle || undefined) : undefined,
     132    'shaer:follower': uitgaand ? undefined : r.follower_uri,
     133    'shaer:followerHandle': uitgaand ? undefined : (r.follower_handle || undefined),
     134    'shaer:quorum': 'all',
     135    'shaer:approvals': stemmen.filter((x) => x.decision === 'approve').length,
     136    'shaer:guardianCount': guardianCount || undefined,
     137    'shaer:myVote': stemmen.some((x) => x.guardian_uri === me),
     138    published: r.created_at,
     139  };
     140}
     141
     142/** De openstaande reviews van een guardian, per richting. */
     143export function listReviewsByDirection(guardianSlug, direction) {
     144  return listReviews(guardianSlug).filter((r) => (r.direction === 'outgoing' ? 'outgoing' : 'incoming') === direction);
    96145}
    97146export function getReview(guardianSlug, id) { return rstmts().get.get(guardianSlug, id); }
     
    102151  recordPending, getPending, listForWard, decide, remove,
    103152  recordReview, getReview, listReviews, removeReview,
     153  listReviewsByDirection, reviewQueueItem,
    104154};
  • src/services/guardianship/queues.js

    rea138c0 r1e172f3  
    66 *  - offers:  pending handshake offers where I am a party (§3), with the full
    77 *             accept tally so the client shows the right action
    8  *  - follows: pending gated follows for my wards (§5.3) — Fase 2, empty for now
     8 *  - follows: pending gated follows ON my wards (§5.3), Fase 2 (shaer-jdb)
    99 *  - wards:   my committed wards
    1010 */
     
    1313import * as availability from './availability.js';
    1414import * as outgoing from './outgoing.js';
     15import * as follows from './follows.js';
    1516import * as handshake from './handshake.js';
    1617
     
    3536}
    3637
    37 /** Gated follows awaiting guardian approval — not built in Klonkt yet (Fase 2). */
    38 export function followsCollection(id) {
    39   return collection(id, []);
     38/**
     39 * Gate-verzoeken OP mijn wards die op mijn antwoord wachten (Guardianship Fase 2,
     40 * shaer-jdb). Dit was een lege stub: de gating zelf werkt sinds shaer-hxg, maar
     41 * werd nooit aan een C2S-client doorgegeven omdat de koers toen op de PWA lag.
     42 *
     43 * Twee bronnen, want een guardian kan wards op andere servers hebben en (nog)
     44 * op deze:
     45 *   - ap_follow_reviews: de doorgestuurde kopie van een REMOTE ward
     46 *   - ap_pending_follows: een ward op deze instance
     47 * Zie shaer-h6u: die tweede hoort op termijn ook over de lijn te gaan.
     48 */
     49export function followsCollection(id, slug, me) {
     50  const items = follows.listReviewsByDirection(slug, 'incoming')
     51    .map((r) => follows.reviewQueueItem(r, me));
     52  for (const w of relations.listWards(slug)) {
     53    const wardSlug = slugOf(w.other_uri);
     54    if (!wardSlug) continue;
     55    for (const p of follows.listForWard(wardSlug)) {
     56      items.push({
     57        id: p.id, type: 'Follow', actor: p.follower_uri, object: w.other_uri,
     58        'shaer:direction': 'incoming', 'shaer:ward': w.other_uri,
     59        'shaer:follower': p.follower_uri, 'shaer:followerHandle': p.follower_handle || undefined,
     60        'shaer:quorum': p.quorum || 'any', published: p.created_at,
     61      });
     62    }
     63  }
     64  return collection(id, items);
    4065}
    4166
    42 /** §5.3 outbound: this ward's own follow requests, waiting for its guardians. */
     67/** De slug van een actor-uri op DEZE instance, of null als hij elders woont. */
     68function slugOf(uri) {
     69  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     70  if (!base || !String(uri || '').startsWith(`${base}/ap/users/`)) return null;
     71  return decodeURIComponent(String(uri).slice(`${base}/ap/users/`.length).split(/[/?#]/)[0]) || null;
     72}
     73
     74/**
     75 * §5.3 uitgaand. Twee lezers, een wachtrij, en dat kan omdat §1 een ward en een
     76 * guardian wederzijds uitsluit: je bent het een of het ander.
     77 *
     78 *   ALS WARD      wat IK wil volgen en waar mijn guardians nog over moeten
     79 *   ALS GUARDIAN  wat mijn WARDS willen volgen en waar IK over moet (shaer-jdb)
     80 *
     81 * Dat tweede ontbrak. De wachtrij serveerde alleen listForWard(slug), en voor
     82 * een guardian is dat per definitie leeg -- dus het scherm "Your wards want to
     83 * follow" kon nooit iets tonen.
     84 */
    4385export function outgoingFollowsCollection(id, slug, me) {
    44   return collection(id, outgoing.listForWard(slug).map((o) => outgoing.queueItem(o, me)));
     86  const items = outgoing.listForWard(slug).map((o) => outgoing.queueItem(o, me));
     87  for (const r of follows.listReviewsByDirection(slug, 'outgoing')) {
     88    items.push(follows.reviewQueueItem(r, me));
     89  }
     90  return collection(id, items);
    4591}
    4692
Note: See TracChangeset for help on using the changeset viewer.