// Which anonymous-read grants are FESTIPOD's — the record that lets public-read revoke only what it // added. // // WHY. A pod resource can be world-readable for reasons that are not Festipod's: its owner shared it // publicly, another app did, or the pod made it so. When an event stops being public, Festipod must // take back the public read IT gave and leave any other one alone. The ACL cannot tell them apart (the // rule is the same `acl:Read` for `foaf:Agent`), so Festipod records each grant it makes, here, and // `revoke` acts only on a recorded one. // // WHAT COUNTS AS FESTIPOD'S: // - a grant `public-read.grant` added (the resource was not already world-readable); // - the public read the pod gives a collection Festipod itself created (an event's attendees // collection: the pod makes it world-readable at creation — observed 2026-10-12/13). // A resource already world-readable before Festipod touched it is never recorded, so never revoked. // // WHERE IT LIVES. Like the catalog (`catalog-store.js`): in the backend's main dataset, DEFAULT graph // (the one Fuseki persists), one triple per resource — subject the resource URI, one Festipod predicate, // the time of the grant. It survives a backend restart. Grants made before this record existed // (development data) are not in it, so they are not revoked. const { iriRef } = require('./catalog-store'); const GRANTED_AT = 'http://festipod.org/publicReadGrantedAt'; /** Record (or re-record) that Festipod made `uri` world-readable. One update = one transaction. */ function recordGrantUpdate(uri) { const s = iriRef(uri); return ` DELETE { ${s} <${GRANTED_AT}> ?old } INSERT { ${s} <${GRANTED_AT}> ?now } WHERE { OPTIONAL { ${s} <${GRANTED_AT}> ?old } BIND(NOW() AS ?now) } `; } /** Forget Festipod's grant on `uri` — only the ledger's predicate, nothing else about that URI. */ function forgetGrantUpdate(uri) { return `DELETE WHERE { ${iriRef(uri)} <${GRANTED_AT}> ?at }`; } /** Whether Festipod's grant on `uri` is recorded. */ function grantedQuery(uri) { return `ASK { ${iriRef(uri)} <${GRANTED_AT}> ?at }`; } // ── Store operations: `ctx` is a moleculer context (anything with `call`) ── async function recordGrant(ctx, { dataset, uri }) { await ctx.call('triplestore.update', { query: recordGrantUpdate(uri), dataset, webId: 'system' }); } async function forgetGrant(ctx, { dataset, uri }) { await ctx.call('triplestore.update', { query: forgetGrantUpdate(uri), dataset, webId: 'system' }); } /** Resolves true only when the store answers the ASK with `true`. */ async function isGrantedByFestipod(ctx, { dataset, uri }) { return (await ctx.call('triplestore.query', { query: grantedQuery(uri), dataset, webId: 'system' })) === true; } module.exports = { GRANTED_AT, recordGrantUpdate, forgetGrantUpdate, grantedQuery, recordGrant, forgetGrant, isGrantedByFestipod };