OverviewFilesCommitsIssuesDocsReleasesPeers

favela-community commits / 14a804b

favela-community 0.1.0 — co-owned community rooms (read + write layer)
Extracted from favela-web (the community protocol is a real 2nd consumer now: the write layer) into a shared module — favela-web reads keylessly, a full-node / CLI / hpp writes. Plaintext hyper-rooms Autobase; membership + moderation. - lib/schema.js — communitySchema (hyper-rooms jsonSchema): add-member/ remove-member/ban/unban/set-meta → member:<did>/banned:<did>/meta:<key>. Deterministic apply. The community protocol contract. - lib/community-room.js — CommunityRoom, three roles in one class: - create() → OWNER: genesis writer + sole indexer (N=1, the safe topology). - open() → MOD: co-writer after the owner addMods its localKey; waitWritable(). - openReadonly() → keyless READER (favela-web): no keyPair, never a writer. Reads: members()/meta()/mods()/isBanned(). Writes: addMember/removeMember/ ban/unban/setMeta (throw if not writable). Admission: addMod (default role 'writer' = non-indexer, avoids the N=2 trap) + addIndexer (explicit, warns). CommunityRooms — keyless read-manager (the Communities.resolve source). - hyper-rooms imported by SUBPATH to skip the pairing/blind-pairing dep. Proof (test/community-room.test.mjs, bare): keyless read + write-refusal; a MOD admitted by the owner co-manages membership/moderation, and its ops linearize through the owner (sole indexer) to both owner and keyless reader. Fully offline. Deploy: vendored-only lib (file:../, like atproto-*); consumers must vendor BOTH favela-community and hyper-rooms. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
14a804b05972f15ea1fa38aec98d5c7f6e04594b
by plankatron <fleeky@gmail.com> · 2026-07-15T03:28:38.000Z

Changed files (8) +1275 -0

