// Festipod's PUBLIC intent, as the backend reads it on a created resource. // // The signal is ActivityStreams public addressing: the ActivityStreams Public collection in // `to` or `cc`. Festipod's create path writes it on every event and meeting point it creates // (`src/shared/refine/providers/activitypods.ts`, the `toValues` of those two bindings), and // the pod stores it on the object as written (observed 2026-10-12: stored as `"to": "as:Public"`; // the outbox `Create` the pod generates for a container POST carries no addressing at all). // // What it is NOT: provenance. The decision does not depend on which client created the // resource, only on whether the resource says it is public. An as:Event without public // addressing — created by another app, posted directly on the pod, addressed to chosen // recipients only, or a future Festipod record in a protected or private scope — never matches. const AS_PUBLIC = 'https://www.w3.org/ns/activitystreams#Public'; // The three equivalent spellings of the Public collection: the full IRI and the two compacted // forms JSON-LD compaction against the ActivityStreams context can produce. const PUBLIC_FORMS = new Set([AS_PUBLIC, 'as:Public', 'Public']); // `to` and `cc`, under the compacted names and their prefixed and full-IRI spellings (a // representation compacted against a context that does not define the short terms). const ADDRESSING_KEYS = [ 'to', 'cc', 'as:to', 'as:cc', 'https://www.w3.org/ns/activitystreams#to', 'https://www.w3.org/ns/activitystreams#cc' ]; const listOf = value => (value === undefined || value === null ? [] : Array.isArray(value) ? value : [value]); /** The address a `to`/`cc` entry names: a bare IRI string, or a node carrying `id`/`@id`. */ const addressOf = entry => { if (typeof entry === 'string') return entry; if (entry && typeof entry === 'object') { const id = entry.id || entry['@id']; return typeof id === 'string' ? id : undefined; } return undefined; }; /** * True when this ONE JSON-LD resource (an object or an activity) is addressed to the * ActivityStreams Public collection in `to` or `cc`. Anything else is false: no addressing, * addressing to actors or collections only, Public in `bto`/`bcc`/`audience`, a non-object input. */ const isAddressedToPublic = resource => { if (!resource || typeof resource !== 'object') return false; return ADDRESSING_KEYS.some(key => listOf(resource[key]).some(entry => PUBLIC_FORMS.has(addressOf(entry)))); }; /** * hasPublicIntent(object, activity?) → boolean. * * `object` is the created resource as stored on the pod (what the app's activity watcher hands * to `onCreate`: the dereferenced `object` of the `Create`). `activity` is optional: the `Create` * itself, when the caller holds it. True when either one is addressed to the Public collection * (see `isAddressedToPublic`); false when neither is — a missing argument counts as not addressed. * * This is the ONLY gate in front of making a resource world-readable or listing it publicly: * a caller that grants or publishes on a creation must ask this first. */ const hasPublicIntent = (object, activity) => isAddressedToPublic(object) || isAddressedToPublic(activity); // ── LISTED public: what a public index may list ─────────────────────────────────────────────── // // ActivityStreams addressing distinguishes two public audiences by WHERE the Public collection is // named, the convention fediverse servers follow: in `to`, the resource is public AND listed — meant // to show up in public timelines and indexes; in `cc` only, it is public but UNLISTED — anyone who // has its link may read it, but nothing should advertise it. The public-read grant serves both // (`hasPublicIntent`); a public index — Festipod's catalog — may list only the first. // `to` only, under the same three spellings as above. const TO_KEYS = ['to', 'as:to', 'https://www.w3.org/ns/activitystreams#to']; /** * True when this ONE JSON-LD resource names the ActivityStreams Public collection in `to` (any of * its spellings, as a bare IRI or a node). Public in `cc` only — unlisted — is false, as is * everything `isAddressedToPublic` refuses. */ const isListedToPublic = resource => { if (!resource || typeof resource !== 'object') return false; return TO_KEYS.some(key => listOf(resource[key]).some(entry => PUBLIC_FORMS.has(addressOf(entry)))); }; /** * hasListedPublicIntent(object, activity?) → boolean. * * Public intent (`hasPublicIntent`, same arguments) AND listed: the Public collection in `to` of * the object or of the activity. Built on `hasPublicIntent`, so it can never be true where that one * is false: nothing is listed that the public-read gate would not have accepted. */ const hasListedPublicIntent = (object, activity) => hasPublicIntent(object, activity) && (isListedToPublic(object) || isListedToPublic(activity)); module.exports = { AS_PUBLIC, isAddressedToPublic, hasPublicIntent, isListedToPublic, hasListedPublicIntent };