OverviewFilesCommitsIssuesDocsReleasesPeers

favela-node commits / 897c486

publish: feed.generator did = did:web SERVICE, group is the owner
app.bsky.feed.generator.did is the getFeedSkeleton SERVICE (the did:web engine, like graze.social), NOT the record owner. Pass `service` (favela-web's did:web); the group DID stays the OWNER + displayName + creator, the did:web is the invisible engine. No service → did = communityDid (legacy self-service fallback, back-compat). Publish test green: service → record.did = did:web; no service → record.did = communityDid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
897c48687126cd83b572f74bb1355450674f7e44
by plankatron <fleeky@gmail.com> · 2026-07-16T02:01:19.000Z

Changed files (2) +17 -9

lib/community.js +9 -5
……@@ -132,12 +132,16 @@ export async function recoverCommunityRepo ({ rootMnemonic, founderDeviceMnemoni
132132// repo may write; the linearized view converges. rkeys are caller-chosen (the feedgen rkey = the feed's
133133// short name; the labeler is the reserved `self`). createdAt is caller-supplied for deterministic tests.
134134
135// app.bsky.feed.generator — declares a custom feed owned by this community DID. `did` = the SERVICE DID
136// that answers getFeedSkeleton; in the self-custody model the community DID IS its own service (its PLC
137// doc carries #bsky_fg → the favela-web endpoint), so did = communityDid. Returns the record.
138export async function publishFeedGenerator (repo, communityDid, { rkey, displayName, description = '', avatar = null, createdAt = new Date().toISOString() } = {}) {
135// app.bsky.feed.generator — the feed record, OWNED by this community/group (it lives in this repo, so its
136// creator = the group account). The record's `did` field is the getFeedSkeleton SERVICE, which — like every
137// real feed (Discover→did:web:discover.bsky.app, Artists→did:web:api.graze.social) — is a shared `did:web`
138// (favela-web), NOT the group's own DID. Pass `service` (the did:web); the group stays the OWNER + name, the
139// did:web is the invisible compute backend. (Falls back to communityDid only for legacy callers.) The feed
140// resolves as "<displayName> by <group handle>", computed by the did:web. Returns the record.
141export async function publishFeedGenerator (repo, communityDid, { rkey, displayName, service = null, description = '', avatar = null, createdAt = new Date().toISOString() } = {}) {
139142 if (!repo || !communityDid || !rkey || !displayName) throw new Error('publishFeedGenerator: repo, communityDid, rkey, displayName required')
140 const record = { $type: 'app.bsky.feed.generator', did: communityDid, displayName, createdAt }
143 const serviceDid = service || communityDid // service SHOULD be a did:web (favela-web); communityDid is the legacy self-service fallback
144 const record = { $type: 'app.bsky.feed.generator', did: serviceDid, displayName, createdAt }
141145 if (description) record.description = description
142146 if (avatar) record.avatar = avatar
143147 await repo.putRecord('app.bsky.feed.generator', String(rkey), record)
test/publish.test.mjs +8 -4
……@@ -26,12 +26,16 @@ assert(op.services.bsky_fg && op.services.bsky_fg.type === 'BskyFeedGenerator',
2626assert(op.services.atproto_labeler && op.services.atproto_labeler.type === 'AtprotoLabeler', 'genesis declares #atproto_labeler → the membrane')
2727assert(op.verificationMethods.atproto_label === op.verificationMethods.atproto, '#atproto_label VM = the shared #atproto signing key (labels co-signed by it)')
2828
29// ── app.bsky.feed.generator ──
30const gen = await publishFeedGenerator(c.repo, c.did, { rkey: 'catlovers', displayName: 'Cat Lovers', description: 'meow', createdAt: CTS })
31assert(gen.did === c.did, 'feed.generator.did = the community DID (self-custody: the DID is its own service)')
29// ── app.bsky.feed.generator — the group OWNS the record; the record's `did` = a did:web SERVICE ──
30const SERVICE = 'did:web:favela.example.net'
31const gen = await publishFeedGenerator(c.repo, c.did, { rkey: 'catlovers', displayName: 'Cat Lovers', description: 'meow', service: SERVICE, createdAt: CTS })
32assert(gen.did === SERVICE, 'feed.generator.did = the did:web SERVICE (favela-web), NOT the group DID — group is the OWNER, did:web is the invisible engine (like graze.social)')
3233const genBack = await c.repo.getRecord('app.bsky.feed.generator', 'catlovers')
33assert(genBack && genBack.$type === 'app.bsky.feed.generator' && genBack.did === c.did, 'feed.generator read back with $type + did')
34assert(genBack && genBack.$type === 'app.bsky.feed.generator' && genBack.did === SERVICE, 'feed.generator read back: $type + did:web service')
3435assert(genBack.displayName === 'Cat Lovers' && genBack.description === 'meow' && genBack.createdAt === CTS, 'feed.generator carries displayName/description/createdAt')
36// the record lives in c.repo (= the group's repo) → the group is the CREATOR/owner; only the service is the did:web
37const legacy = await publishFeedGenerator(c.repo, c.did, { rkey: 'legacyfeed', displayName: 'Legacy', createdAt: CTS })
38assert(legacy.did === c.did, 'legacy fallback: no service → did = communityDid (back-compat for old callers)')
3539
3640// ── app.bsky.labeler.service (rkey self) ──
3741const lab = await publishLabelerService(c.repo, { labelValues: ['spam', '!hide'], createdAt: CTS })