| … | … | @@ -6,4 +6,4 @@ |
| 6 | 6 | // app.bsky.feed.generator + app.bsky.labeler.service records and its com.atproto.label labels. |
| 7 | 7 | // 2. the keyless CommunityRoom roster (favela-community) — the MEMBER audience favela-web reads. |
| 8 | 8 | // The room's MODS == the DID's admins (one attested group). This module composes them. |
| 9 | export { communityIdentity, attestAdmin, openCommunityRepo, createCommunity, bootCommunityAdmin } from './lib/community.js' | |
| 9 | export { communityIdentity, attestAdmin, openCommunityRepo, createCommunity, bootCommunityAdmin, publishFeedGenerator, publishLabelerService } from './lib/community.js' |
| … | … | @@ -99,3 +99,30 @@ export async function createCommunity ({ rootMnemonic, founderDeviceMnemonic, ad |
| 99 | 99 | export async function bootCommunityAdmin ({ storage, did, signingKey, rootPub, deviceMnemonic, deviceProof, members, swarmOpts = null, network = true } = {}) { |
| 100 | 100 | return openCommunityRepo({ storage, did, signingKey, rootPub, deviceMnemonic, deviceProof, members, swarmOpts, network }) |
| 101 | 101 | } |
| 102 | ||
| 103 | // ── lens-export records (spec/lens-export.md §6.2) — written into the community DID's CO-OWNED repo, | |
| 104 | // so they are co-signed by the lease-holding admin under the shared #atproto key. Any admitted admin's | |
| 105 | // repo may write; the linearized view converges. rkeys are caller-chosen (the feedgen rkey = the feed's | |
| 106 | // short name; the labeler is the reserved `self`). createdAt is caller-supplied for deterministic tests. | |
| 107 | ||
| 108 | // app.bsky.feed.generator — declares a custom feed owned by this community DID. `did` = the SERVICE DID | |
| 109 | // that answers getFeedSkeleton; in the self-custody model the community DID IS its own service (its PLC | |
| 110 | // doc carries #bsky_fg → the favela-web endpoint), so did = communityDid. Returns the record. | |
| 111 | export async function publishFeedGenerator (repo, communityDid, { rkey, displayName, description = '', avatar = null, createdAt = new Date().toISOString() } = {}) { | |
| 112 | if (!repo || !communityDid || !rkey || !displayName) throw new Error('publishFeedGenerator: repo, communityDid, rkey, displayName required') | |
| 113 | const record = { $type: 'app.bsky.feed.generator', did: communityDid, displayName, createdAt } | |
| 114 | if (description) record.description = description | |
| 115 | if (avatar) record.avatar = avatar | |
| 116 | await repo.putRecord('app.bsky.feed.generator', String(rkey), record) | |
| 117 | return record | |
| 118 | } | |
| 119 | ||
| 120 | // app.bsky.labeler.service (rkey 'self') — declares this community DID as a labeler + the label values it | |
| 121 | // emits. Consumers verify labels against the DID's #atproto_label key (its PLC doc). Returns the record. | |
| 122 | export async function publishLabelerService (repo, { labelValues = [], labelValueDefinitions = [], createdAt = new Date().toISOString() } = {}) { | |
| 123 | if (!repo) throw new Error('publishLabelerService: repo required') | |
| 124 | const record = { $type: 'app.bsky.labeler.service', policies: { labelValues }, createdAt } | |
| 125 | if (labelValueDefinitions.length) record.policies.labelValueDefinitions = labelValueDefinitions | |
| 126 | await repo.putRecord('app.bsky.labeler.service', 'self', record) | |
| 127 | return record | |
| 128 | } |
| … | … | @@ -1,11 +1,11 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "favela-node", |
| 3 | "version": "0.1.0", | |
| 3 | "version": "0.1.1", | |
| 4 | 4 | "description": "favela-node — the full-node that composes a community's CO-OWNED atproto DID (hyper-proto-peer / atproto-room: StateBase admins + Lease + Signer/LabelRoom = the multiswarm-PDS pattern) with the keyless CommunityRoom roster (favela-community) and, later, the favela-web lens-export gateway. See favela-web/spec/lens-export.md.", |
| 5 | 5 | "type": "module", |
| 6 | 6 | "main": "index.js", |
| 7 | 7 | "scripts": { |
| 8 | "test": "bare test/community.test.mjs" | |
| 8 | "test": "bare test/publish.test.mjs && bare test/community.test.mjs" | |
| 9 | 9 | }, |
| 10 | 10 | "dependencies": { |
| 11 | 11 | "atproto-identity": "file:../atproto-identity", |
| … | … | @@ -0,0 +1,44 @@ |
| 1 | // favela-node phase 3 — publish the lens-export records into the community DID's co-owned repo: | |
| 2 | // app.bsky.feed.generator (did = the community DID = its own service) + app.bsky.labeler.service. | |
| 3 | // Single-node (co-signing across admins is proven in community.test.mjs); here we assert the records are | |
| 4 | // written + read back with the correct atproto shape. Run: bare test/publish.test.mjs | |
| 5 | import '../lib/bare-globals.js' | |
| 6 | import Corestore from 'corestore' | |
| 7 | import { createCommunity, publishFeedGenerator, publishLabelerService } from '../index.js' | |
| 8 | ||
| 9 | const assert = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log(' ok -', m) } | |
| 10 | const stamp = Date.now() | |
| 11 | const tmp = (s) => `data/fn-pub-${s}-${stamp}` | |
| 12 | const ROOT = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about' | |
| 13 | const FOUNDER = 'legal winner thank year wave sausage worth useful legal winner thank yellow' | |
| 14 | const CTS = '2026-07-15T00:00:00.000Z' | |
| 15 | ||
| 16 | const roomStore = new Corestore(tmp('room')) | |
| 17 | const c = await createCommunity({ | |
| 18 | rootMnemonic: ROOT, founderDeviceMnemonic: FOUNDER, name: 'Cat Lovers', | |
| 19 | roomStore, repoStorage: tmp('repo'), handle: 'catlovers.test', network: false | |
| 20 | }) | |
| 21 | assert(/^did:plc:/.test(c.did), 'community minted a did:plc (single-node, no network)') | |
| 22 | ||
| 23 | // ── app.bsky.feed.generator ── | |
| 24 | const gen = await publishFeedGenerator(c.repo, c.did, { rkey: 'catlovers', displayName: 'Cat Lovers', description: 'meow', createdAt: CTS }) | |
| 25 | assert(gen.did === c.did, 'feed.generator.did = the community DID (self-custody: the DID is its own service)') | |
| 26 | const genBack = await c.repo.getRecord('app.bsky.feed.generator', 'catlovers') | |
| 27 | assert(genBack && genBack.$type === 'app.bsky.feed.generator' && genBack.did === c.did, 'feed.generator read back with $type + did') | |
| 28 | assert(genBack.displayName === 'Cat Lovers' && genBack.description === 'meow' && genBack.createdAt === CTS, 'feed.generator carries displayName/description/createdAt') | |
| 29 | ||
| 30 | // ── app.bsky.labeler.service (rkey self) ── | |
| 31 | const lab = await publishLabelerService(c.repo, { labelValues: ['spam', '!hide'], createdAt: CTS }) | |
| 32 | assert(lab.$type === 'app.bsky.labeler.service', 'labeler.service $type') | |
| 33 | const labBack = await c.repo.getRecord('app.bsky.labeler.service', 'self') | |
| 34 | assert(labBack && labBack.$type === 'app.bsky.labeler.service' && Array.isArray(labBack.policies.labelValues), 'labeler.service read back (rkey self) with policies.labelValues') | |
| 35 | assert(labBack.policies.labelValues.includes('spam') && labBack.policies.labelValues.includes('!hide'), 'labeler.service declares its label values') | |
| 36 | ||
| 37 | // ── both records coexist in the repo ── | |
| 38 | const all = await c.repo.listRecords() | |
| 39 | const has = (coll, rk) => all.some((r) => r.collection === coll && r.rkey === rk) | |
| 40 | assert(has('app.bsky.feed.generator', 'catlovers') && has('app.bsky.labeler.service', 'self'), 'both records present in the community repo (listRecords)') | |
| 41 | ||
| 42 | console.log('\nFAVELA-NODE PUBLISH OK: feed.generator + labeler.service written into the co-owned community repo (correct atproto shape)') | |
| 43 | await c.sb.close(); await c.room.close() | |
| 44 | try { await roomStore.close() } catch {} |