OverviewFilesCommitsIssuesDocsReleasesPeers

favela-node commits / 5a9a103

favela-node 0.1.2: phase 2 — community DID declares feed-gen + labeler at genesis
communityIdentity / createCommunity thread feedGenEndpoint / labelerEndpoint (default to the membrane, pdsUrl) into the genesis op, so a community's did:plc declares its #bsky_fg + #atproto_labeler services + #atproto_label VM at creation (spec §5). #atproto_label defaults to the shared #atproto key (labels co-signed by it). Test: the community genesis op carries all three service entries. Offline — no PLC submit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5a9a10388e69a994ba421af627d6407962698307
by plankatron <fleeky@gmail.com> · 2026-07-15T14:37:32.000Z

Changed files (3) +22 -11

lib/community.js +10 -5
……@@ -33,9 +33,12 @@ function roomSecrets (did, signingKey) {
3333const hex = (k) => (typeof k === 'string' ? k : b4a.toString(k, 'hex'))
3434
3535// The community's atproto identity from its ROOT mnemonic (the founder holds it; the recovery key stays
36// root-side — devices never receive it). Returns { did, signedOp, signingKey, recoveryKey, didKey }.
37export function communityIdentity (rootMnemonic, { handle, pdsUrl } = {}) {
38 return createSwarmIdentity(rootMnemonic, { handle, pdsUrl })
36// root-side — devices never receive it). feedGenEndpoint/labelerEndpoint declare the community DID as a
37// self-custody feed-generator + labeler in its genesis op (#bsky_fg / #atproto_labeler services +
38// #atproto_label VM — spec/lens-export.md §5), pointing at the membrane that proxies the XRPC. Returns
39// { did, signedOp, signingKey, recoveryKey, didKey }.
40export function communityIdentity (rootMnemonic, { handle, pdsUrl, feedGenEndpoint = null, labelerEndpoint = null } = {}) {
41 return createSwarmIdentity(rootMnemonic, { handle, pdsUrl, feedGenEndpoint, labelerEndpoint })
3942}
4043
4144// ROOT-side: attest an admin device into the community (a keet root→device chain). The founder (root
……@@ -77,9 +80,11 @@ export async function openCommunityRepo ({ storage, did, signingKey, rootPub, de
7780// adminDeviceKeys: pre-known co-admin device pubkeys (hex/Buffer) to seed into the roster now (attest +
7881// hand them their bundles separately via attestAdmin). Returns everything a caller needs to add records +
7982// admins. roomStore/repoStorage are separate (the room is plaintext/keyless; the repo is encrypted).
80export async function createCommunity ({ rootMnemonic, founderDeviceMnemonic, adminDeviceKeys = [], name, roomStore, repoStorage, handle, pdsUrl = 'https://pds.7rnx.net', swarmOpts = null, network = true } = {}) {
83export async function createCommunity ({ rootMnemonic, founderDeviceMnemonic, adminDeviceKeys = [], name, roomStore, repoStorage, handle, pdsUrl = 'https://pds.7rnx.net', feedGenEndpoint = null, labelerEndpoint = null, swarmOpts = null, network = true } = {}) {
8184 if (!rootMnemonic || !founderDeviceMnemonic || !roomStore || !repoStorage) throw new Error('createCommunity: rootMnemonic, founderDeviceMnemonic, roomStore, repoStorage required')
82 const identity = communityIdentity(rootMnemonic, { handle, pdsUrl })
85 // Default the lens-export endpoints to the same membrane that fronts the PDS (it proxies the feed/label
86 // XRPC to favela-web — spec §4). A community DID thus declares its feed-generator + labeler at genesis.
87 const identity = communityIdentity(rootMnemonic, { handle, pdsUrl, feedGenEndpoint: feedGenEndpoint || pdsUrl, labelerEndpoint: labelerEndpoint || pdsUrl })
8388 const signingKey = b4a.toString(identity.signingKey.privateKey, 'hex')
8489 const rootPub = b4a.toString(await rootIdentityPublicKey(rootMnemonic), 'hex')
8590 const founderKp = await deviceKeetKeyPair(founderDeviceMnemonic)
package.json +6 -6
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-node",
3 "version": "0.1.1",
3 "version": "0.1.2",
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",
……@@ -10,13 +10,13 @@
1010 "dependencies": {
1111 "atproto-identity": "file:../atproto-identity",
1212 "atproto-room": "file:../atproto-room",
13 "favela-community": "file:../favela-community",
14 "corestore": "^7.4.0",
15 "hyperswarm": "^4.11.0",
16 "hypercore-crypto": "^3.4.0",
13 "b4a": "^1.8.1",
1714 "bare-path": "^3.0.0",
1815 "bare-process": "^4.0.0",
19 "b4a": "^1.8.1"
16 "corestore": "^7.4.0",
17 "favela-community": "file:../favela-community",
18 "hypercore-crypto": "^3.4.0",
19 "hyperswarm": "^4.11.0"
2020 },
2121 "devDependencies": {
2222 "@hyperswarm/testnet": "^3.1.4"
test/publish.test.mjs +6 -0
……@@ -20,6 +20,12 @@ const c = await createCommunity({
2020})
2121assert(/^did:plc:/.test(c.did), 'community minted a did:plc (single-node, no network)')
2222
23// ── phase 2: the community DID declares its feed-generator + labeler services at GENESIS ──
24const op = c.identity.signedOp
25assert(op.services.bsky_fg && op.services.bsky_fg.type === 'BskyFeedGenerator', 'genesis declares #bsky_fg (feed generator) → the membrane')
26assert(op.services.atproto_labeler && op.services.atproto_labeler.type === 'AtprotoLabeler', 'genesis declares #atproto_labeler → the membrane')
27assert(op.verificationMethods.atproto_label === op.verificationMethods.atproto, '#atproto_label VM = the shared #atproto signing key (labels co-signed by it)')
28
2329// ── app.bsky.feed.generator ──
2430const gen = await publishFeedGenerator(c.repo, c.did, { rkey: 'catlovers', displayName: 'Cat Lovers', description: 'meow', createdAt: CTS })
2531assert(gen.did === c.did, 'feed.generator.did = the community DID (self-custody: the DID is its own service)')