.gitignore +2 -0
……@@ -0,0 +1,2 @@
1data/
2node_modules/
README.md +66 -0
……@@ -0,0 +1,66 @@
1# favela-community
2
3Co-owned, **plaintext** community rooms for [Favela](../favela-web) — membership + moderation on a
4[hyper-rooms](../hyper-rooms) Autobase. One community protocol, shared across surfaces: **read keylessly**
5by the gateway (`favela-web`), **written** by mods (a full-node / CLI / hpp).
6
7## The model
8
9A community is a co-owned room. Its authoritative state — the member roster + moderation — linearizes into
10a plaintext Hyperbee view. **Posts are not here**; they live in each member's own signed atproto repo. The
11room holds only public metadata (member DIDs, bans, labels).
12
13Three roles, one `CommunityRoom` class:
14
15| Role | How | Can |
16|---|---|---|
17| **Owner** | `CommunityRoom.create()` | genesis writer + **sole indexer** (N=1); manage membership + admit mods |
18| **Mod** | `CommunityRoom.open()` then owner `addMod`s its `localKey` | co-write membership/moderation after `waitWritable()` |
19| **Reader** | `CommunityRoom.openReadonly()` | **keyless** — opens by the room's *public* bootstrapKey, holds no key, never a writer; derives the roster |
20
21### Why plaintext (not encrypted)
22
23Keyless-read and encryption are mutually exclusive: reading an encrypted Autobase needs the members-only
24secret, which would make the gateway a keyholder. The room holds only *public* metadata, so encryption buys
25nothing — and it lets `favela-web` stay keyless. Private/invite-only communities are a separate mode (secret
26topic + encryption; the reader must then hold the key). See `lib/schema.js`.
27
28### Indexer topology (the N=2 trap)
29
30The owner is the **sole indexer** (N=1) — the safe default. Mods are added as plain **writers**
31(`indexer:false`); their ops still linearize (the owner acks via `ackInterval`). Autobase majority is
32`floor(N/2)+1`, so **N=2 deadlocks** if either indexer drops. Only grow the indexer set to an **odd count
33≥3** on independent, always-on boxes — the explicit `addIndexer()` path. Never make a mod the 2nd indexer.
34
35## API
36
37```js
38import { CommunityRoom } from 'favela-community'
39
40// owner
41const room = await CommunityRoom.create(corestore, { name: 'My Community' })
42await room.addMember('did:plc:…'); await room.ban('did:plc:…')
43const roomKey = room.key // share this PUBLIC key
44
45// mod (co-writer)
46const mod = await CommunityRoom.open(corestore2, roomKey)
47// → hand mod.localKey to the owner, who calls room.addMod(mod.localKey)
48await mod.waitWritable(); await mod.addMember('did:plc:…')
49
50// keyless reader (favela-web)
51const view = await CommunityRoom.openReadonly(corestore3, roomKey)
52await view.members() // ['did:plc:…', …] (roster minus banned)
53```
54
55`CommunityRooms(corestore, swarm)` is a server-side read-manager: it opens + joins a keyless replica per
56room and answers `members(roomKey)` — the `Communities.resolve` source in `favela-web`.
57
58## Deploy
59
60Vendored-only lib (`file:../`, like `atproto-*`). It depends on `hyper-rooms` (`file:../`), so a consumer's
61deploy must vendor **both** `favela-community` and `hyper-rooms` (`cp -rL` into `node_modules`).
62
63## Test
64
65`npm test` → `bare test/community-room.test.mjs` — keyless read + mod admission + co-writer ops linearizing
66through the owner. Fully offline (in-process replication).
index.js +5 -0
……@@ -0,0 +1,5 @@
1// favela-community — co-owned, plaintext community rooms for Favela.
2// The community protocol (schema) + the CommunityRoom class (owner/mod/keyless-reader roles) + a
3// keyless read-manager. Shared by favela-web (read), favela-cli / a full-node (write), and hpp.
4export { communitySchema, isDid } from './lib/schema.js'
5export { CommunityRoom, CommunityRooms } from './lib/community-room.js'
lib/community-room.js +164 -0
……@@ -0,0 +1,164 @@
1// CommunityRoom — a co-owned, plaintext community room on a hyper-rooms Autobase.
2//
3// One class, three roles:
4// - OWNER (create): genesis writer + sole indexer; manages membership + admits other mods.
5// - MOD (open + admitted): a co-writer; appends membership/moderation ops after being addWritered.
6// - READER (openReadonly): KEYLESS — opens by the room's PUBLIC bootstrapKey, holds no key, never a
7// writer; replicates and derives the roster. This is favela-web's path.
8//
9// Membership + moderation linearize into a plaintext Hyperbee view (schema.js). Posts are NOT here — they
10// live in each member's own signed repo. See schema.js for the "why plaintext" rationale.
11//
12// hyper-rooms `Room` is imported by SUBPATH (not the barrel) to skip its room-pairing → blind-pairing dep,
13// which the read/write paths here don't use (mod admission is key-exchange, below).
14import { Room } from 'hyper-rooms/lib/room.js'
15import { communitySchema, isDid } from './schema.js'
16
17const MEMBER = 'member:'
18const BANNED = 'banned:'
19const META = 'meta:'
20const normKey = (k) => (typeof k === 'string' ? k.toLowerCase() : k)
21
22export class CommunityRoom {
23 constructor (room) { this._room = room }
24
25 // OWNER — create a fresh community room. This peer is the genesis writer AND the sole indexer (N=1, the
26 // safe default per the autobase quorum rule — never auto-grow to N=2). Optionally set the name.
27 static async create (corestore, { namespace = 'community', name = null } = {}) {
28 const room = new Room(corestore, { schema: communitySchema, bootstrapKey: null, namespace })
29 await room.ready()
30 const cr = new CommunityRoom(room)
31 if (name) await cr.setMeta('name', name)
32 return cr
33 }
34
35 // MOD — open an EXISTING room to co-write. Becomes writable only AFTER the owner addMods this peer's
36 // localKey (call waitWritable() to block until then). Until admitted it can still READ.
37 static async open (corestore, roomKey, { namespace } = {}) {
38 const key = normKey(roomKey)
39 if (!/^[0-9a-f]{64}$/.test(String(key))) throw new Error('roomKey must be a 32-byte hex bootstrap key')
40 const room = new Room(corestore, { schema: communitySchema, bootstrapKey: key, namespace: namespace || `community:${key}` })
41 await room.ready()
42 return new CommunityRoom(room)
43 }
44
45 // READER — keyless read-only open (semantically identical to open(); named for intent). favela-web uses
46 // this: it never calls a write method, so it is never admitted and holds no key.
47 static openReadonly (corestore, roomKey, opts) { return CommunityRoom.open(corestore, roomKey, opts) }
48
49 // ── identity / replication surface ──
50 get key () { return this._room.keyHex } // PUBLIC bootstrap key — share to invite mods/readers
51 get localKey () { return this._room.localKeyHex } // THIS peer's writer key — give to the owner for addMod
52 get discoveryKey () { return this._room.discoveryKey }
53 get writable () { return this._room.writable }
54 get corestore () { return this._room.corestore }
55 get base () { return this._room.base }
56 get view () { return this._room.view }
57
58 async update () { await this._room.base.update() }
59 async close () { await this._room.close() }
60
61 // Block until this peer has been admitted as a writer (autopass whenWritable pattern). Returns false on
62 // timeout. A mod calls this after handing its localKey to the owner, before writing.
63 waitWritable ({ timeout = 30000 } = {}) {
64 const base = this._room.base
65 if (base.writable) return Promise.resolve(true)
66 return new Promise((resolve) => {
67 let done = false
68 const finish = (v) => { if (done) return; done = true; base.off('update', check); clearTimeout(timer); resolve(v) }
69 const check = () => { if (base.writable) finish(true) }
70 const timer = setTimeout(() => finish(false), timeout)
71 base.on('update', check)
72 })
73 }
74
75 // ── reads (keyless-safe) ──
76 async members () {
77 await this._room.base.update()
78 const banned = new Set()
79 for await (const { key } of this._room.view.createReadStream({ gte: BANNED, lt: BANNED + '\xff' })) banned.add(key.slice(BANNED.length))
80 const out = []
81 for await (const { key } of this._room.view.createReadStream({ gte: MEMBER, lt: MEMBER + '\xff' })) {
82 const did = key.slice(MEMBER.length)
83 if (!banned.has(did)) out.push(did)
84 }
85 return out
86 }
87
88 async isBanned (did) { await this._room.base.update(); return !!(await this._room.view.get(BANNED + did)) }
89
90 async meta () {
91 await this._room.base.update()
92 const m = {}
93 for await (const { key, value } of this._room.view.createReadStream({ gte: META, lt: META + '\xff' })) m[key.slice(META.length)] = value && value.value
94 return m
95 }
96
97 // The co-writers (mods) — { userId, role, writerKey, addedBy, addedAt }. role 'owner' = indexer.
98 async mods () { return this._room.listWriters() }
99
100 // ── writes (mods; throw if not admitted) ──
101 async addMember (did, { by = null } = {}) { this._assertWritable('addMember'); this._assertDid(did); await this._room.append({ type: 'add-member', did, by, at: Date.now() }) }
102 async removeMember (did) { this._assertWritable('removeMember'); this._assertDid(did); await this._room.append({ type: 'remove-member', did }) }
103 async ban (did, { by = null } = {}) { this._assertWritable('ban'); this._assertDid(did); await this._room.append({ type: 'ban', did, by, at: Date.now() }) }
104 async unban (did) { this._assertWritable('unban'); this._assertDid(did); await this._room.append({ type: 'unban', did }) }
105 async setMeta (key, value) { this._assertWritable('setMeta'); await this._room.append({ type: 'set-meta', key, value }) }
106
107 // ── admission (owner adds a co-writer) ──
108 // Add another mod as a co-writer. role 'writer' (DEFAULT) = a plain writer (indexer:false) — the SAFE
109 // choice: the owner stays the sole indexer (N=1), so the mod's ops still linearize (owner acks via
110 // ackInterval) without risking the N=2 quorum trap. role 'owner' promotes to INDEXER — only do this to
111 // reach an ODD indexer count ≥3 (a real anchor), NEVER as the 2nd indexer (see addIndexer's warning).
112 async addMod (writerKeyHex, { role = 'writer', userId = null, addedBy = null } = {}) {
113 this._assertWritable('addMod')
114 if (!/^[0-9a-f]{64}$/i.test(String(writerKeyHex))) throw new Error('addMod requires the mod\'s 64-hex localKey')
115 if (!['owner', 'writer', 'mirror'].includes(role)) throw new Error('role must be owner, writer, or mirror')
116 await this._room.addWriter(userId || String(writerKeyHex).toLowerCase(), { writerKey: String(writerKeyHex).toLowerCase(), role, addedBy })
117 }
118
119 // Promote a co-writer to INDEXER (consensus participant). ⚠ AUTOBASE QUORUM: majority = floor(N/2)+1, so
120 // N=2 DEADLOCKS if either indexer drops (autobase forbids 2→1). Only grow the indexer set to an ODD count
121 // ≥3 on independent, always-on boxes (e.g. an anchor). This helper is the explicit, eyes-open path.
122 async addIndexer (writerKeyHex, { userId = null, addedBy = null } = {}) {
123 return this.addMod(writerKeyHex, { role: 'owner', userId, addedBy })
124 }
125
126 async removeMod (userId) { this._assertWritable('removeMod'); await this._room.removeWriter(userId) }
127
128 _assertWritable (op) { if (!this._room.writable) throw new Error(`${op}: not a writer — this peer holds no key / has not been admitted (favela-web is keyless by design)`) }
129 _assertDid (did) { if (!isDid(did)) throw new Error('a valid did is required') }
130}
131
132// ── read-manager (favela-web server) ───────────────────────────
133// Opens + maintains a KEYLESS read replica per community room on a shared corestore + swarm, and answers
134// members(roomKey). This is the `Communities.resolve` source for co-owned communities: the server wires
135// `resolve = (community) => communityRooms.members(community.roomKey)`.
136export class CommunityRooms {
137 constructor (corestore, swarm) {
138 this._store = corestore
139 this._swarm = swarm
140 this._rooms = new Map() // roomKeyHex -> Promise<CommunityRoom>
141 }
142
143 room (roomKeyHex) {
144 const key = normKey(roomKeyHex)
145 if (this._rooms.has(key)) return this._rooms.get(key)
146 const p = (async () => {
147 const cr = await CommunityRoom.openReadonly(this._store, key)
148 if (this._swarm) this._swarm.join(cr.discoveryKey, { client: true, server: true })
149 return cr
150 })().catch((e) => { this._rooms.delete(key); throw e }) // don't cache a failed open
151 this._rooms.set(key, p)
152 return p
153 }
154
155 async members (roomKeyHex) { return (await this.room(roomKeyHex)).members() }
156 async meta (roomKeyHex) { return (await this.room(roomKeyHex)).meta() }
157
158 async close () {
159 for (const p of this._rooms.values()) { try { await (await p).close() } catch {} }
160 this._rooms.clear()
161 }
162}
163
164export default { CommunityRoom, CommunityRooms }
lib/schema.js +48 -0
……@@ -0,0 +1,48 @@
1// The community protocol — the op contract for a co-owned community room, as a hyper-rooms schema.
2// Shared by the READ layer (favela-web / favela-native open the room keylessly and derive the roster) and
3// the WRITE layer (mods' hpp / full-node / CLI append ops). Plaintext by design (keyless-read ⊥ encryption;
4// the room holds only PUBLIC metadata — member DIDs + moderation — never posts or private messages).
5//
6// Domain ops (writer ops add-writer/remove-writer are handled by hyper-rooms Room itself):
7// add-member {did} → member:<did> (the roster)
8// remove-member {did} → del member:<did>
9// ban {did} → banned:<did> + del member:<did> (moderation; excluded from the roster)
10// unban {did} → del banned:<did>
11// set-meta {key,value} → meta:<key> (name / description / …)
12//
13// apply is DETERMINISTIC — it reads only fields carried in the op (op.at/op.by, set at append time), never
14// Date.now()/randomness (Autobase replays apply on every boot + reorg).
15import { jsonSchema } from 'hyper-rooms/lib/json-schema.js'
16
17export const isDid = (d) => typeof d === 'string' && /^did:[a-z]+:/.test(d)
18
19export const communitySchema = jsonSchema({
20 validate (op) {
21 if (['add-member', 'remove-member', 'ban', 'unban'].includes(op.type) && !isDid(op.did)) {
22 throw new Error(`${op.type} requires a valid did`)
23 }
24 if (op.type === 'set-meta' && typeof op.key !== 'string') throw new Error('set-meta requires a key')
25 },
26 async apply (batch, op, host) {
27 switch (op.type) {
28 case 'add-member':
29 await batch.put(`member:${op.did}`, { did: op.did, addedBy: op.by || null, at: op.at || null })
30 break
31 case 'remove-member':
32 await batch.del(`member:${op.did}`)
33 break
34 case 'ban':
35 await batch.del(`member:${op.did}`)
36 await batch.put(`banned:${op.did}`, { by: op.by || null, at: op.at || null })
37 break
38 case 'unban':
39 await batch.del(`banned:${op.did}`)
40 break
41 case 'set-meta':
42 await batch.put(`meta:${op.key}`, { value: op.value })
43 break
44 }
45 }
46})
47
48export default communitySchema
package-lock.json +899 -0
……@@ -0,0 +1,899 @@
1{
2 "name": "favela-community",
3 "version": "0.1.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "favela-community",
9 "version": "0.1.0",
10 "dependencies": {
11 "autobase": "^7.28.0",
12 "b4a": "^1.8.1",
13 "hyper-rooms": "file:../hyper-rooms"
14 },
15 "devDependencies": {
16 "corestore": "^7.4.0"
17 }
18 },
19 "../hyper-rooms": {
20 "version": "0.1.2",
21 "dependencies": {
22 "autobase": "^7.20.0",
23 "b4a": "^1.6.0",
24 "blind-pairing": "^2.3.1",
25 "hyperbee": "^2.20.0",
26 "hypercore-crypto": "^3.4.0",
27 "ready-resource": "^1.1.0"
28 },
29 "devDependencies": {
30 "@hyperswarm/testnet": "^3.1.4",
31 "autobase-test-helpers": "^2.0.0",
32 "brittle": "^3.0.0",
33 "corestore": "^7.0.0",
34 "hyperswarm": "^4.8.0"
35 }
36 },
37 "node_modules/@hyperswarm/secret-stream": {
38 "version": "6.9.1",
39 "resolved": "https://registry.npmjs.org/@hyperswarm/secret-stream/-/secret-stream-6.9.1.tgz",
40 "integrity": "sha512-xb0S5y3YJwBakD77JOGBHlBxdp63mHClZoXBYoLv+9wH8e054ESKlmQptWqjJK5dv5VMUIVYOJB4MaOpB0JdGw==",
41 "license": "Apache-2.0",
42 "dependencies": {
43 "b4a": "^1.1.0",
44 "hypercore-crypto": "^3.3.1",
45 "noise-curve-ed": "^2.0.1",
46 "noise-handshake": "^4.0.0",
47 "sodium-secretstream": "^1.1.0",
48 "sodium-universal": "^5.0.0",
49 "streamx": "^2.14.0",
50 "timeout-refresh": "^2.0.0",
51 "unslab": "^1.3.0"
52 }
53 },
54 "node_modules/autobase": {
55 "version": "7.28.1",
56 "resolved": "https://registry.npmjs.org/autobase/-/autobase-7.28.1.tgz",
57 "integrity": "sha512-I8SHcJE/ru5Nun6NpqwRjenB8aK01b9z1qNeyx4XuG+BxeFUpXk0ZT2u3/nG+GZnObUTFMCKOo2fbAv7ipcaLw==",
58 "license": "Apache-2.0",
59 "dependencies": {
60 "b4a": "^1.6.1",
61 "bare-events": "^2.2.0",
62 "compact-encoding": "^3.0.0",
63 "core-coupler": "^2.0.0",
64 "debounceify": "^1.0.0",
65 "encryption-encoding": "^1.0.3",
66 "hyperbee": "^2.22.0",
67 "hypercore": "^11.27.12",
68 "hypercore-crypto": "^3.4.0",
69 "hypercore-id-encoding": "^1.2.0",
70 "hyperschema": "^1.12.1",
71 "index-encoder": "^3.3.2",
72 "nanoassert": "^2.0.0",
73 "protomux-wakeup": "^2.0.0",
74 "ready-resource": "^1.0.0",
75 "resolve-reject-promise": "^1.1.0",
76 "safety-catch": "^1.0.2",
77 "scope-lock": "^1.2.4",
78 "signal-promise": "^1.0.3",
79 "sodium-universal": "^5.0.1",
80 "sub-encoder": "^2.1.1",
81 "tiny-buffer-map": "^1.1.1"
82 }
83 },
84 "node_modules/b4a": {
85 "version": "1.8.1",
86 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
87 "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==",
88 "license": "Apache-2.0",
89 "peerDependencies": {
90 "react-native-b4a": "*"
91 },
92 "peerDependenciesMeta": {
93 "react-native-b4a": {
94 "optional": true
95 }
96 }
97 },
98 "node_modules/bare-addon-resolve": {
99 "version": "1.10.0",
100 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
101 "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
102 "license": "Apache-2.0",
103 "dependencies": {
104 "bare-module-resolve": "^1.10.0",
105 "bare-semver": "^1.0.0"
106 },
107 "peerDependencies": {
108 "bare-url": "*"
109 },
110 "peerDependenciesMeta": {
111 "bare-url": {
112 "optional": true
113 }
114 }
115 },
116 "node_modules/bare-ansi-escapes": {
117 "version": "2.2.3",
118 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
119 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
120 "license": "Apache-2.0",
121 "dependencies": {
122 "bare-stream": "^2.6.5"
123 },
124 "peerDependencies": {
125 "bare-buffer": "*"
126 },
127 "peerDependenciesMeta": {
128 "bare-buffer": {
129 "optional": true
130 }
131 }
132 },
133 "node_modules/bare-assert": {
134 "version": "1.2.0",
135 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
136 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
137 "license": "Apache-2.0",
138 "dependencies": {
139 "bare-inspect": "^3.1.2"
140 }
141 },
142 "node_modules/bare-events": {
143 "version": "2.9.1",
144 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz",
145 "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==",
146 "license": "Apache-2.0",
147 "peerDependencies": {
148 "bare-abort-controller": "*"
149 },
150 "peerDependenciesMeta": {
151 "bare-abort-controller": {
152 "optional": true
153 }
154 }
155 },
156 "node_modules/bare-fs": {
157 "version": "4.7.4",
158 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.4.tgz",
159 "integrity": "sha512-y1kC+ffIx/tPLdTE693uNjHfzTfr+ravR5tvWlMXe25nELbkqV400S71qHDwbkAQ1FVEZobB1NFRzFbCCcyBCQ==",
160 "license": "Apache-2.0",
161 "dependencies": {
162 "bare-events": "^2.5.4",
163 "bare-path": "^3.0.0",
164 "bare-stream": "^2.6.4",
165 "bare-url": "^2.2.2",
166 "fast-fifo": "^1.3.2"
167 },
168 "engines": {
169 "bare": ">=1.16.0"
170 },
171 "peerDependencies": {
172 "bare-buffer": "*"
173 },
174 "peerDependenciesMeta": {
175 "bare-buffer": {
176 "optional": true
177 }
178 }
179 },
180 "node_modules/bare-inspect": {
181 "version": "3.1.4",
182 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
183 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
184 "license": "Apache-2.0",
185 "dependencies": {
186 "bare-ansi-escapes": "^2.1.0",
187 "bare-type": "^1.0.0"
188 },
189 "engines": {
190 "bare": ">=1.18.0"
191 }
192 },
193 "node_modules/bare-module-resolve": {
194 "version": "1.12.2",
195 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.2.tgz",
196 "integrity": "sha512-j+hiD5k99qec4KjJvYsI67q5AOBifmy9JG3oeMVxTmvrhn2sIdp8StrUvZu4YNgwTpO+NhniQG16N1ETDe1k5w==",
197 "license": "Apache-2.0",
198 "dependencies": {
199 "bare-semver": "^1.0.0"
200 },
201 "peerDependencies": {
202 "bare-url": "*"
203 },
204 "peerDependenciesMeta": {
205 "bare-url": {
206 "optional": true
207 }
208 }
209 },
210 "node_modules/bare-path": {
211 "version": "3.1.1",
212 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz",
213 "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==",
214 "license": "Apache-2.0"
215 },
216 "node_modules/bare-semver": {
217 "version": "1.1.0",
218 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.1.0.tgz",
219 "integrity": "sha512-1Hw5qJ7hXdVt3uPUqjeFTuxyvBUJauvz5A1I2jk8gzjZMHp04n//6nV9MDbG9CMw78JHY2lGV0w6s//LrASm2w==",
220 "license": "Apache-2.0"
221 },
222 "node_modules/bare-stream": {
223 "version": "2.13.3",
224 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz",
225 "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==",
226 "license": "Apache-2.0",
227 "dependencies": {
228 "b4a": "^1.8.1",
229 "streamx": "^2.25.0",
230 "teex": "^1.0.1"
231 },
232 "peerDependencies": {
233 "bare-abort-controller": "*",
234 "bare-buffer": "*",
235 "bare-events": "*"
236 },
237 "peerDependenciesMeta": {
238 "bare-abort-controller": {
239 "optional": true
240 },
241 "bare-buffer": {
242 "optional": true
243 },
244 "bare-events": {
245 "optional": true
246 }
247 }
248 },
249 "node_modules/bare-type": {
250 "version": "1.1.0",
251 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
252 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
253 "license": "Apache-2.0",
254 "engines": {
255 "bare": ">=1.2.0"
256 }
257 },
258 "node_modules/bare-url": {
259 "version": "2.4.5",
260 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz",
261 "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==",
262 "license": "Apache-2.0",
263 "dependencies": {
264 "bare-path": "^3.0.0"
265 }
266 },
267 "node_modules/big-sparse-array": {
268 "version": "1.0.3",
269 "resolved": "https://registry.npmjs.org/big-sparse-array/-/big-sparse-array-1.0.3.tgz",
270 "integrity": "sha512-6RjV/3mSZORlMdpUaQ6rUSpG637cZm0//E54YYGtQg1c1O+AbZP8UTdJ/TchsDZcTVLmyWZcseBfp2HBeXUXOQ==",
271 "license": "MIT"
272 },
273 "node_modules/codecs": {
274 "version": "3.1.0",
275 "resolved": "https://registry.npmjs.org/codecs/-/codecs-3.1.0.tgz",
276 "integrity": "sha512-Dqx8NwvBvnMeuPQdVKy/XEF71igjR5apxBvCGeV0pP1tXadOiaLvDTXt7xh+/5wI1ASB195mXQGJbw3Ml4YDWQ==",
277 "license": "MIT",
278 "dependencies": {
279 "b4a": "^1.6.3"
280 }
281 },
282 "node_modules/compact-encoding": {
283 "version": "3.3.0",
284 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-3.3.0.tgz",
285 "integrity": "sha512-e64XyzlBvTRJ3iScuU/U2w25Dglkm7fhrRbrGaHIwuTlNnzjRKMaQBCVl7mJRvZg7wI5a9wX1IVTXCuaAANNkw==",
286 "license": "Apache-2.0",
287 "dependencies": {
288 "b4a": "^1.3.0"
289 }
290 },
291 "node_modules/core-coupler": {
292 "version": "2.0.0",
293 "resolved": "https://registry.npmjs.org/core-coupler/-/core-coupler-2.0.0.tgz",
294 "integrity": "sha512-FJuEvsdCMwx0Wu+gFQ49rGCi8LCXh8kizHsCQwkdgPZFEFiF0z2HDvyIs+fPt5wMIfU2UVFDuN+dtpfbIxJE6g==",
295 "license": "Apache-2.0",
296 "dependencies": {
297 "safety-catch": "^1.0.2"
298 }
299 },
300 "node_modules/corestore": {
301 "version": "7.11.1",
302 "resolved": "https://registry.npmjs.org/corestore/-/corestore-7.11.1.tgz",
303 "integrity": "sha512-X6UsGFyHeAcqffTqgoDV7WDZjhfvolWabwzws6GbdqeXQY0CTM6gNpNMGEi6RGvAdZDPnFaboF5jFBy1lw1rDA==",
304 "dev": true,
305 "license": "MIT",
306 "dependencies": {
307 "b4a": "^1.6.7",
308 "bare-events": "^2.8.3",
309 "hypercore": "^11.32.0",
310 "hypercore-crypto": "^3.4.2",
311 "hypercore-errors": "^1.4.0",
312 "hypercore-id-encoding": "^1.3.0",
313 "ready-resource": "^1.1.1",
314 "sodium-universal": "^5.0.1",
315 "streamx": "^2.26.0",
316 "which-runtime": "^1.2.1"
317 }
318 },
319 "node_modules/debounceify": {
320 "version": "1.1.0",
321 "resolved": "https://registry.npmjs.org/debounceify/-/debounceify-1.1.0.tgz",
322 "integrity": "sha512-eKuHDVfJVg+u/0nPy8P+fhnLgbyuTgVxuCRrS/R7EpDSMMkBDgSes41MJtSAY1F1hcqfHz3Zy/qpqHHIp/EhdA==",
323 "license": "MIT"
324 },
325 "node_modules/device-file": {
326 "version": "2.3.1",
327 "resolved": "https://registry.npmjs.org/device-file/-/device-file-2.3.1.tgz",
328 "integrity": "sha512-bmON44lwxJPle9N2OcH4tqM44pMGZKT8G6OkzXkz0urvqQ9LKkoQdTS6w0ztYfmLdUE27R4UUmx0+y2gEz4Jug==",
329 "license": "Apache-2.0",
330 "dependencies": {
331 "b4a": "^1.6.7",
332 "bare-fs": "^4.0.1",
333 "bare-path": "^3.0.0",
334 "fd-lock": "^2.1.0",
335 "fs-native-extensions": "^1.4.0",
336 "ready-resource": "^1.2.0"
337 }
338 },
339 "node_modules/encryption-encoding": {
340 "version": "1.0.3",
341 "resolved": "https://registry.npmjs.org/encryption-encoding/-/encryption-encoding-1.0.3.tgz",
342 "integrity": "sha512-+SlKCeULnNnwBF2rGrJlSLYjwITwfjO8PLSHzue4yt+2DsVJUU8GakRkH4+mTaa3FzpxVjpTy1kKR2gYGctNWg==",
343 "license": "Apache-2.0",
344 "dependencies": {
345 "hyperschema": "^1.19.0"
346 }
347 },
348 "node_modules/events-universal": {
349 "version": "1.0.1",
350 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
351 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
352 "license": "Apache-2.0",
353 "dependencies": {
354 "bare-events": "^2.7.0"
355 }
356 },
357 "node_modules/fast-fifo": {
358 "version": "1.3.2",
359 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
360 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
361 "license": "MIT"
362 },
363 "node_modules/fd-lock": {
364 "version": "2.2.0",
365 "resolved": "https://registry.npmjs.org/fd-lock/-/fd-lock-2.2.0.tgz",
366 "integrity": "sha512-Il4jWBhjjgvUg+z8d0bC7ncXqB42caqKTUpnZ9jpcqiPmw8bkIixt7Kic1czbZPMxAQD/9kEqfZ5Dq77nSll0w==",
367 "license": "Apache-2.0",
368 "dependencies": {
369 "bare-fs": "^4.5.0",
370 "fs-native-extensions": "^1.4.4",
371 "ready-resource": "^1.2.0",
372 "resource-on-exit": "^1.0.0"
373 }
374 },
375 "node_modules/flat-tree": {
376 "version": "1.13.0",
377 "resolved": "https://registry.npmjs.org/flat-tree/-/flat-tree-1.13.0.tgz",
378 "integrity": "sha512-fT3HIuCPwHhFgJ20QYzDHgUG0zMmFg5cHvFiFo5h+QMSJ28TihsEVY0f8HGliuO+pOzmvjMx1odToeaEWkTnyQ==",
379 "license": "MIT"
380 },
381 "node_modules/fs-native-extensions": {
382 "version": "1.5.0",
383 "resolved": "https://registry.npmjs.org/fs-native-extensions/-/fs-native-extensions-1.5.0.tgz",
384 "integrity": "sha512-nuZLFm9mGCxvyi7Llww/J4OyifKCS21nEUTAmnlTZp3FObPOvA32aCedCmt4Z+8yk+caqfClNaSCfn/P7T7FLQ==",
385 "license": "Apache-2.0",
386 "dependencies": {
387 "require-addon": "^1.1.0",
388 "which-runtime": "^1.2.0"
389 }
390 },
391 "node_modules/generate-object-property": {
392 "version": "2.0.0",
393 "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-2.0.0.tgz",
394 "integrity": "sha512-KwuURPyqn2Mz8DdV29pJwQu0Y7tcsbkULr82eeOcY/ZllFK6I9Wm8dsRByIu7CKWlFi9BdW1b3mcXMp/kQBQsw==",
395 "license": "MIT",
396 "dependencies": {
397 "is-property": "^1.0.0"
398 }
399 },
400 "node_modules/generate-string": {
401 "version": "1.0.1",
402 "resolved": "https://registry.npmjs.org/generate-string/-/generate-string-1.0.1.tgz",
403 "integrity": "sha512-IfTY0dKZM43ACyGvXkbG7De7WY7MxTS5VO6Juhe8oJKpCmrYYXoqp/cJMskkpi0k9H8wuXq0H+eI898/BCqvXg==",
404 "license": "MIT"
405 },
406 "node_modules/hyper-rooms": {
407 "resolved": "../hyper-rooms",
408 "link": true
409 },
410 "node_modules/hyperbee": {
411 "version": "2.27.3",
412 "resolved": "https://registry.npmjs.org/hyperbee/-/hyperbee-2.27.3.tgz",
413 "integrity": "sha512-PXURH2U4juUZyJRKHTrY5z1zX851pmI1Q0jfv5F/hCIErDt/ND8jOZuxc3hfOLM9f0W3qJEDTMlV5AJBkVPy8w==",
414 "license": "MIT",
415 "dependencies": {
416 "b4a": "^1.6.0",
417 "codecs": "^3.0.0",
418 "debounceify": "^1.0.0",
419 "hypercore-errors": "^1.0.0",
420 "mutexify": "^1.4.0",
421 "protocol-buffers-encodings": "^1.2.0",
422 "rache": "^1.0.0",
423 "ready-resource": "^1.0.0",
424 "resolve-reject-promise": "^1.1.0",
425 "safety-catch": "^1.0.2",
426 "streamx": "^2.12.4",
427 "unslab": "^1.2.0"
428 }
429 },
430 "node_modules/hypercore": {
431 "version": "11.34.0",
432 "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-11.34.0.tgz",
433 "integrity": "sha512-wXln1nAf4pAOAxxxyWMvN/c2PrOxZSbJnMTeAPKVA4fhhQx/b6DrqfdAfS2vkvcieqRD50EJRwi5179rINheTQ==",
434 "license": "MIT",
435 "dependencies": {
436 "@hyperswarm/secret-stream": "^6.0.0",
437 "b4a": "^1.1.0",
438 "bare-events": "^2.2.0",
439 "big-sparse-array": "^1.0.3",
440 "compact-encoding": "^3.0.0",
441 "fast-fifo": "^1.3.0",
442 "flat-tree": "^1.9.0",
443 "hypercore-crypto": "^3.2.1",
444 "hypercore-errors": "^1.5.0",
445 "hypercore-id-encoding": "^1.2.0",
446 "hypercore-storage": "^3.2.0",
447 "is-options": "^1.0.1",
448 "nanoassert": "^2.0.0",
449 "protomux": "^3.5.0",
450 "quickbit-universal": "^2.2.0",
451 "random-array-iterator": "^1.0.0",
452 "safety-catch": "^1.0.1",
453 "sodium-universal": "^5.0.1",
454 "streamx": "^2.12.4",
455 "unslab": "^1.3.0",
456 "z32": "^1.0.0"
457 }
458 },
459 "node_modules/hypercore-crypto": {
460 "version": "3.7.0",
461 "resolved": "https://registry.npmjs.org/hypercore-crypto/-/hypercore-crypto-3.7.0.tgz",
462 "integrity": "sha512-SWxobptQf2V/+ZLCCDRTXqFzGfTPIkNIuDyLKXpEMfcpJ1/ec94N9aWgylHcWOHmRt14ng/KR7z0BugebWHK9Q==",
463 "license": "MIT",
464 "dependencies": {
465 "b4a": "^1.6.6",
466 "compact-encoding": "^3.0.0",
467 "sodium-universal": "^5.0.0"
468 }
469 },
470 "node_modules/hypercore-errors": {
471 "version": "1.5.0",
472 "resolved": "https://registry.npmjs.org/hypercore-errors/-/hypercore-errors-1.5.0.tgz",
473 "integrity": "sha512-5KQ/SuDxsvet+7qWA35Ay6zdD9WyAHQoyWHGcPUTbmJBd300gvNIJoi3oma7kp4TTCSzii6qYumNZe/s0j/saQ==",
474 "license": "Apache-2.0",
475 "dependencies": {
476 "hypercore-id-encoding": "^1.3.0"
477 }
478 },
479 "node_modules/hypercore-id-encoding": {
480 "version": "1.3.0",
481 "resolved": "https://registry.npmjs.org/hypercore-id-encoding/-/hypercore-id-encoding-1.3.0.tgz",
482 "integrity": "sha512-W6sHdGo5h7LXEsoWfKf/KfuROZmZRQDlGqJF2EPHW+noCK66Vvr0+zE6cL0vqQi18s0kQPeN7Sq3QyR0Ytc2VQ==",
483 "license": "Apache-2.0",
484 "dependencies": {
485 "b4a": "^1.5.3",
486 "z32": "^1.0.0"
487 }
488 },
489 "node_modules/hypercore-storage": {
490 "version": "3.2.0",
491 "resolved": "https://registry.npmjs.org/hypercore-storage/-/hypercore-storage-3.2.0.tgz",
492 "integrity": "sha512-i5O5ZMkdZaNAWGuNRXUZjuzv7B41OIhDHUwLCZPjucgVmywY0ZE5PDafu6e5oPuhtghjl6S8q6Jn+POVKN2BvQ==",
493 "license": "Apache-2.0",
494 "dependencies": {
495 "b4a": "^1.6.7",
496 "bare-path": "^3.0.0",
497 "compact-encoding": "^3.1.0",
498 "device-file": "^2.1.2",
499 "flat-tree": "^1.12.1",
500 "hypercore-crypto": "^3.4.2",
501 "hyperschema": "^1.21.0",
502 "index-encoder": "^3.3.2",
503 "resolve-reject-promise": "^1.0.0",
504 "rocksdb-native": "^3.11.0",
505 "scope-lock": "^1.2.4",
506 "streamx": "^2.21.1",
507 "xache": "^1.2.1"
508 }
509 },
510 "node_modules/hyperschema": {
511 "version": "1.21.0",
512 "resolved": "https://registry.npmjs.org/hyperschema/-/hyperschema-1.21.0.tgz",
513 "integrity": "sha512-lEnIbLTUQf7w6FU6X+R3yUJhBwCzF4ewRnAaYOrPdcVWe1rbOf94PzMiXb5pBKxroCXzlrPLxVujxAsTrrHc8g==",
514 "license": "Apache-2.0",
515 "dependencies": {
516 "bare-fs": "^4.0.1",
517 "compact-encoding": "^3.0.0",
518 "generate-object-property": "^2.0.0",
519 "generate-string": "^1.0.1"
520 }
521 },
522 "node_modules/index-encoder": {
523 "version": "3.5.0",
524 "resolved": "https://registry.npmjs.org/index-encoder/-/index-encoder-3.5.0.tgz",
525 "integrity": "sha512-idZ1cxtZz2dRV6rUiaP9Xo99UjXbSzjcMacoQmxUMu/A7fEQcNPngvwDJYeWelQUS5XFlY71/or70lKn6XnwbQ==",
526 "license": "Apache-2.0",
527 "dependencies": {
528 "b4a": "^1.6.4"
529 }
530 },
531 "node_modules/is-options": {
532 "version": "1.0.2",
533 "resolved": "https://registry.npmjs.org/is-options/-/is-options-1.0.2.tgz",
534 "integrity": "sha512-u+Ai74c8Q74aS8BuHwPdI1jptGOT1FQXgCq8/zv0xRuE+wRgSMEJLj8lVO8Zp9BeGb29BXY6AsNPinfqjkr7Fg==",
535 "license": "MIT",
536 "dependencies": {
537 "b4a": "^1.1.1"
538 }
539 },
540 "node_modules/is-property": {
541 "version": "1.0.2",
542 "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
543 "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
544 "license": "MIT"
545 },
546 "node_modules/mutexify": {
547 "version": "1.4.0",
548 "resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz",
549 "integrity": "sha512-pbYSsOrSB/AKN5h/WzzLRMFgZhClWccf2XIB4RSMC8JbquiB0e0/SH5AIfdQMdyHmYtv4seU7yV/TvAwPLJ1Yg==",
550 "license": "MIT",
551 "dependencies": {
552 "queue-tick": "^1.0.0"
553 }
554 },
555 "node_modules/nanoassert": {
556 "version": "2.0.0",
557 "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz",
558 "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==",
559 "license": "ISC"
560 },
561 "node_modules/noise-curve-ed": {
562 "version": "2.1.0",
563 "resolved": "https://registry.npmjs.org/noise-curve-ed/-/noise-curve-ed-2.1.0.tgz",
564 "integrity": "sha512-zAzJx+VwZM3w6EA1hTmDhJfvAnCeBQn/1FAeZ0LtGxCcCtlAK/uJXQVF/eDVUOaAZ286lHlx77WJ+qj9SmsRRg==",
565 "license": "ISC",
566 "dependencies": {
567 "b4a": "^1.1.0",
568 "nanoassert": "^2.0.0",
569 "sodium-universal": "^5.0.0"
570 }
571 },
572 "node_modules/noise-handshake": {
573 "version": "4.2.0",
574 "resolved": "https://registry.npmjs.org/noise-handshake/-/noise-handshake-4.2.0.tgz",
575 "integrity": "sha512-9O/VTNX/E2/AToyMTTDU0J/4WhaXMTdqc2DHs9vf+snoZ0cenSBq0dNYTVV1snYYEkmo6QeRrYMxtqtoYnY+LA==",
576 "license": "Apache-2.0",
577 "dependencies": {
578 "b4a": "^1.1.0",
579 "nanoassert": "^2.0.0",
580 "sodium-universal": "^5.0.0"
581 }
582 },
583 "node_modules/protocol-buffers-encodings": {
584 "version": "1.2.0",
585 "resolved": "https://registry.npmjs.org/protocol-buffers-encodings/-/protocol-buffers-encodings-1.2.0.tgz",
586 "integrity": "sha512-daeNPuKh1NlLD1uDfbLpD+xyUTc07nEtfHwmBZmt/vH0B7VOM+JOCOpDcx9ZRpqHjAiIkGqyTDi+wfGSl17R9w==",
587 "license": "MIT",
588 "dependencies": {
589 "b4a": "^1.6.0",
590 "signed-varint": "^2.0.1",
591 "varint": "5.0.0"
592 }
593 },
594 "node_modules/protomux": {
595 "version": "3.11.0",
596 "resolved": "https://registry.npmjs.org/protomux/-/protomux-3.11.0.tgz",
597 "integrity": "sha512-Ze43XaNHbL6zQ/zJwVYvue7Aj8pDB1HNnISsrFhAur3291Y+Iu2fMhOpySD73J/32BoBIUYrhl07lJsBR/fUVA==",
598 "license": "MIT",
599 "dependencies": {
600 "b4a": "^1.3.1",
601 "compact-encoding": "^3.0.0",
602 "queue-tick": "^1.0.0",
603 "safety-catch": "^1.0.1",
604 "unslab": "^1.3.0"
605 }
606 },
607 "node_modules/protomux-wakeup": {
608 "version": "2.9.0",
609 "resolved": "https://registry.npmjs.org/protomux-wakeup/-/protomux-wakeup-2.9.0.tgz",
610 "integrity": "sha512-K93WhS9qIRL8WAQPU2V3PxdpQ1oRlQCFyCyIYInoq/iRkcuO1BegRERWo7SS1Tcme14VKGWOr/IivjjYnHan3Q==",
611 "license": "Apache-2.0",
612 "dependencies": {
613 "b4a": "^1.6.7",
614 "hypercore-crypto": "^3.5.0",
615 "hyperschema": "^1.10.4",
616 "protomux": "^3.10.1"
617 }
618 },
619 "node_modules/queue-tick": {
620 "version": "1.0.1",
621 "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
622 "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
623 "license": "MIT"
624 },
625 "node_modules/quickbit-native": {
626 "version": "2.4.8",
627 "resolved": "https://registry.npmjs.org/quickbit-native/-/quickbit-native-2.4.8.tgz",
628 "integrity": "sha512-FcCcqI+nIAWGknqhtrYT5TSD7t/N+Xd8ctM+2PrIIBuwOi5hx0SxAvuPtzLIEMfT/2h9+fhBakUe2uALOHX6yw==",
629 "license": "Apache-2.0",
630 "optional": true,
631 "dependencies": {
632 "require-addon": "^1.1.0"
633 }
634 },
635 "node_modules/quickbit-universal": {
636 "version": "2.2.0",
637 "resolved": "https://registry.npmjs.org/quickbit-universal/-/quickbit-universal-2.2.0.tgz",
638 "integrity": "sha512-w02i1R8n7+6pEKTud8DfF8zbFY9o7RtPlUc3jWbtCkDKvhbx/AvV7oNnz4/TcmsPGpSJS+fq5Ud6RH6+YPvSGg==",
639 "license": "ISC",
640 "dependencies": {
641 "b4a": "^1.6.0",
642 "simdle-universal": "^1.1.0"
643 },
644 "optionalDependencies": {
645 "quickbit-native": "^2.2.0"
646 }
647 },
648 "node_modules/rache": {
649 "version": "1.0.0",
650 "resolved": "https://registry.npmjs.org/rache/-/rache-1.0.0.tgz",
651 "integrity": "sha512-e0k0g0w/8jOCB+7YqCIlOa+OJ38k0wrYS4x18pMSmqOvLKoyhmMhmQyCcvfY6VaP8D75cqkEnlakXs+RYYLqNg==",
652 "license": "Apache-2.0"
653 },
654 "node_modules/random-array-iterator": {
655 "version": "1.0.0",
656 "resolved": "https://registry.npmjs.org/random-array-iterator/-/random-array-iterator-1.0.0.tgz",
657 "integrity": "sha512-u7xCM93XqKEvPTP6xZp2ehttcAemKnh73oKNf1FvzuVCfpt6dILDt1Kxl1LeBjm2iNIeR49VGFhy4Iz3yOun+Q==",
658 "license": "MIT"
659 },
660 "node_modules/ready-resource": {
661 "version": "1.2.0",
662 "resolved": "https://registry.npmjs.org/ready-resource/-/ready-resource-1.2.0.tgz",
663 "integrity": "sha512-nfcco/8iAFV0M+2PYnmIc+/xY0iRb35d42HFHQ7AfjulbGEAFa+XWpByfwSyeVeiBoMLLFVMv1HixxNCqzSQ1g==",
664 "license": "MIT",
665 "dependencies": {
666 "bare-events": "^2.2.0"
667 }
668 },
669 "node_modules/refcounter": {
670 "version": "1.0.0",
671 "resolved": "https://registry.npmjs.org/refcounter/-/refcounter-1.0.0.tgz",
672 "integrity": "sha512-1WosVzUy0kPUaPMEtlNDwm99UsteALIhXXR8rerELoa63WkYIXAl0hxgwPFrIYBRWZPGUyekQ04FRtPJ7dHk9w==",
673 "license": "Apache-2.0"
674 },
675 "node_modules/require-addon": {
676 "version": "1.2.0",
677 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
678 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
679 "license": "Apache-2.0",
680 "dependencies": {
681 "bare-addon-resolve": "^1.3.0"
682 },
683 "engines": {
684 "bare": ">=1.10.0"
685 }
686 },
687 "node_modules/resolve-reject-promise": {
688 "version": "1.1.0",
689 "resolved": "https://registry.npmjs.org/resolve-reject-promise/-/resolve-reject-promise-1.1.0.tgz",
690 "integrity": "sha512-LWsTOA91AqzBTjSGgX79Tc130pwcBK6xjpJEO+qRT5IKZ6bGnHKcc8QL3upUBcWuU8OTIDzKK2VNSwmmlqvAVg==",
691 "license": "MIT"
692 },
693 "node_modules/resource-on-exit": {
694 "version": "1.0.0",
695 "resolved": "https://registry.npmjs.org/resource-on-exit/-/resource-on-exit-1.0.0.tgz",
696 "integrity": "sha512-ViJwJAknCkLRJRPR+9SISQQ7R5eRgtdIHLJsM2hHx1MweAJbJxJ5XnMjjq0Lc7ZGv44ufzAqds1nKxiVkdy4ag==",
697 "license": "Apache-2.0"
698 },
699 "node_modules/rocksdb-native": {
700 "version": "3.17.2",
701 "resolved": "https://registry.npmjs.org/rocksdb-native/-/rocksdb-native-3.17.2.tgz",
702 "integrity": "sha512-6kijLkb7NrUFCyWQsIxKIU/iQASxdqLDWrteAeXVEq1MJWrJUcXP6MDPRh8Ux7im2sBereDiI/D4vxYV2ia90A==",
703 "license": "Apache-2.0",
704 "dependencies": {
705 "compact-encoding": "^3.0.0",
706 "ready-resource": "^1.0.0",
707 "refcounter": "^1.0.0",
708 "require-addon": "^1.0.2",
709 "resolve-reject-promise": "^1.1.0",
710 "signal-promise": "^1.0.3",
711 "streamx": "^2.16.1"
712 },
713 "engines": {
714 "bare": ">=1.16.0"
715 }
716 },
717 "node_modules/safety-catch": {
718 "version": "1.0.3",
719 "resolved": "https://registry.npmjs.org/safety-catch/-/safety-catch-1.0.3.tgz",
720 "integrity": "sha512-Zq+J1TefpoEq/HTUabo0YXX5MNvttjWYODGohgPBO2jfko8Wqx3JYMgE823szDFVamdH5PlpByvfiWScTdSYDA==",
721 "license": "MIT"
722 },
723 "node_modules/scope-lock": {
724 "version": "1.2.4",
725 "resolved": "https://registry.npmjs.org/scope-lock/-/scope-lock-1.2.4.tgz",
726 "integrity": "sha512-BpSd8VCuCxW9ZitcdIC/vjs3gMaP9bRBL5nkHcyfX2VrS52n13/rHuBA2xJ/S/4DPuRdAO/Bk8pWd8eD/gHCIA==",
727 "license": "Apache-2.0"
728 },
729 "node_modules/signal-promise": {
730 "version": "1.0.3",
731 "resolved": "https://registry.npmjs.org/signal-promise/-/signal-promise-1.0.3.tgz",
732 "integrity": "sha512-WBgv0UnIq2C+Aeh0/n+IRpP6967eIx9WpynTUoiW3isPpfe1zu2LJzyfXdo9Tgef8yR/sGjcMvoUXD7EYdiz+g==",
733 "license": "MIT"
734 },
735 "node_modules/signed-varint": {
736 "version": "2.0.1",
737 "resolved": "https://registry.npmjs.org/signed-varint/-/signed-varint-2.0.1.tgz",
738 "integrity": "sha512-abgDPg1106vuZZOvw7cFwdCABddfJRz5akcCcchzTbhyhYnsG31y4AlZEgp315T7W3nQq5P4xeOm186ZiPVFzw==",
739 "license": "MIT",
740 "dependencies": {
741 "varint": "~5.0.0"
742 }
743 },
744 "node_modules/simdle-native": {
745 "version": "1.3.9",
746 "resolved": "https://registry.npmjs.org/simdle-native/-/simdle-native-1.3.9.tgz",
747 "integrity": "sha512-Isc8sP4OiiIU0mpslD4GHEnR0VQWvR/54WN7YtwEDkNdTJVWtpmvsSvsgRlw5BNGxdYXlVRegdnrSu10H/PhvA==",
748 "license": "Apache-2.0",
749 "optional": true,
750 "dependencies": {
751 "b4a": "^1.6.0",
752 "require-addon": "^1.1.0"
753 }
754 },
755 "node_modules/simdle-universal": {
756 "version": "1.1.2",
757 "resolved": "https://registry.npmjs.org/simdle-universal/-/simdle-universal-1.1.2.tgz",
758 "integrity": "sha512-3n3w1bs+uwgHKQjt6arez83EywNlhZzYvNOhvAASTl/8KqNIcqr6aHyGt3JRlfuUC7iB0tomJRPlJ2cRGIpBzA==",
759 "license": "ISC",
760 "dependencies": {
761 "b4a": "^1.6.0"
762 },
763 "optionalDependencies": {
764 "simdle-native": "^1.1.1"
765 }
766 },
767 "node_modules/sodium-native": {
768 "version": "5.1.0",
769 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.1.0.tgz",
770 "integrity": "sha512-3RxgyWyJlhTsABPnJVpCI5CoTDANZTqqFrEPqr+kjfnRaBihpVtMUE3yTF40ukdoB1APXeoBNKF3MzZAIHg39g==",
771 "license": "MIT",
772 "dependencies": {
773 "bare-assert": "^1.2.0",
774 "require-addon": "^1.1.0",
775 "which-runtime": "^1.2.1"
776 },
777 "engines": {
778 "bare": ">=1.16.0"
779 }
780 },
781 "node_modules/sodium-secretstream": {
782 "version": "1.2.0",
783 "resolved": "https://registry.npmjs.org/sodium-secretstream/-/sodium-secretstream-1.2.0.tgz",
784 "integrity": "sha512-q/DbraNFXm1KfCiiZvapmz5UC3OlpirYFIvBK2MhGaOFSb3gRyk8OXTi17UI9SGfshQNCpsVvlopogbzZNyW6Q==",
785 "license": "MIT",
786 "dependencies": {
787 "b4a": "^1.1.1",
788 "sodium-universal": "^5.0.0"
789 }
790 },
791 "node_modules/sodium-universal": {
792 "version": "5.0.1",
793 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
794 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
795 "license": "MIT",
796 "dependencies": {
797 "sodium-native": "^5.0.1"
798 },
799 "peerDependencies": {
800 "sodium-javascript": "~0.8.0"
801 },
802 "peerDependenciesMeta": {
803 "sodium-javascript": {
804 "optional": true
805 }
806 }
807 },
808 "node_modules/streamx": {
809 "version": "2.28.0",
810 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz",
811 "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==",
812 "license": "MIT",
813 "dependencies": {
814 "events-universal": "^1.0.0",
815 "fast-fifo": "^1.3.2",
816 "text-decoder": "^1.1.0"
817 }
818 },
819 "node_modules/sub-encoder": {
820 "version": "2.1.3",
821 "resolved": "https://registry.npmjs.org/sub-encoder/-/sub-encoder-2.1.3.tgz",
822 "integrity": "sha512-Xxx04ygZo/1J3yHvaSA6VhDmiSaBQkw/PmO3YnnYFXle+tfOGToC6FcDpIfMztWZXJzuKG14b/57HMkiL58C6A==",
823 "license": "Apache-2.0",
824 "dependencies": {
825 "b4a": "^1.6.0",
826 "codecs": "^3.1.0"
827 }
828 },
829 "node_modules/teex": {
830 "version": "1.0.1",
831 "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
832 "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
833 "license": "MIT",
834 "dependencies": {
835 "streamx": "^2.12.5"
836 }
837 },
838 "node_modules/text-decoder": {
839 "version": "1.2.7",
840 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
841 "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
842 "license": "Apache-2.0",
843 "dependencies": {
844 "b4a": "^1.6.4"
845 }
846 },
847 "node_modules/timeout-refresh": {
848 "version": "2.0.1",
849 "resolved": "https://registry.npmjs.org/timeout-refresh/-/timeout-refresh-2.0.1.tgz",
850 "integrity": "sha512-SVqEcMZBsZF9mA78rjzCrYrUs37LMJk3ShZ851ygZYW1cMeIjs9mL57KO6Iv5mmjSQnOe/29/VAfGXo+oRCiVw==",
851 "license": "MIT"
852 },
853 "node_modules/tiny-buffer-map": {
854 "version": "1.1.1",
855 "resolved": "https://registry.npmjs.org/tiny-buffer-map/-/tiny-buffer-map-1.1.1.tgz",
856 "integrity": "sha512-C1eDw6ks9CmkDbWVCPHobuixPTkxGa7IDERlaVk98dv4tOUdz42o3haHBr0uhNxbj0gczBTVIyS2uQsu+1vc2Q==",
857 "license": "MIT",
858 "dependencies": {
859 "b4a": "^1.6.0"
860 }
861 },
862 "node_modules/unslab": {
863 "version": "1.3.0",
864 "resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz",
865 "integrity": "sha512-YATkfKAFj47kTzmiQrWXMyRvaVrHsW6MEALa4bm+FhiA2YG4oira+Z3DXN6LrYOYn2Y8eO94Lwl9DOHjs1FpoQ==",
866 "license": "Apache-2.0",
867 "dependencies": {
868 "b4a": "^1.6.6"
869 }
870 },
871 "node_modules/varint": {
872 "version": "5.0.0",
873 "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz",
874 "integrity": "sha512-gC13b/bWrqQoKY2EmROCZ+AR0jitc6DnDGaQ6Ls9QpKmuSgJB1eQ7H3KETtQm7qSdMWMKCmsshyCmUwMLh3OAA==",
875 "license": "MIT"
876 },
877 "node_modules/which-runtime": {
878 "version": "1.4.0",
879 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.4.0.tgz",
880 "integrity": "sha512-0ugbP4CJW4e2D20jvEcC4973dCgIaHI4Rw1PT+26U9zEve7FyYdWAIwUnoeOYvoCfn+wXHoHTKb1KhkYlb60Pw==",
881 "license": "Apache-2.0"
882 },
883 "node_modules/xache": {
884 "version": "1.2.1",
885 "resolved": "https://registry.npmjs.org/xache/-/xache-1.2.1.tgz",
886 "integrity": "sha512-igRS6jPreJ54ABdzhh4mCDXcz+XMaWO2q1ABRV2yWYuk29jlp8VT7UBdCqNkX7rpYBbXsebVVKkwIuYZjyZNqA==",
887 "license": "MIT"
888 },
889 "node_modules/z32": {
890 "version": "1.1.0",
891 "resolved": "https://registry.npmjs.org/z32/-/z32-1.1.0.tgz",
892 "integrity": "sha512-1WUHy+VS6d0HPNspDxvLssBbeQjXMjSnpv0vH82vRAUfg847NmX3OXozp/hRP5jPhxBbrVzrgvAt+UsGNzRFQQ==",
893 "license": "MIT",
894 "dependencies": {
895 "b4a": "^1.5.3"
896 }
897 }
898 }
899}
package.json +18 -0
……@@ -0,0 +1,18 @@
1{
2 "name": "favela-community",
3 "version": "0.1.0",
4 "description": "Co-owned, plaintext community rooms for Favela — membership + moderation on a hyper-rooms Autobase. Read KEYLESSLY by the gateway (favela-web); WRITTEN by mods (hpp / full-node / CLI). One community protocol, shared across surfaces.",
5 "type": "module",
6 "main": "index.js",
7 "scripts": {
8 "test": "bare test/community-room.test.mjs"
9 },
10 "dependencies": {
11 "hyper-rooms": "file:../hyper-rooms",
12 "autobase": "^7.28.0",
13 "b4a": "^1.8.1"
14 },
15 "devDependencies": {
16 "corestore": "^7.4.0"
17 }
18}
test/community-room.test.mjs +73 -0
……@@ -0,0 +1,73 @@
1// favela-community — the co-owned community room, both halves:
2// READ : a KEYLESS reader (favela-web) derives the roster from the replicated plaintext view (no key).
3// WRITE : a MOD, admitted by the owner (addMod → writer), co-manages membership + moderation, and its
4// ops linearize through the owner (sole indexer) — the write layer.
5// Bare-native (Autobase/Hyperbee need a disk path). Run: bare test/community-room.test.mjs
6import Corestore from 'corestore'
7import { CommunityRoom } from '../index.js'
8
9const assert = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log(' ok -', m) }
10const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
11const stamp = Date.now()
12const A = 'did:plc:alice2222222222222222222', B = 'did:plc:bob222222222222222222222'
13const C = 'did:plc:carol22222222222222222222', D = 'did:plc:dave2222222222222222222222'
14
15const ownerStore = new Corestore('data/cc-owner-' + stamp)
16const readerStore = new Corestore('data/cc-reader-' + stamp)
17const modStore = new Corestore('data/cc-mod-' + stamp)
18
19// pipe two bases' replication streams together (in-process stand-in for the swarm)
20function wire (a, b) {
21 const s1 = a.base.replicate(true); const s2 = b.base.replicate(false)
22 s1.on('error', () => {}); s2.on('error', () => {})
23 s1.pipe(s2).pipe(s1)
24 return () => { try { s1.destroy() } catch {} try { s2.destroy() } catch {} }
25}
26async function until (fn, ms = 15000) { const end = Date.now() + ms; while (Date.now() < end) { if (await fn()) return true; await sleep(100) } return false }
27
28// ── OWNER: create + manage ──
29const owner = await CommunityRoom.create(ownerStore, { name: 'Test Community' })
30const ROOM = owner.key
31assert(/^[0-9a-f]{64}$/.test(ROOM), 'fresh room has a 64-hex public bootstrap key')
32assert(owner.writable, 'creator is a writer (genesis indexer)')
33for (const did of [A, B, C]) await owner.addMember(did)
34await owner.ban(B)
35await owner.update()
36let om = await owner.members()
37assert(om.length === 2 && om.includes(A) && om.includes(C) && !om.includes(B), 'owner roster = {A,C}, B banned')
38
39// ── KEYLESS READER ──
40const reader = await CommunityRoom.openReadonly(readerStore, ROOM)
41assert(!reader.writable, 'keyless reader is NOT writable (holds no key)')
42const unwireR = wire(owner, reader)
43assert(await until(async () => (await reader.members()).length === 2), 'KEYLESS READ: reader derives roster {A,C} from replication')
44const meta = await reader.meta()
45assert(meta.name === 'Test Community', 'keyless reader reads meta.name')
46let threw = false
47try { await reader.addMember(D) } catch { threw = true }
48assert(threw, 'keyless reader REFUSES to write (addMember throws — favela-web is keyless)')
49
50// ── MOD: admitted by the owner, co-writes (the write layer) ──
51const mod = await CommunityRoom.open(modStore, ROOM)
52assert(!mod.writable, 'mod is not writable before admission')
53const unwireM = wire(owner, mod)
54// owner admits the mod as a plain WRITER (non-indexer — the safe topology; owner stays sole indexer N=1)
55await owner.addMod(mod.localKey, { userId: 'mod-b' })
56assert(await mod.waitWritable({ timeout: 15000 }), 'mod becomes writable after owner.addMod (admission)')
57
58const mods = await owner.mods()
59const modRec = mods.find((w) => w.userId === 'mod-b')
60assert(modRec && modRec.role === 'writer', 'addMod default role = writer (non-indexer → no N=2 quorum trap)')
61
62// mod writes; its ops linearize through the owner (sole indexer, ackInterval) and reach the owner + reader
63await mod.addMember(D)
64await mod.ban(A)
65assert(await until(async () => { const m = await owner.members(); return m.includes(D) && !m.includes(A) }), 'WRITE LAYER: owner sees the mod\'s add(D) + ban(A) — co-writer ops linearize through the owner')
66assert(await until(async () => { const m = await reader.members(); return m.includes(D) && !m.includes(A) }), 'the keyless reader also converges on the mod\'s writes')
67const final = (await owner.members()).sort()
68assert(final.length === 2 && final.includes(C) && final.includes(D), 'final roster = {C,D} (A banned by mod, B banned by owner)')
69
70console.log('\nFAVELA-COMMUNITY OK (Bare-native): keyless read + mod admission + co-writer membership/moderation linearize through the owner')
71unwireR(); unwireM()
72await owner.close(); await reader.close(); await mod.close()
73try { await ownerStore.close(); await readerStore.close(); await modStore.close() } catch {}