OverviewFilesCommitsIssuesDocsReleasesPeers

favela-node commits / 39ed8b1

favela-node 0.1.1: phase 3 — publish feed.generator + labeler.service (co-signed)
publishFeedGenerator / publishLabelerService write the two lens-export records into the community DID's co-owned repo (spec/lens-export.md §6.2), so they are co-signed by the lease-holding admin under the shared #atproto key. feed.generator.did = the community DID (self-custody: the DID is its own service, #bsky_fg in its PLC doc); labeler.service (rkey self) declares policies.labelValues. Test: both records written + read back with correct atproto shape + coexist in the repo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
39ed8b101d9f401ed4dd9478541ea6b3c46835c5
by plankatron <fleeky@gmail.com> · 2026-07-15T14:13:26.000Z

Changed files (4) +74 -3

index.js +1 -1
……@@ -6,4 +6,4 @@
66// app.bsky.feed.generator + app.bsky.labeler.service records and its com.atproto.label labels.
77// 2. the keyless CommunityRoom roster (favela-community) — the MEMBER audience favela-web reads.
88// The room's MODS == the DID's admins (one attested group). This module composes them.
9export { communityIdentity, attestAdmin, openCommunityRepo, createCommunity, bootCommunityAdmin } from './lib/community.js'
9export { communityIdentity, attestAdmin, openCommunityRepo, createCommunity, bootCommunityAdmin, publishFeedGenerator, publishLabelerService } from './lib/community.js'
lib/community.js +27 -0
……@@ -99,3 +99,30 @@ export async function createCommunity ({ rootMnemonic, founderDeviceMnemonic, ad
9999export async function bootCommunityAdmin ({ storage, did, signingKey, rootPub, deviceMnemonic, deviceProof, members, swarmOpts = null, network = true } = {}) {
100100 return openCommunityRepo({ storage, did, signingKey, rootPub, deviceMnemonic, deviceProof, members, swarmOpts, network })
101101}
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.
111export 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.
122export 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}
package.json +2 -2
……@@ -1,11 +1,11 @@
11{
22 "name": "favela-node",
3 "version": "0.1.0",
3 "version": "0.1.1",
44 "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.",
55 "type": "module",
66 "main": "index.js",
77 "scripts": {
8 "test": "bare test/community.test.mjs"
8 "test": "bare test/publish.test.mjs && bare test/community.test.mjs"
99 },
1010 "dependencies": {
1111 "atproto-identity": "file:../atproto-identity",
test/publish.test.mjs +44 -0
……@@ -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
5import '../lib/bare-globals.js'
6import Corestore from 'corestore'
7import { createCommunity, publishFeedGenerator, publishLabelerService } from '../index.js'
8
9const assert = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log(' ok -', m) }
10const stamp = Date.now()
11const tmp = (s) => `data/fn-pub-${s}-${stamp}`
12const ROOT = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
13const FOUNDER = 'legal winner thank year wave sausage worth useful legal winner thank yellow'
14const CTS = '2026-07-15T00:00:00.000Z'
15
16const roomStore = new Corestore(tmp('room'))
17const c = await createCommunity({
18 rootMnemonic: ROOT, founderDeviceMnemonic: FOUNDER, name: 'Cat Lovers',
19 roomStore, repoStorage: tmp('repo'), handle: 'catlovers.test', network: false
20})
21assert(/^did:plc:/.test(c.did), 'community minted a did:plc (single-node, no network)')
22
23// ── app.bsky.feed.generator ──
24const gen = await publishFeedGenerator(c.repo, c.did, { rkey: 'catlovers', displayName: 'Cat Lovers', description: 'meow', createdAt: CTS })
25assert(gen.did === c.did, 'feed.generator.did = the community DID (self-custody: the DID is its own service)')
26const genBack = await c.repo.getRecord('app.bsky.feed.generator', 'catlovers')
27assert(genBack && genBack.$type === 'app.bsky.feed.generator' && genBack.did === c.did, 'feed.generator read back with $type + did')
28assert(genBack.displayName === 'Cat Lovers' && genBack.description === 'meow' && genBack.createdAt === CTS, 'feed.generator carries displayName/description/createdAt')
29
30// ── app.bsky.labeler.service (rkey self) ──
31const lab = await publishLabelerService(c.repo, { labelValues: ['spam', '!hide'], createdAt: CTS })
32assert(lab.$type === 'app.bsky.labeler.service', 'labeler.service $type')
33const labBack = await c.repo.getRecord('app.bsky.labeler.service', 'self')
34assert(labBack && labBack.$type === 'app.bsky.labeler.service' && Array.isArray(labBack.policies.labelValues), 'labeler.service read back (rkey self) with policies.labelValues')
35assert(labBack.policies.labelValues.includes('spam') && labBack.policies.labelValues.includes('!hide'), 'labeler.service declares its label values')
36
37// ── both records coexist in the repo ──
38const all = await c.repo.listRecords()
39const has = (coll, rk) => all.some((r) => r.collection === coll && r.rkey === rk)
40assert(has('app.bsky.feed.generator', 'catlovers') && has('app.bsky.labeler.service', 'self'), 'both records present in the community repo (listRecords)')
41
42console.log('\nFAVELA-NODE PUBLISH OK: feed.generator + labeler.service written into the co-owned community repo (correct atproto shape)')
43await c.sb.close(); await c.room.close()
44try { await roomStore.close() } catch {}