OverviewFilesCommitsIssuesDocsReleasesPeers

favela-community / lib / feed-schema.js

65 lines · javascript ref:
// The FEED REGISTRY protocol — the op contract for a watched list of feeds, as a hyper-rooms schema. This
// is the single source of truth that makes "a feed exists" one durable P2P fact everyone reacts to
// (favela-web/spec/community-ux.md): the mint writes an entry; favela-web (index), the membrane (front +
// requestCrawl), the multi-DID hpp (serve co-owned ones), and the blind-relay (seed) all keyless-READ +
// WATCH it — no per-feed config, no restart. Plaintext by design (feed identities are PUBLIC — a did + rkey
// + curation contract; never a secret).
//
// A feed is the PRIMITIVE (a community is just the roster-curated, co-owned kind): { curator, params } + a
// HOME repo (`did`) that holds the app.bsky.feed.generator record. For a light/arbitrary feed `did` is the
// creator's OWN hpp repo (no new DID); for a community it's the co-owned DID. `roomKey` is present only for
// roster-type feeds (the audience CommunityRoom favela-web reads).
//
// Domain ops (writer ops add-writer/remove-writer are handled by hyper-rooms Room itself):
//   add-feed {did,rkey,name,curator,params,roomKey?,handle?} → feed:<did>/<rkey>
//   remove-feed {did,rkey}                                   → del feed:<did>/<rkey>
//
// apply is DETERMINISTIC — reads only fields carried in the op (op.at/op.by set at append time), never
// Date.now()/randomness (Autobase replays apply on every boot + reorg).
import { jsonSchema } from 'hyper-rooms/lib/json-schema.js'

export const isDid = (d) => typeof d === 'string' && /^did:[a-z]+:/.test(d)
// rkey syntax = [A-Za-z0-9._~-], 1–512, not "."/".."; keep it simple + safe for the `feed:<did>/<rkey>` key.
export const isRkey = (r) => typeof r === 'string' && r.length > 0 && r.length <= 512 && /^[A-Za-z0-9._~-]+$/.test(r) && r !== '.' && r !== '..'
const isPlainObject = (o) => o != null && typeof o === 'object' && !Array.isArray(o)

export const FEED_PREFIX = 'feed:'
export const feedKey = (did, rkey) => `${FEED_PREFIX}${did}/${rkey}`

export const feedRegistrySchema = jsonSchema({
  validate (op) {
    if (op.type === 'add-feed') {
      if (!isDid(op.did)) throw new Error('add-feed requires a valid did (the feed record home repo)')
      if (!isRkey(op.rkey)) throw new Error('add-feed requires a valid rkey')
      if (typeof op.curator !== 'string' || !op.curator) throw new Error('add-feed requires a curator name')
      if (op.params != null && !isPlainObject(op.params)) throw new Error('add-feed params must be an object')
      if (op.roomKey != null && !/^[0-9a-f]{64}$/i.test(String(op.roomKey))) throw new Error('add-feed roomKey must be a 32-byte hex key')
    }
    if (op.type === 'remove-feed') {
      if (!isDid(op.did) || !isRkey(op.rkey)) throw new Error('remove-feed requires a valid did + rkey')
    }
  },
  async apply (batch, op, host) {
    switch (op.type) {
      case 'add-feed':
        await batch.put(feedKey(op.did, op.rkey), {
          did: op.did,
          rkey: op.rkey,
          name: op.name || op.rkey,
          curator: op.curator,
          params: op.params || {},
          roomKey: op.roomKey ? String(op.roomKey).toLowerCase() : null,
          handle: op.handle || null,
          createdBy: op.by || null,
          at: op.at || null
        })
        break
      case 'remove-feed':
        await batch.del(feedKey(op.did, op.rkey))
        break
    }
  }
})

export default feedRegistrySchema