OverviewFilesCommitsIssuesDocsReleasesPeers

favela-markdown / lib / reach.js

89 lines · javascript ref:
// maxReach — the lattice. library.md §6.1, record-vocabulary.md §5.2, interaction.md §5.2
//
// ⛔ THIS IS THE ONE PLACE A DEFECT IS A PRIVACY BUG rather than a rendering bug (library §6.1).
// Written here once, this check would otherwise be written three times — favela-web,
// hyper-proto-peer, ap-bridge — and the third one would eventually be written wrong.
//
// Everything is deny-by-default. Unknown room type, unknown verb, missing argument, hostile input:
// the answer is {favela}, which is not a restriction but a fact — "every post is local; the
// canonical record always exists on the P2P side; that is what authoring one means"
// (record-vocabulary §5.4). Local is the substrate, not a scope you opt into.
//
// SIGNATURE CHANGED 2026-09-05. library.md §6.1 signs this `maxReach(visibility)`, from when
// visibility was a frontmatter field. Visibility is now a PLACE — the room you wrote in
// (SOCIAL-MODEL §4) — so the inputs are the room's kind and the record's verb. This library has no
// storage and cannot inspect a room, so the CALLER supplies the kind. That is honest about the
// library boundary (§1: no storage, no network) rather than pretending to knowledge it lacks.

export const NETWORKS = ['favela', 'bsky', 'fediverse']

// What each kind of room can be expressed as, on each network.
//
//   public    → everywhere. AS2 Public; an atproto repo is world-readable by construction.
//   followers → the fediverse ONLY. AP has addressing, so a followers room maps onto the followers
//               collection. ⛔ atproto has NO addressing at all (record-vocabulary §5.2), so a
//               projection it cannot restrict is a leak, and the only safe answer is to withhold.
//   members   → favela only. AS2 *can* address actors individually, but an arbitrary room's
//   dm          membership is not expressible as an audience a receiving server will honour, and
//               approximating it is how a private post reaches a stranger. Fails closed.
const ROOM_REACH = {
  public: ['favela', 'bsky', 'fediverse'],
  followers: ['favela', 'fediverse'],
  members: ['favela'],
  dm: ['favela']
}

// What each verb can be expressed as. interaction.md §5.4 (the signed set) and §5.5 (native).
// ⛔ A verb absent from this table reaches {favela} and nothing else — that IS the extension
// mechanism (interaction §5.2): a native-only verb cannot fork a signed projection, because it
// never reaches a shared derivation. Adding a verb here is a spec change, not a plugin point.
const VERB_REACH = {
  post: ['favela', 'bsky', 'fediverse'],
  reply: ['favela', 'bsky', 'fediverse'],
  like: ['favela', 'bsky', 'fediverse'],
  repost: ['favela', 'bsky', 'fediverse'],
  follow: ['favela', 'bsky', 'fediverse'],
  // AS2 `Block` exists but Mastodon does not federate it, so it projects with a loss entry rather
  // than being withheld — the receiving server simply does nothing with it.
  block: ['favela', 'bsky', 'fediverse'],
  // AS2 `Flag` is a REPORT to the origin's moderators, a different act from a shared signal, so
  // conflating them would misrepresent the moderator (SOCIAL-MODEL §6.7). atproto has a real
  // equivalent — a signed com.atproto.label, which we already emit.
  flag: ['favela', 'bsky'],
  // Internal: how a node knows two names, or two byte-sets, denote one thing. No projection
  // anywhere, by design (interaction §5.5, record-vocabulary §3.5).
  alias: ['favela'],
  'variant-of': ['favela']
}

const LOCAL = () => new Set(['favela'])

/**
 * maxReach({ roomType, verb }) -> Set<network>
 *
 * The MEET of what the room can express and what the verb can express. TOTAL: never throws, and
 * always contains 'favela'.
 */
export function maxReach (opts) {
  const roomType = opts && typeof opts.roomType === 'string' ? opts.roomType : null
  const verb = opts && typeof opts.verb === 'string' ? opts.verb : null

  // Exact match only. 'PUBLIC' and 'public ' are NOT 'public': normalising input here would mean a
  // typo could widen an audience, which is precisely the class of bug §6.1 exists to prevent.
  const room = (roomType && Object.prototype.hasOwnProperty.call(ROOM_REACH, roomType))
    ? ROOM_REACH[roomType]
    : null
  const verbs = (verb && Object.prototype.hasOwnProperty.call(VERB_REACH, verb))
    ? VERB_REACH[verb]
    : null

  if (!room || !verbs) return LOCAL()

  const out = new Set()
  for (const n of room) if (verbs.includes(n)) out.add(n)
  out.add('favela') // §5.4 — local is the substrate; there is no way to opt out of it
  return out
}

export default { maxReach, NETWORKS }