OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-rooms commits / 131c5f8

Initial commit — hyper-rooms
Multiwriter rooms on Autobase with pluggable schemas. Provides the substrate for hyper-issues, hyper-orgs, and other domain schemas: a Room wraps an Autobase whose apply function dispatches to schema-defined op handlers, with built-in writer-set management and schema versioning. Room primitives handle the multi-writer dance (addWriter, sync, replicate, heads convergence) so schemas only express their domain ops. 13 tests / all passing, multiwriter replication verified via the canonical autobase pattern (replicateAndSync from autobase-test-helpers). Phase 3 of the pear-git-v3 bottom-up rebuild. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
131c5f8df84f6cd5404a7943944554f8752ad3d9
by plankatron <fleeky@gmail.com> · 2026-04-15T03:40:02.000Z

Changed files (9) +2654 -0

.gitignore +15 -0
……@@ -0,0 +1,15 @@
1# Dependencies
2node_modules/
3
4# Storage directories created by example CLIs
5.hyper-rooms-*/
6
7# Editor and OS cruft
8.DS_Store
9*.log
10*.swp
11.vscode/
12.idea/
13
14# Test artifacts
15coverage/
examples/cli.js +230 -0
……@@ -0,0 +1,230 @@
1#!/usr/bin/env node
2/**
3 * Mini CLI example for hyper-rooms
4 *
5 * Demonstrates a Room with a tiny "todos" schema. Lets you create rooms,
6 * append todos, list them, and manage writers.
7 *
8 * Usage:
9 * node examples/cli.js init Create a new room
10 * node examples/cli.js info Show room key + writers
11 * node examples/cli.js add <text> Add a todo
12 * node examples/cli.js list List todos
13 * node examples/cli.js check <id> Mark todo done
14 * node examples/cli.js del <id> Delete todo
15 * node examples/cli.js add-writer <userId> <writerKey> Add another writer
16 * node examples/cli.js list-writers List writers
17 * node examples/cli.js demo Run a full demo
18 *
19 * Storage: HYPER_ROOMS_STORE env var (default: ./.hyper-rooms-cli)
20 *
21 * NOTE: this CLI uses its own storage path. The web example uses a different
22 * default. They don't share state — RocksDB single-writer lock. For shared
23 * state across CLI/web/native interfaces in production, embed hyper-rooms in
24 * a single daemon and have all clients talk to the daemon via hyper-svc
25 * (planned).
26 */
27
28import Corestore from 'corestore'
29import { Room, jsonSchema } from '../index.js'
30
31const storagePath = process.env.HYPER_ROOMS_STORE || './.hyper-rooms-cli'
32const args = process.argv.slice(2)
33const cmd = args[0]
34
35const todosSchema = jsonSchema({
36 async apply (batch, op, host) {
37 if (op.type === 'add-todo') {
38 await batch.put(`todo:${op.id}`, {
39 id: op.id,
40 text: op.text,
41 done: false,
42 createdAt: op.timestamp
43 })
44 // Bump the counter
45 const counter = await batch.get('meta:next-id')
46 await batch.put('meta:next-id', { value: Math.max(counter?.value?.value || 1, op.id + 1) })
47 } else if (op.type === 'check-todo') {
48 const e = await batch.get(`todo:${op.id}`)
49 if (e) await batch.put(`todo:${op.id}`, { ...e.value, done: true })
50 } else if (op.type === 'delete-todo') {
51 await batch.del(`todo:${op.id}`)
52 }
53 }
54})
55
56async function open () {
57 const store = new Corestore(storagePath)
58 await store.ready()
59 const room = new Room(store, { schema: todosSchema, namespace: 'todos' })
60 await room.ready()
61 return { room, store }
62}
63
64async function close ({ room, store }) {
65 await room.close()
66 await store.close()
67}
68
69async function nextId (room) {
70 const counter = await room.view.get('meta:next-id')
71 return counter ? counter.value.value : 1
72}
73
74async function main () {
75 if (cmd === 'init') {
76 const ctx = await open()
77 console.log('Room initialized at: %s', storagePath)
78 console.log(' key: %s', ctx.room.keyHex)
79 console.log(' localKey: %s', ctx.room.localKeyHex)
80 console.log(' writable: %s', ctx.room.writable)
81 await close(ctx)
82 return
83 }
84
85 if (cmd === 'info') {
86 const ctx = await open()
87 console.log('Room: %s', storagePath)
88 console.log(' key: %s', ctx.room.keyHex)
89 console.log(' localKey: %s', ctx.room.localKeyHex)
90 console.log(' writable: %s', ctx.room.writable)
91 const writers = await ctx.room.listWriters()
92 console.log(' writers: %d', writers.length)
93 for (const w of writers) {
94 console.log(' %s (%s) %s', w.userId, w.role, (w.writerKey || '').slice(0, 16) + '...')
95 }
96 await close(ctx)
97 return
98 }
99
100 if (cmd === 'add') {
101 const text = args.slice(1).join(' ')
102 if (!text) { console.error('Usage: cli.js add <text>'); process.exit(1) }
103 const ctx = await open()
104 if (!ctx.room.writable) {
105 console.error('Room is not writable from this peer (no writer key registered).')
106 await close(ctx)
107 process.exit(1)
108 }
109 const id = await nextId(ctx.room)
110 await ctx.room.append({ type: 'add-todo', id, text, timestamp: Date.now() })
111 await ctx.room.base.update()
112 console.log('Added todo #%d: %s', id, text)
113 await close(ctx)
114 return
115 }
116
117 if (cmd === 'list') {
118 const ctx = await open()
119 const todos = []
120 for await (const entry of ctx.room.view.createReadStream({
121 gte: 'todo:', lt: 'todo:\xff'
122 })) {
123 todos.push(entry.value)
124 }
125 if (todos.length === 0) {
126 console.log('(no todos)')
127 } else {
128 for (const t of todos) {
129 console.log(' [%s] #%d %s', t.done ? 'x' : ' ', t.id, t.text)
130 }
131 console.log('\n%d todos', todos.length)
132 }
133 await close(ctx)
134 return
135 }
136
137 if (cmd === 'check') {
138 const id = parseInt(args[1])
139 if (!id) { console.error('Usage: cli.js check <id>'); process.exit(1) }
140 const ctx = await open()
141 await ctx.room.append({ type: 'check-todo', id })
142 await ctx.room.base.update()
143 console.log('Checked #%d', id)
144 await close(ctx)
145 return
146 }
147
148 if (cmd === 'del') {
149 const id = parseInt(args[1])
150 if (!id) { console.error('Usage: cli.js del <id>'); process.exit(1) }
151 const ctx = await open()
152 await ctx.room.append({ type: 'delete-todo', id })
153 await ctx.room.base.update()
154 console.log('Deleted #%d', id)
155 await close(ctx)
156 return
157 }
158
159 if (cmd === 'add-writer') {
160 const userId = args[1]
161 const writerKey = args[2]
162 if (!userId || !writerKey) {
163 console.error('Usage: cli.js add-writer <userId> <writerKey>')
164 process.exit(1)
165 }
166 const ctx = await open()
167 await ctx.room.addWriter(userId, { writerKey, role: 'writer' })
168 await ctx.room.base.update()
169 console.log('Added writer %s (%s)', userId, writerKey.slice(0, 16) + '...')
170 await close(ctx)
171 return
172 }
173
174 if (cmd === 'list-writers') {
175 const ctx = await open()
176 const writers = await ctx.room.listWriters()
177 if (writers.length === 0) {
178 console.log('(no writers)')
179 } else {
180 for (const w of writers) {
181 console.log(' %s (%s) writerKey=%s addedBy=%s', w.userId, w.role, (w.writerKey || '').slice(0, 16) + '...', w.addedBy || '?')
182 }
183 }
184 await close(ctx)
185 return
186 }
187
188 if (cmd === 'demo') {
189 const ctx = await open()
190 console.log('--- hyper-rooms todos demo ---\n')
191 console.log('Room key: %s', ctx.room.keyHex)
192 console.log('Local key: %s\n', ctx.room.localKeyHex)
193
194 console.log('1. Add three todos')
195 await ctx.room.append({ type: 'add-todo', id: 1, text: 'buy milk', timestamp: Date.now() })
196 await ctx.room.append({ type: 'add-todo', id: 2, text: 'walk dog', timestamp: Date.now() })
197 await ctx.room.append({ type: 'add-todo', id: 3, text: 'write code', timestamp: Date.now() })
198 await ctx.room.base.update()
199
200 console.log('2. Check off "walk dog"')
201 await ctx.room.append({ type: 'check-todo', id: 2 })
202 await ctx.room.base.update()
203
204 console.log('3. List todos:')
205 for await (const entry of ctx.room.view.createReadStream({ gte: 'todo:', lt: 'todo:\xff' })) {
206 const t = entry.value
207 console.log(' [%s] #%d %s', t.done ? 'x' : ' ', t.id, t.text)
208 }
209
210 await close(ctx)
211 return
212 }
213
214 console.log('hyper-rooms example CLI (todos schema)')
215 console.log(' init Create a new room')
216 console.log(' info Show room key + writers')
217 console.log(' add <text> Add a todo')
218 console.log(' list List todos')
219 console.log(' check <id> Mark todo done')
220 console.log(' del <id> Delete todo')
221 console.log(' add-writer <userId> <key> Authorize another writer')
222 console.log(' list-writers List authorized writers')
223 console.log(' demo Run a full demo')
224 console.log('')
225 console.log('Storage: HYPER_ROOMS_STORE env var (default: ./.hyper-rooms-cli)')
226 console.log('Note: this CLI does NOT share state with examples/web.js — they use')
227 console.log('different storage paths because RocksDB only allows one writer.')
228}
229
230main().catch((err) => { console.error(err.message); process.exit(1) })
examples/web.js +210 -0
……@@ -0,0 +1,210 @@
1#!/usr/bin/env node
2/**
3 * Mini web example for hyper-rooms
4 *
5 * SSR dashboard for a "todos" room. Lets you add, check, and delete todos
6 * via HTML form submissions. Shows the room key, local writer key, and
7 * writer list.
8 *
9 * Usage: node examples/web.js [--port 9095] [--storage <path>]
10 */
11
12import http from 'http'
13import Corestore from 'corestore'
14import { Room, jsonSchema } from '../index.js'
15import b4a from 'b4a'
16
17const args = process.argv.slice(2)
18function getFlag (name) {
19 const i = args.indexOf('--' + name)
20 return i !== -1 && args[i + 1] ? args[i + 1] : null
21}
22
23const port = parseInt(getFlag('port')) || 9095
24// NOTE: distinct default from examples/cli.js — they don't share state.
25const storagePath = getFlag('storage') || './.hyper-rooms-web'
26
27const todosSchema = jsonSchema({
28 async apply (batch, op, host) {
29 if (op.type === 'add-todo') {
30 await batch.put(`todo:${op.id}`, {
31 id: op.id,
32 text: op.text,
33 done: false,
34 createdAt: op.timestamp
35 })
36 const counter = await batch.get('meta:next-id')
37 await batch.put('meta:next-id', { value: Math.max(counter?.value?.value || 1, op.id + 1) })
38 } else if (op.type === 'check-todo') {
39 const e = await batch.get(`todo:${op.id}`)
40 if (e) await batch.put(`todo:${op.id}`, { ...e.value, done: true })
41 } else if (op.type === 'delete-todo') {
42 await batch.del(`todo:${op.id}`)
43 }
44 }
45})
46
47const store = new Corestore(storagePath)
48await store.ready()
49const room = new Room(store, { schema: todosSchema, namespace: 'todos' })
50await room.ready()
51
52function escapeHtml (s) {
53 return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
54}
55
56function collectBody (req) {
57 return new Promise((resolve, reject) => {
58 const parts = []
59 req.on('data', chunk => parts.push(chunk))
60 req.on('end', () => resolve(b4a.concat(parts).toString('utf8')))
61 req.on('error', reject)
62 })
63}
64
65async function nextId () {
66 const counter = await room.view.get('meta:next-id')
67 return counter ? counter.value.value : 1
68}
69
70async function listTodos () {
71 const out = []
72 for await (const entry of room.view.createReadStream({
73 gte: 'todo:', lt: 'todo:\xff'
74 })) {
75 out.push(entry.value)
76 }
77 return out
78}
79
80const httpServer = http.createServer(async (req, res) => {
81 const url = req.url.split('?')[0]
82 try {
83 if (req.method === 'GET' && url === '/') {
84 const todos = await listTodos()
85 const writers = await room.listWriters()
86 res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
87 res.end(renderPage(todos, writers))
88 return
89 }
90
91 if (req.method === 'POST' && url === '/add') {
92 const params = new URLSearchParams(await collectBody(req))
93 const text = params.get('text')
94 if (text && room.writable) {
95 const id = await nextId()
96 await room.append({ type: 'add-todo', id, text, timestamp: Date.now() })
97 await room.base.update()
98 }
99 res.writeHead(303, { Location: '/' })
100 res.end()
101 return
102 }
103
104 if (req.method === 'POST' && url === '/check') {
105 const params = new URLSearchParams(await collectBody(req))
106 const id = parseInt(params.get('id'))
107 if (id && room.writable) {
108 await room.append({ type: 'check-todo', id })
109 await room.base.update()
110 }
111 res.writeHead(303, { Location: '/' })
112 res.end()
113 return
114 }
115
116 if (req.method === 'POST' && url === '/del') {
117 const params = new URLSearchParams(await collectBody(req))
118 const id = parseInt(params.get('id'))
119 if (id && room.writable) {
120 await room.append({ type: 'delete-todo', id })
121 await room.base.update()
122 }
123 res.writeHead(303, { Location: '/' })
124 res.end()
125 return
126 }
127
128 res.writeHead(404, { 'Content-Type': 'text/plain' })
129 res.end('Not found')
130 } catch (err) {
131 console.error(err)
132 res.writeHead(500, { 'Content-Type': 'text/plain' })
133 res.end(err.message)
134 }
135})
136
137function renderPage (todos, writers) {
138 return `<!DOCTYPE html>
139<html><head><title>hyper-rooms</title>
140<style>
141 body { font-family: monospace; background: #0d1117; color: #c9d1d9; padding: 2rem; max-width: 720px; margin: 0 auto; }
142 h1 { color: #58a6ff; }
143 .card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 1rem; margin: 1rem 0; }
144 .card h2 { margin: 0 0 0.5rem; font-size: 0.75rem; text-transform: uppercase; color: #8b949e; }
145 .row { display: flex; justify-content: space-between; padding: 0.3rem 0; }
146 .label { color: #8b949e; }
147 .value { font-family: monospace; color: #79c0ff; word-break: break-all; max-width: 70%; text-align: right; font-size: 0.8rem; }
148 .todo { display: flex; align-items: center; padding: 0.4rem 0; border-bottom: 1px solid #21262d; }
149 .todo:last-child { border-bottom: none; }
150 .todo .text { flex: 1; margin-left: 0.5rem; }
151 .done .text { text-decoration: line-through; color: #6e7681; }
152 input, button { background: #0d1117; color: #c9d1d9; border: 1px solid #30363d; padding: 0.4rem 0.6rem; border-radius: 4px; font-family: monospace; font-size: 0.85rem; }
153 button { background: #21262d; cursor: pointer; }
154 button:hover { background: #30363d; }
155 button.btn-primary { background: #1f6feb; border-color: #1f6feb; color: white; }
156 button.btn-primary:hover { background: #1158c7; }
157 button.btn-small { padding: 0.15rem 0.4rem; font-size: 0.75rem; }
158 .empty { color: #484f58; font-style: italic; padding: 1rem 0; }
159 form { margin: 0; display: inline; }
160</style></head><body>
161<h1>hyper-rooms (todos)</h1>
162
163<div class="card">
164 <h2>Room</h2>
165 <div class="row"><span class="label">key</span><span class="value">${escapeHtml(room.keyHex || '')}</span></div>
166 <div class="row"><span class="label">localKey</span><span class="value">${escapeHtml(room.localKeyHex || '')}</span></div>
167 <div class="row"><span class="label">writable</span><span class="value">${room.writable}</span></div>
168 <div class="row"><span class="label">writers</span><span class="value">${writers.length}</span></div>
169</div>
170
171<div class="card">
172 <h2>Add todo</h2>
173 <form action="/add" method="POST">
174 <input name="text" placeholder="What needs doing?" size="40" required>
175 <button class="btn-primary" type="submit"${room.writable ? '' : ' disabled'}>Add</button>
176 </form>
177 ${room.writable ? '' : '<div class="empty" style="margin-top:0.5rem">Room is not writable from this peer.</div>'}
178</div>
179
180<div class="card">
181 <h2>Todos (${todos.length})</h2>
182 ${todos.length === 0 ? '<div class="empty">(none yet)</div>' : todos.map(t => `
183 <div class="todo${t.done ? ' done' : ''}">
184 <form action="/check" method="POST">
185 <input type="hidden" name="id" value="${t.id}">
186 <button class="btn-small" type="submit"${t.done ? ' disabled' : ''}>${t.done ? '✓' : '○'}</button>
187 </form>
188 <span class="text">#${t.id} ${escapeHtml(t.text)}</span>
189 <form action="/del" method="POST">
190 <input type="hidden" name="id" value="${t.id}">
191 <button class="btn-small" type="submit">×</button>
192 </form>
193 </div>`).join('')}
194</div>
195
196<div class="card">
197 <h2>Writers</h2>
198 ${writers.length === 0 ? '<div class="empty">(none)</div>' : writers.map(w => `
199 <div class="row"><span class="label">${escapeHtml(w.userId)} <small>(${escapeHtml(w.role)})</small></span><span class="value">${escapeHtml((w.writerKey || '').slice(0, 16))}...</span></div>`).join('')}
200</div>
201
202</body></html>`
203}
204
205httpServer.listen(port, () => {
206 console.log('hyper-rooms todos demo:')
207 console.log(' http://localhost:%d', port)
208 console.log(' storage: %s', storagePath)
209 console.log(' room key: %s', room.keyHex)
210})
index.js +10 -0
……@@ -0,0 +1,10 @@
1/**
2 * hyper-rooms — multiwriter substrate built on Autobase + Hyperbee
3 *
4 * Exports:
5 * Room — the multiwriter room class
6 * jsonSchema — convenience builder for JSON-encoded schemas
7 */
8
9export { Room } from './lib/room.js'
10export { jsonSchema } from './lib/json-schema.js'
lib/json-schema.js +52 -0
……@@ -0,0 +1,52 @@
1/**
2 * jsonSchema — helper to build a Room schema from a single apply function
3 *
4 * Most domain schemas (issues, orgs, todos) just want JSON encoding +
5 * a switch in the apply function. This helper saves them from defining
6 * encodeOp/decodeOp/validateOp boilerplate.
7 *
8 * Usage:
9 *
10 * import { jsonSchema } from 'hyper-rooms'
11 *
12 * const todosSchema = jsonSchema({
13 * async apply (batch, op, host) {
14 * if (op.type === 'add-todo') {
15 * await batch.put(`todo:${op.id}`, { text: op.text, done: false })
16 * }
17 * },
18 * // optional: validate ops before encoding
19 * validate (op) {
20 * if (op.type === 'add-todo' && !op.text) throw new Error('text required')
21 * }
22 * })
23 *
24 * const room = new Room(corestore, { schema: todosSchema })
25 */
26
27import b4a from 'b4a'
28
29export function jsonSchema ({ apply, validate } = {}) {
30 if (typeof apply !== 'function') {
31 throw new Error('jsonSchema requires an apply function')
32 }
33
34 return {
35 encodeOp (op) {
36 return b4a.from(JSON.stringify(op), 'utf8')
37 },
38
39 decodeOp (buf) {
40 const str = b4a.toString(buf, 'utf8')
41 return JSON.parse(str)
42 },
43
44 validateOp (op) {
45 if (!op || typeof op !== 'object') throw new Error('op must be an object')
46 if (typeof op.type !== 'string') throw new Error('op.type must be a string')
47 if (validate) validate(op)
48 },
49
50 apply
51 }
52}
lib/room.js +300 -0
……@@ -0,0 +1,300 @@
1/**
2 * Room — multiwriter substrate built on Autobase + Hyperbee view.
3 *
4 * Extracted from the duplicated patterns in packages/hyper-issues and
5 * packages/hyper-orgs. Both modules had nearly identical Autobase setup,
6 * Hyperbee view, and writer-management apply logic — the only difference
7 * was their domain-specific ops.
8 *
9 * `Room` handles:
10 * - Autobase + Hyperbee view boilerplate
11 * - Generic writer-management ops (add-writer, remove-writer)
12 * - Op encode/decode delegation to a pluggable schema
13 * - Single-writer or multi-writer (Autobase handles both)
14 *
15 * Schemas plug in by providing:
16 * - encodeOp(op) → Buffer
17 * - decodeOp(buf) → op object
18 * - validateOp(op) → throws on invalid
19 * - apply(batch, op, host) → mutates the Hyperbee batch
20 *
21 * The schema's apply is called for every NON-writer op. Writer ops are
22 * handled by Room itself.
23 *
24 * Usage:
25 *
26 * import { Room } from 'hyper-rooms'
27 *
28 * const room = new Room(corestore, {
29 * schema: myIssuesSchema,
30 * namespace: 'project-x',
31 * bootstrapKey: existingKey || null // null = create new room
32 * })
33 * await room.ready()
34 *
35 * // Append a domain op (validated by schema, applied to view)
36 * await room.append({ type: 'create-issue', id: 1, title: 'Bug' })
37 *
38 * // Read the view directly
39 * const issue = await room.view.get('issue:1')
40 *
41 * // Add a writer (other process holds writerKey from their local hypercore)
42 * await room.addWriter('alice', { writerKey: aliceLocalKeyHex, role: 'writer' })
43 *
44 * // Replicate via Hyperswarm
45 * swarm.on('connection', (conn) => room.corestore.replicate(conn))
46 */
47
48import Autobase from 'autobase'
49import Hyperbee from 'hyperbee'
50import ReadyResource from 'ready-resource'
51import b4a from 'b4a'
52
53export class Room extends ReadyResource {
54 /**
55 * @param {Corestore} corestore - parent corestore (will be namespaced)
56 * @param {object} opts
57 * @param {object} opts.schema - { encodeOp, decodeOp, validateOp, apply }
58 * @param {string} [opts.namespace='room'] - corestore namespace for isolation
59 * @param {Buffer|string|null} [opts.bootstrapKey=null] - existing room key, null = create new
60 * @param {string} [opts.viewName='view'] - hyperbee core name within the namespace
61 */
62 constructor (corestore, opts = {}) {
63 super()
64 if (!opts.schema) throw new Error('Room requires a schema')
65 this._validateSchema(opts.schema)
66
67 this._schema = opts.schema
68 this._namespace = opts.namespace || 'room'
69 this._viewName = opts.viewName || 'view'
70 this._corestore = corestore.namespace(this._namespace)
71 this._bootstrapKey = this._normalizeKey(opts.bootstrapKey)
72
73 this._base = new Autobase(this._corestore, this._bootstrapKey, {
74 open: (store) => {
75 const core = store.get(this._viewName, { valueEncoding: 'json' })
76 return new Hyperbee(core, {
77 keyEncoding: 'utf-8',
78 valueEncoding: 'json'
79 })
80 },
81 apply: this._apply.bind(this),
82 valueEncoding: 'binary'
83 })
84 }
85
86 // ── Lifecycle ────────────────────────────────────────────────
87
88 async _open () {
89 await this._base.ready()
90 }
91
92 async _close () {
93 await this._base.close()
94 }
95
96 // ── Properties ───────────────────────────────────────────────
97
98 /** Bootstrap key (Autobase root). Share this to invite mirrors / readers. */
99 get key () { return this._base.key }
100 get keyHex () { return this._base.key ? b4a.toString(this._base.key, 'hex') : null }
101 get discoveryKey () { return this._base.discoveryKey }
102
103 /** Local writer key (this peer's Hypercore). Share THIS for addWriter on the inviter. */
104 get localKey () { return this._base.local ? this._base.local.key : null }
105 get localKeyHex () { return this._base.local ? b4a.toString(this._base.local.key, 'hex') : null }
106
107 /** True if this peer can append ops. */
108 get writable () { return this._base.writable }
109
110 /** Read-only access to the Hyperbee view. */
111 get view () { return this._base.view }
112
113 /** The corestore (namespaced). For replication via swarm. */
114 get corestore () { return this._corestore }
115
116 /** Underlying Autobase (escape hatch). */
117 get base () { return this._base }
118
119 // ── Op append (writer side) ──────────────────────────────────
120
121 /**
122 * Append a domain op to the room. The schema validates and encodes it.
123 * Writer ops (add-writer / remove-writer) should NOT be appended via this
124 * method — use addWriter() / removeWriter() instead.
125 *
126 * @param {object} op - the op object (will be validated by the schema)
127 */
128 async append (op) {
129 if (op && (op.type === 'add-writer' || op.type === 'remove-writer')) {
130 throw new Error('Use addWriter() / removeWriter() for writer ops')
131 }
132 this._schema.validateOp(op)
133 const encoded = this._schema.encodeOp(op)
134 await this._base.append(encoded)
135 }
136
137 // ── Writer management ────────────────────────────────────────
138
139 /**
140 * Append an add-writer op. The new peer must already have its OWN local
141 * Hypercore in some other corestore — pass the hex key of that local core.
142 *
143 * Roles:
144 * - 'owner' → indexer = true (full Autobase indexer)
145 * - 'writer' → indexer = false (can append, doesn't index)
146 * - 'mirror' → metadata only, NOT registered as a writer (read-only mirror)
147 *
148 * @param {string} userId - opaque identifier for the writer
149 * @param {object} opts
150 * @param {string} opts.writerKey - hex of the writer's local Hypercore key
151 * @param {'owner'|'writer'|'mirror'} [opts.role='writer']
152 * @param {string} [opts.addedBy] - who added this writer (for audit trail)
153 */
154 async addWriter (userId, opts = {}) {
155 const { writerKey, role = 'writer', addedBy = null } = opts
156 if (!writerKey) throw new Error('addWriter requires opts.writerKey (hex)')
157 if (!['owner', 'writer', 'mirror'].includes(role)) {
158 throw new Error('role must be owner, writer, or mirror')
159 }
160
161 const op = {
162 type: 'add-writer',
163 userId,
164 writerKey,
165 role,
166 addedBy,
167 timestamp: Date.now()
168 }
169 await this._base.append(this._schema.encodeOp(op))
170 }
171
172 /**
173 * Append a remove-writer op for the given userId.
174 * @param {string} userId
175 */
176 async removeWriter (userId) {
177 const op = {
178 type: 'remove-writer',
179 userId,
180 timestamp: Date.now()
181 }
182 await this._base.append(this._schema.encodeOp(op))
183 }
184
185 /**
186 * List all writers in the view.
187 * @returns {Promise<Array<{ userId, role, writerKey, addedBy, addedAt }>>}
188 */
189 async listWriters () {
190 const out = []
191 for await (const entry of this._base.view.createReadStream({
192 gte: 'writer:',
193 lt: 'writer:\xff'
194 })) {
195 out.push({
196 userId: entry.key.slice('writer:'.length),
197 ...entry.value
198 })
199 }
200 return out
201 }
202
203 /**
204 * Get a specific writer by userId.
205 */
206 async getWriter (userId) {
207 const entry = await this._base.view.get(`writer:${userId}`)
208 return entry ? entry.value : null
209 }
210
211 // ── Apply function (called by Autobase) ──────────────────────
212
213 async _apply (nodes, view, host) {
214 const batch = view.batch()
215
216 for (const node of nodes) {
217 let op
218 try {
219 op = this._schema.decodeOp(node.value)
220 } catch (err) {
221 // Invalid op — skip silently to keep apply deterministic
222 continue
223 }
224
225 // Generic writer ops
226 if (op.type === 'add-writer') {
227 await this._applyAddWriter(batch, op, host)
228 continue
229 }
230 if (op.type === 'remove-writer') {
231 await this._applyRemoveWriter(batch, op, host)
232 continue
233 }
234
235 // Schema-specific ops
236 try {
237 await this._schema.apply(batch, op, host)
238 } catch (err) {
239 // Don't throw inside apply — log and continue.
240 // (apply must be deterministic; an exception would corrupt state.)
241 // eslint-disable-next-line no-console
242 console.error('[Room] schema.apply error:', err.message)
243 }
244 }
245
246 await batch.flush()
247 }
248
249 async _applyAddWriter (batch, op, host) {
250 await batch.put(`writer:${op.userId}`, {
251 role: op.role,
252 writerKey: op.writerKey || null,
253 addedBy: op.addedBy || null,
254 addedAt: op.timestamp || Date.now()
255 })
256
257 if (op.writerKey && op.role !== 'mirror') {
258 try {
259 await host.addWriter(b4a.from(op.writerKey, 'hex'), {
260 indexer: op.role === 'owner'
261 })
262 } catch (err) {
263 // eslint-disable-next-line no-console
264 console.error('[Room] host.addWriter failed:', err.message)
265 }
266 }
267 }
268
269 async _applyRemoveWriter (batch, op, host) {
270 const existing = await batch.get(`writer:${op.userId}`)
271 if (!existing) return
272
273 await batch.del(`writer:${op.userId}`)
274
275 if (existing.value.writerKey) {
276 try {
277 await host.removeWriter(b4a.from(existing.value.writerKey, 'hex'))
278 } catch (err) {
279 // eslint-disable-next-line no-console
280 console.error('[Room] host.removeWriter failed:', err.message)
281 }
282 }
283 }
284
285 // ── Helpers ──────────────────────────────────────────────────
286
287 _normalizeKey (key) {
288 if (!key) return null
289 return typeof key === 'string' ? b4a.from(key, 'hex') : key
290 }
291
292 _validateSchema (schema) {
293 const required = ['encodeOp', 'decodeOp', 'validateOp', 'apply']
294 for (const fn of required) {
295 if (typeof schema[fn] !== 'function') {
296 throw new Error(`Schema missing required function: ${fn}`)
297 }
298 }
299 }
300}
package-lock.json +1505 -0
……@@ -0,0 +1,1505 @@
1{
2 "name": "hyper-rooms",
3 "version": "0.1.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "hyper-rooms",
9 "version": "0.1.0",
10 "dependencies": {
11 "autobase": "^7.20.0",
12 "b4a": "^1.6.0",
13 "hyperbee": "^2.20.0",
14 "hypercore-crypto": "^3.4.0",
15 "ready-resource": "^1.1.0"
16 },
17 "devDependencies": {
18 "autobase-test-helpers": "^2.0.0",
19 "brittle": "^3.0.0",
20 "corestore": "^7.0.0"
21 }
22 },
23 "node_modules/@hyperswarm/secret-stream": {
24 "version": "6.9.1",
25 "resolved": "https://registry.npmjs.org/@hyperswarm/secret-stream/-/secret-stream-6.9.1.tgz",
26 "integrity": "sha512-xb0S5y3YJwBakD77JOGBHlBxdp63mHClZoXBYoLv+9wH8e054ESKlmQptWqjJK5dv5VMUIVYOJB4MaOpB0JdGw==",
27 "license": "Apache-2.0",
28 "dependencies": {
29 "b4a": "^1.1.0",
30 "hypercore-crypto": "^3.3.1",
31 "noise-curve-ed": "^2.0.1",
32 "noise-handshake": "^4.0.0",
33 "sodium-secretstream": "^1.1.0",
34 "sodium-universal": "^5.0.0",
35 "streamx": "^2.14.0",
36 "timeout-refresh": "^2.0.0",
37 "unslab": "^1.3.0"
38 }
39 },
40 "node_modules/@jridgewell/resolve-uri": {
41 "version": "3.1.2",
42 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
43 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
44 "dev": true,
45 "license": "MIT",
46 "engines": {
47 "node": ">=6.0.0"
48 }
49 },
50 "node_modules/@jridgewell/sourcemap-codec": {
51 "version": "1.5.5",
52 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
53 "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
54 "dev": true,
55 "license": "MIT"
56 },
57 "node_modules/@jridgewell/trace-mapping": {
58 "version": "0.3.31",
59 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
60 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
61 "dev": true,
62 "license": "MIT",
63 "dependencies": {
64 "@jridgewell/resolve-uri": "^3.1.0",
65 "@jridgewell/sourcemap-codec": "^1.4.14"
66 }
67 },
68 "node_modules/@types/istanbul-lib-coverage": {
69 "version": "2.0.6",
70 "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
71 "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
72 "dev": true,
73 "license": "MIT"
74 },
75 "node_modules/autobase": {
76 "version": "7.27.3",
77 "resolved": "https://registry.npmjs.org/autobase/-/autobase-7.27.3.tgz",
78 "integrity": "sha512-eH0UUYYO2kvy9Vug0KLj/mjjSGEslA6YL7axBlPsArlmadBDJmkYj3olpbWVqnsA+VtTndiHGQZyC/004x/aVw==",
79 "license": "Apache-2.0",
80 "dependencies": {
81 "b4a": "^1.6.1",
82 "bare-events": "^2.2.0",
83 "compact-encoding": "^2.16.0",
84 "core-coupler": "^2.0.0",
85 "debounceify": "^1.0.0",
86 "encryption-encoding": "^1.0.3",
87 "hyperbee": "^2.22.0",
88 "hypercore": "^11.27.12",
89 "hypercore-crypto": "^3.4.0",
90 "hypercore-id-encoding": "^1.2.0",
91 "hyperschema": "^1.12.1",
92 "index-encoder": "^3.3.2",
93 "nanoassert": "^2.0.0",
94 "protomux-wakeup": "^2.0.0",
95 "ready-resource": "^1.0.0",
96 "resolve-reject-promise": "^1.1.0",
97 "safety-catch": "^1.0.2",
98 "scope-lock": "^1.2.4",
99 "signal-promise": "^1.0.3",
100 "sodium-universal": "^5.0.1",
101 "sub-encoder": "^2.1.1",
102 "tiny-buffer-map": "^1.1.1"
103 }
104 },
105 "node_modules/autobase-test-helpers": {
106 "version": "2.1.3",
107 "resolved": "https://registry.npmjs.org/autobase-test-helpers/-/autobase-test-helpers-2.1.3.tgz",
108 "integrity": "sha512-DYaBLakwMeWv32VcW0StyKZ6mQjA2HV8qRlnIx0XeM93/nw8l13DdiGXZf9EAB14nhKG5drHQIOQrJAiW5BOfg==",
109 "dev": true,
110 "license": "Apache-2.0",
111 "dependencies": {
112 "b4a": "^1.6.4"
113 }
114 },
115 "node_modules/b4a": {
116 "version": "1.8.0",
117 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
118 "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
119 "license": "Apache-2.0",
120 "peerDependencies": {
121 "react-native-b4a": "*"
122 },
123 "peerDependenciesMeta": {
124 "react-native-b4a": {
125 "optional": true
126 }
127 }
128 },
129 "node_modules/bare-abort": {
130 "version": "2.0.13",
131 "resolved": "https://registry.npmjs.org/bare-abort/-/bare-abort-2.0.13.tgz",
132 "integrity": "sha512-zdc8l88eB11Jsz5rDd6sCAgv2kUFXgdrZWoMlgU6JMkfAi1/uuGFC3IEHswKbIRQTk5H3T5CMuechsXYxiaHlQ==",
133 "dev": true,
134 "license": "Apache-2.0"
135 },
136 "node_modules/bare-addon-resolve": {
137 "version": "1.10.0",
138 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
139 "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
140 "license": "Apache-2.0",
141 "dependencies": {
142 "bare-module-resolve": "^1.10.0",
143 "bare-semver": "^1.0.0"
144 },
145 "peerDependencies": {
146 "bare-url": "*"
147 },
148 "peerDependenciesMeta": {
149 "bare-url": {
150 "optional": true
151 }
152 }
153 },
154 "node_modules/bare-ansi-escapes": {
155 "version": "2.2.3",
156 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
157 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
158 "license": "Apache-2.0",
159 "dependencies": {
160 "bare-stream": "^2.6.5"
161 },
162 "peerDependencies": {
163 "bare-buffer": "*"
164 },
165 "peerDependenciesMeta": {
166 "bare-buffer": {
167 "optional": true
168 }
169 }
170 },
171 "node_modules/bare-assert": {
172 "version": "1.2.0",
173 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
174 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
175 "license": "Apache-2.0",
176 "dependencies": {
177 "bare-inspect": "^3.1.2"
178 }
179 },
180 "node_modules/bare-bundle": {
181 "version": "1.10.0",
182 "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
183 "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
184 "dev": true,
185 "license": "Apache-2.0",
186 "peerDependencies": {
187 "bare-buffer": "*",
188 "bare-url": "*"
189 },
190 "peerDependenciesMeta": {
191 "bare-buffer": {
192 "optional": true
193 },
194 "bare-url": {
195 "optional": true
196 }
197 }
198 },
199 "node_modules/bare-cov": {
200 "version": "1.2.1",
201 "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz",
202 "integrity": "sha512-SHNqb/WG9udUEj8m5qGSehoUQ9o0Xzv8nrf4HIdmG+KNU/XdhPLhTP5VPrHnSAMAXhAzEB4Q+2IYZ0x9qPzN1g==",
203 "dev": true,
204 "license": "Apache-2.0",
205 "dependencies": {
206 "bare-fs": "^4.1.2",
207 "bare-inspector": "^4.0.1",
208 "bare-path": "^3.0.0",
209 "bare-process": "^4.2.1",
210 "bare-url": "^2.1.5",
211 "bare-v8-to-istanbul": "^1.0.2",
212 "picomatch": "^4.0.2",
213 "which-runtime": "^1.2.1"
214 }
215 },
216 "node_modules/bare-crypto": {
217 "version": "1.13.4",
218 "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.4.tgz",
219 "integrity": "sha512-JiCZ5l2YOG1y8J7yy1BCAKTCZrPnHLb7pDRIdurBTOn5oIwBQDIv8iH5Pl2V85vzjl1NZXRfNY4HZLsE942jJA==",
220 "dev": true,
221 "license": "Apache-2.0",
222 "dependencies": {
223 "bare-assert": "^1.2.0",
224 "bare-stream": "^2.6.3"
225 },
226 "peerDependencies": {
227 "bare-buffer": "*"
228 },
229 "peerDependenciesMeta": {
230 "bare-buffer": {
231 "optional": true
232 }
233 }
234 },
235 "node_modules/bare-debug-log": {
236 "version": "2.0.0",
237 "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
238 "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
239 "dev": true,
240 "license": "Apache-2.0",
241 "dependencies": {
242 "bare-os": "^3.0.1"
243 }
244 },
245 "node_modules/bare-dns": {
246 "version": "2.1.4",
247 "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
248 "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==",
249 "dev": true,
250 "license": "Apache-2.0",
251 "engines": {
252 "bare": ">=1.7.0"
253 }
254 },
255 "node_modules/bare-encoding": {
256 "version": "1.0.3",
257 "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
258 "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
259 "dev": true,
260 "license": "Apache-2.0",
261 "peerDependencies": {
262 "bare-buffer": "*"
263 },
264 "peerDependenciesMeta": {
265 "bare-buffer": {
266 "optional": true
267 }
268 }
269 },
270 "node_modules/bare-env": {
271 "version": "3.0.0",
272 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
273 "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
274 "dev": true,
275 "license": "Apache-2.0",
276 "dependencies": {
277 "bare-os": "^3.0.1"
278 }
279 },
280 "node_modules/bare-events": {
281 "version": "2.8.2",
282 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
283 "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
284 "license": "Apache-2.0",
285 "peerDependencies": {
286 "bare-abort-controller": "*"
287 },
288 "peerDependenciesMeta": {
289 "bare-abort-controller": {
290 "optional": true
291 }
292 }
293 },
294 "node_modules/bare-format": {
295 "version": "1.0.2",
296 "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.2.tgz",
297 "integrity": "sha512-GswdhnOnP9QtwRbrf4wLApw5widkaLMsLe2XOs35fQD2YfEN1ApoGka+cZ7PfvzxMgfYXmMhj/2OGlVn5/Dxgw==",
298 "dev": true,
299 "license": "Apache-2.0",
300 "dependencies": {
301 "bare-inspect": "^3.0.0"
302 }
303 },
304 "node_modules/bare-fs": {
305 "version": "4.7.0",
306 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.0.tgz",
307 "integrity": "sha512-xzqKsCFxAek9aezYhjJuJRXBIaYlg/0OGDTZp+T8eYmYMlm66cs6cYko02drIyjN2CBbi+I6L7YfXyqpqtKRXA==",
308 "license": "Apache-2.0",
309 "dependencies": {
310 "bare-events": "^2.5.4",
311 "bare-path": "^3.0.0",
312 "bare-stream": "^2.6.4",
313 "bare-url": "^2.2.2",
314 "fast-fifo": "^1.3.2"
315 },
316 "engines": {
317 "bare": ">=1.16.0"
318 },
319 "peerDependencies": {
320 "bare-buffer": "*"
321 },
322 "peerDependenciesMeta": {
323 "bare-buffer": {
324 "optional": true
325 }
326 }
327 },
328 "node_modules/bare-hrtime": {
329 "version": "2.1.1",
330 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
331 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
332 "dev": true,
333 "license": "Apache-2.0"
334 },
335 "node_modules/bare-http-parser": {
336 "version": "1.1.3",
337 "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.1.3.tgz",
338 "integrity": "sha512-+dhVvQi6brHq14L/XHNRQ+TLuVE76VjRmMt61wVEtS+Od8xUslfMHWJN/ZjIIt3RtTG6vPuA+x9cOh7KrkBJsA==",
339 "dev": true,
340 "license": "Apache-2.0"
341 },
342 "node_modules/bare-http1": {
343 "version": "4.5.6",
344 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.5.6.tgz",
345 "integrity": "sha512-31OAwMkSU+z1VuUOCk65hx3aWQgzCfH/zQ6LGxbJtmiy2Czsw0+uvOBM9YkqaL6zUSTSYG2pLbL0v/TjME3Buw==",
346 "dev": true,
347 "license": "Apache-2.0",
348 "dependencies": {
349 "bare-events": "^2.6.0",
350 "bare-http-parser": "^1.1.1",
351 "bare-stream": "^2.10.0",
352 "bare-tcp": "^2.2.0"
353 },
354 "peerDependencies": {
355 "bare-buffer": "*",
356 "bare-url": "*"
357 },
358 "peerDependenciesMeta": {
359 "bare-buffer": {
360 "optional": true
361 },
362 "bare-url": {
363 "optional": true
364 }
365 }
366 },
367 "node_modules/bare-https": {
368 "version": "2.1.3",
369 "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.3.tgz",
370 "integrity": "sha512-0TI/mJXQGYXmG7UUyWEG+KCJusayIAQLywUjFAskDoKuxfqVnGF+M/mTMrEV8J64DaIdU+5x761FvgmT7M68tA==",
371 "dev": true,
372 "license": "Apache-2.0",
373 "dependencies": {
374 "bare-http1": "^4.4.0",
375 "bare-tcp": "^2.2.0",
376 "bare-tls": "^2.0.0"
377 }
378 },
379 "node_modules/bare-inspect": {
380 "version": "3.1.4",
381 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
382 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
383 "license": "Apache-2.0",
384 "dependencies": {
385 "bare-ansi-escapes": "^2.1.0",
386 "bare-type": "^1.0.0"
387 },
388 "engines": {
389 "bare": ">=1.18.0"
390 }
391 },
392 "node_modules/bare-inspector": {
393 "version": "4.0.3",
394 "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-4.0.3.tgz",
395 "integrity": "sha512-yYk6+7tTVKJKHOja5+QBn989ETv8oVsmwoH0nCM+QYC6w5MC03GC4aQNK+lipAZSz1EaAj42WZ8Wewjpy0L9TA==",
396 "dev": true,
397 "license": "Apache-2.0",
398 "dependencies": {
399 "bare-events": "^2.1.0",
400 "bare-http1": "^4.0.0",
401 "bare-stream": "^2.0.0",
402 "bare-url": "^2.0.0",
403 "bare-ws": "^2.0.0"
404 },
405 "engines": {
406 "bare": ">=1.2.0"
407 },
408 "peerDependencies": {
409 "bare-tcp": "*"
410 },
411 "peerDependenciesMeta": {
412 "bare-tcp": {
413 "optional": true
414 }
415 }
416 },
417 "node_modules/bare-module": {
418 "version": "6.1.3",
419 "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz",
420 "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==",
421 "dev": true,
422 "license": "Apache-2.0",
423 "dependencies": {
424 "bare-bundle": "^1.3.0",
425 "bare-module-lexer": "^1.0.0",
426 "bare-module-resolve": "^1.8.0",
427 "bare-path": "^3.0.0",
428 "bare-url": "^2.0.1"
429 },
430 "engines": {
431 "bare": ">=1.23.0"
432 },
433 "peerDependencies": {
434 "bare-buffer": "*"
435 },
436 "peerDependenciesMeta": {
437 "bare-buffer": {
438 "optional": true
439 }
440 }
441 },
442 "node_modules/bare-module-lexer": {
443 "version": "1.4.7",
444 "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
445 "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
446 "dev": true,
447 "license": "Apache-2.0",
448 "dependencies": {
449 "require-addon": "^1.0.2"
450 },
451 "peerDependencies": {
452 "bare-buffer": "*"
453 },
454 "peerDependenciesMeta": {
455 "bare-buffer": {
456 "optional": true
457 }
458 }
459 },
460 "node_modules/bare-module-resolve": {
461 "version": "1.12.1",
462 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz",
463 "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==",
464 "license": "Apache-2.0",
465 "dependencies": {
466 "bare-semver": "^1.0.0"
467 },
468 "peerDependencies": {
469 "bare-url": "*"
470 },
471 "peerDependenciesMeta": {
472 "bare-url": {
473 "optional": true
474 }
475 }
476 },
477 "node_modules/bare-net": {
478 "version": "2.3.1",
479 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.3.1.tgz",
480 "integrity": "sha512-MypSqDKpDU2Xt7FIfazn5yGvRnV09gFcIPHGWstW0gxuzA4tucTcwJSZeos97C4F89vtU5oGwXDN/HrGN6Y4Jw==",
481 "dev": true,
482 "license": "Apache-2.0",
483 "dependencies": {
484 "bare-events": "^2.2.2",
485 "bare-pipe": "^4.0.0",
486 "bare-stream": "^2.0.0",
487 "bare-tcp": "^2.0.0"
488 }
489 },
490 "node_modules/bare-os": {
491 "version": "3.8.7",
492 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.7.tgz",
493 "integrity": "sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w==",
494 "license": "Apache-2.0",
495 "engines": {
496 "bare": ">=1.14.0"
497 }
498 },
499 "node_modules/bare-path": {
500 "version": "3.0.0",
501 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
502 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
503 "license": "Apache-2.0",
504 "dependencies": {
505 "bare-os": "^3.0.1"
506 }
507 },
508 "node_modules/bare-pipe": {
509 "version": "4.1.5",
510 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.5.tgz",
511 "integrity": "sha512-6OfxaG8JSkRh3Gc4hzHRsxNt+yu2PpN7lrv1V+T78GdknWQkVGwiEvu4m+1nbfk8cMVQ0TGxRvQ90XA4rhnTuw==",
512 "dev": true,
513 "license": "Apache-2.0",
514 "dependencies": {
515 "bare-events": "^2.0.0",
516 "bare-stream": "^2.0.0"
517 },
518 "engines": {
519 "bare": ">=1.16.0"
520 }
521 },
522 "node_modules/bare-process": {
523 "version": "4.4.1",
524 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.4.1.tgz",
525 "integrity": "sha512-JfcTtymq6akqM2bdyTsP4A4GYJceBzb91k/ECmFps75uFf6uO33SM1bOZsRAqyRXExabsQJLn5S41NBl8HTM4A==",
526 "dev": true,
527 "license": "Apache-2.0",
528 "dependencies": {
529 "bare-abort": "^2.0.13",
530 "bare-env": "^3.0.0",
531 "bare-events": "^2.3.1",
532 "bare-hrtime": "^2.0.0",
533 "bare-os": "^3.7.1",
534 "bare-signals": "^4.0.0",
535 "bare-stdio": "^1.0.1"
536 }
537 },
538 "node_modules/bare-semver": {
539 "version": "1.0.3",
540 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.3.tgz",
541 "integrity": "sha512-HS/A30bi2+PiRJfU6R4+Kp+6KeLSCSByjYM2iiobOKzLAvtu1CT+S8xWfiU7wz0erknjkUoC+yXy108tzIuP5Q==",
542 "license": "Apache-2.0"
543 },
544 "node_modules/bare-signals": {
545 "version": "4.2.0",
546 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz",
547 "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==",
548 "dev": true,
549 "license": "Apache-2.0",
550 "dependencies": {
551 "bare-events": "^2.5.3",
552 "bare-os": "^3.3.1"
553 },
554 "engines": {
555 "bare": ">=1.7.0"
556 }
557 },
558 "node_modules/bare-stdio": {
559 "version": "1.0.2",
560 "resolved": "https://registry.npmjs.org/bare-stdio/-/bare-stdio-1.0.2.tgz",
561 "integrity": "sha512-3WJDqtvVGP4f+j68kyEC05umOYNwKJ1xG+YAXL8yZ605WgNqiRhVaFq+mVIhBt2eKNp7pa5vCQdhOt1pNh79SA==",
562 "dev": true,
563 "license": "Apache-2.0",
564 "dependencies": {
565 "bare-fs": "^4.5.2",
566 "bare-pipe": "^4.1.5",
567 "bare-tty": "^5.0.3"
568 }
569 },
570 "node_modules/bare-stream": {
571 "version": "2.13.0",
572 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.0.tgz",
573 "integrity": "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==",
574 "license": "Apache-2.0",
575 "dependencies": {
576 "streamx": "^2.25.0",
577 "teex": "^1.0.1"
578 },
579 "peerDependencies": {
580 "bare-abort-controller": "*",
581 "bare-buffer": "*",
582 "bare-events": "*"
583 },
584 "peerDependenciesMeta": {
585 "bare-abort-controller": {
586 "optional": true
587 },
588 "bare-buffer": {
589 "optional": true
590 },
591 "bare-events": {
592 "optional": true
593 }
594 }
595 },
596 "node_modules/bare-stylize": {
597 "version": "0.0.1",
598 "resolved": "https://registry.npmjs.org/bare-stylize/-/bare-stylize-0.0.1.tgz",
599 "integrity": "sha512-l3MjmIl476bWijYWf3RbE+osl4iuXSOMudzp0vAqzIK7gPgn/+G3oAxp8Oin9CFF911KBP0LO9kts8Ci8mGZaQ==",
600 "dev": true,
601 "license": "Apache-2.0",
602 "dependencies": {
603 "bare-ansi-escapes": "^2.2.3",
604 "bare-process": "^4.2.1"
605 }
606 },
607 "node_modules/bare-subprocess": {
608 "version": "5.2.3",
609 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.3.tgz",
610 "integrity": "sha512-07wwswlV7M3sC9IykbZRZ/jHAkrXFWVLqdBWGv1y0ojCimtRD9hGwxdHmR5FUFmDUZLNsBmTYJNQqgio5+A85Q==",
611 "dev": true,
612 "license": "Apache-2.0",
613 "dependencies": {
614 "bare-env": "^3.0.0",
615 "bare-events": "^2.5.4",
616 "bare-os": "^3.0.1",
617 "bare-pipe": "^4.0.0",
618 "bare-url": "^2.2.2"
619 },
620 "engines": {
621 "bare": ">=1.7.0"
622 },
623 "peerDependencies": {
624 "bare-buffer": "*"
625 },
626 "peerDependenciesMeta": {
627 "bare-buffer": {
628 "optional": true
629 }
630 }
631 },
632 "node_modules/bare-tcp": {
633 "version": "2.2.7",
634 "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.7.tgz",
635 "integrity": "sha512-rjpqNQ2cOCkNo3NeYA/W4GTK3DRkl8sDHO3uos+AEswUjLC8XXMQF8WrJCSjlIowCbS6NUVxKE92X5RGXjyefg==",
636 "dev": true,
637 "license": "Apache-2.0",
638 "dependencies": {
639 "bare-dns": "^2.0.4",
640 "bare-events": "^2.5.4",
641 "bare-stream": "^2.6.4"
642 },
643 "engines": {
644 "bare": ">=1.16.0"
645 }
646 },
647 "node_modules/bare-tls": {
648 "version": "2.2.3",
649 "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.2.3.tgz",
650 "integrity": "sha512-dPYBGEXtgLceRFMfGaF2/rqR86/xMxMyrM9ootO/gaRKL/z2uNHJs7aP7IOBtnJF8eUCt5qwMzbKfrKgDIxPLg==",
651 "dev": true,
652 "license": "Apache-2.0",
653 "dependencies": {
654 "bare-net": "^2.0.1",
655 "bare-stream": "^2.6.4"
656 },
657 "engines": {
658 "bare": ">=1.7.0"
659 }
660 },
661 "node_modules/bare-tty": {
662 "version": "5.1.0",
663 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.1.0.tgz",
664 "integrity": "sha512-EZLvW4A+XiJgI3TW+e1pMME9PIJsfEXe/DA/WSKzIkq/v7Yarpv/rvG6Z5pGnpo4V/Bd+qopwnCLSR71hMMBYA==",
665 "dev": true,
666 "license": "Apache-2.0",
667 "dependencies": {
668 "bare-events": "^2.2.0",
669 "bare-signals": "^4.0.0",
670 "bare-stream": "^2.0.0"
671 },
672 "engines": {
673 "bare": ">=1.16.0"
674 }
675 },
676 "node_modules/bare-type": {
677 "version": "1.1.0",
678 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
679 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
680 "license": "Apache-2.0",
681 "engines": {
682 "bare": ">=1.2.0"
683 }
684 },
685 "node_modules/bare-url": {
686 "version": "2.4.0",
687 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz",
688 "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==",
689 "license": "Apache-2.0",
690 "peer": true,
691 "dependencies": {
692 "bare-path": "^3.0.0"
693 }
694 },
695 "node_modules/bare-utils": {
696 "version": "1.6.0",
697 "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.6.0.tgz",
698 "integrity": "sha512-WhQEIkkAxkSnW7u1QgrI0AfNm5JpMruETXeYsb5qnkBJ0TTfNKygZmsh6rkoHBANaV+C/7Jed7bJP9OmEHG7rQ==",
699 "dev": true,
700 "license": "Apache-2.0",
701 "dependencies": {
702 "bare-debug-log": "^2.0.0",
703 "bare-encoding": "^1.0.0",
704 "bare-format": "^1.0.0",
705 "bare-inspect": "^3.0.0",
706 "bare-stylize": "^0.0.1",
707 "bare-type": "^1.0.6"
708 }
709 },
710 "node_modules/bare-v8-to-istanbul": {
711 "version": "1.0.2",
712 "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
713 "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==",
714 "dev": true,
715 "license": "Apache-2.0",
716 "dependencies": {
717 "bare-assert": "^1.0.2",
718 "bare-fs": "^4.1.2",
719 "bare-module": "^6.1.2",
720 "bare-path": "^3.0.0",
721 "bare-process": "^4.2.0",
722 "bare-url": "^2.1.5",
723 "bare-utils": "^1.2.0",
724 "v8-to-istanbul": "^9.3.0",
725 "which-runtime": "^1.2.1"
726 }
727 },
728 "node_modules/bare-ws": {
729 "version": "2.1.0",
730 "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.1.0.tgz",
731 "integrity": "sha512-2gEWlPK9iyBchACdIY6oQXgmDz3KLrChdwrPgmU3IVXOFTnxTqSUT27WE/+izd4QojHj/SsqDkQiD2HDvuTdAA==",
732 "dev": true,
733 "license": "Apache-2.0",
734 "dependencies": {
735 "bare-crypto": "^1.2.0",
736 "bare-events": "^2.3.1",
737 "bare-http1": "^4.0.0",
738 "bare-https": "^2.0.0",
739 "bare-stream": "^2.1.2"
740 },
741 "peerDependencies": {
742 "bare-buffer": "*",
743 "bare-url": "*"
744 },
745 "peerDependenciesMeta": {
746 "bare-buffer": {
747 "optional": true
748 },
749 "bare-url": {
750 "optional": true
751 }
752 }
753 },
754 "node_modules/big-sparse-array": {
755 "version": "1.0.3",
756 "resolved": "https://registry.npmjs.org/big-sparse-array/-/big-sparse-array-1.0.3.tgz",
757 "integrity": "sha512-6RjV/3mSZORlMdpUaQ6rUSpG637cZm0//E54YYGtQg1c1O+AbZP8UTdJ/TchsDZcTVLmyWZcseBfp2HBeXUXOQ==",
758 "license": "MIT"
759 },
760 "node_modules/brittle": {
761 "version": "3.19.1",
762 "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz",
763 "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==",
764 "dev": true,
765 "license": "Apache-2.0",
766 "dependencies": {
767 "b4a": "^1.6.0",
768 "bare-assert": "^1.0.2",
769 "bare-cov": "^1.1.0",
770 "bare-fs": "^4.1.6",
771 "bare-os": "^3.6.1",
772 "bare-path": "^3.0.0",
773 "bare-process": "^4.2.1",
774 "bare-subprocess": "^5.0.0",
775 "bare-url": "^2.1.6",
776 "error-stack-parser": "^2.1.4",
777 "globbie": "^1.0.2",
778 "paparam": "^1.6.2",
779 "same-object": "^1.0.2",
780 "test-tmp": "^1.4.0",
781 "tmatch": "^5.0.0"
782 },
783 "bin": {
784 "brittle": "bin/node.js",
785 "brittle-bare": "bin/bare.js",
786 "brittle-node": "bin/node.js",
787 "brittle-pear": "bin/pear.js"
788 }
789 },
790 "node_modules/codecs": {
791 "version": "3.1.0",
792 "resolved": "https://registry.npmjs.org/codecs/-/codecs-3.1.0.tgz",
793 "integrity": "sha512-Dqx8NwvBvnMeuPQdVKy/XEF71igjR5apxBvCGeV0pP1tXadOiaLvDTXt7xh+/5wI1ASB195mXQGJbw3Ml4YDWQ==",
794 "license": "MIT",
795 "dependencies": {
796 "b4a": "^1.6.3"
797 }
798 },
799 "node_modules/compact-encoding": {
800 "version": "2.19.2",
801 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-2.19.2.tgz",
802 "integrity": "sha512-/YjhHQE/5L4F7l5Bht69dRbP9RV6zoJPeowi8bMKQxNKe3Nh6hOY8pBGoVE9fz5GaWfEd8fWJ2aU9sB4KZuMYg==",
803 "license": "Apache-2.0",
804 "dependencies": {
805 "b4a": "^1.3.0"
806 }
807 },
808 "node_modules/convert-source-map": {
809 "version": "2.0.0",
810 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
811 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
812 "dev": true,
813 "license": "MIT"
814 },
815 "node_modules/core-coupler": {
816 "version": "2.0.0",
817 "resolved": "https://registry.npmjs.org/core-coupler/-/core-coupler-2.0.0.tgz",
818 "integrity": "sha512-FJuEvsdCMwx0Wu+gFQ49rGCi8LCXh8kizHsCQwkdgPZFEFiF0z2HDvyIs+fPt5wMIfU2UVFDuN+dtpfbIxJE6g==",
819 "license": "Apache-2.0",
820 "dependencies": {
821 "safety-catch": "^1.0.2"
822 }
823 },
824 "node_modules/corestore": {
825 "version": "7.9.2",
826 "resolved": "https://registry.npmjs.org/corestore/-/corestore-7.9.2.tgz",
827 "integrity": "sha512-dyIktVSVLu0skZ1UM4yODbNbZ46dBpTHypzwlwET1btVuS3YIf66Zz8Tx8h3Xa8nbLO2USGqrYtvPYzSRj2wCA==",
828 "dev": true,
829 "license": "MIT",
830 "dependencies": {
831 "b4a": "^1.6.7",
832 "hypercore": "^11.19.0",
833 "hypercore-crypto": "^3.4.2",
834 "hypercore-errors": "^1.4.0",
835 "hypercore-id-encoding": "^1.3.0",
836 "ready-resource": "^1.1.1",
837 "sodium-universal": "^5.0.1",
838 "which-runtime": "^1.2.1"
839 }
840 },
841 "node_modules/debounceify": {
842 "version": "1.1.0",
843 "resolved": "https://registry.npmjs.org/debounceify/-/debounceify-1.1.0.tgz",
844 "integrity": "sha512-eKuHDVfJVg+u/0nPy8P+fhnLgbyuTgVxuCRrS/R7EpDSMMkBDgSes41MJtSAY1F1hcqfHz3Zy/qpqHHIp/EhdA==",
845 "license": "MIT"
846 },
847 "node_modules/device-file": {
848 "version": "2.3.1",
849 "resolved": "https://registry.npmjs.org/device-file/-/device-file-2.3.1.tgz",
850 "integrity": "sha512-bmON44lwxJPle9N2OcH4tqM44pMGZKT8G6OkzXkz0urvqQ9LKkoQdTS6w0ztYfmLdUE27R4UUmx0+y2gEz4Jug==",
851 "license": "Apache-2.0",
852 "dependencies": {
853 "b4a": "^1.6.7",
854 "bare-fs": "^4.0.1",
855 "bare-path": "^3.0.0",
856 "fd-lock": "^2.1.0",
857 "fs-native-extensions": "^1.4.0",
858 "ready-resource": "^1.2.0"
859 }
860 },
861 "node_modules/encryption-encoding": {
862 "version": "1.0.3",
863 "resolved": "https://registry.npmjs.org/encryption-encoding/-/encryption-encoding-1.0.3.tgz",
864 "integrity": "sha512-+SlKCeULnNnwBF2rGrJlSLYjwITwfjO8PLSHzue4yt+2DsVJUU8GakRkH4+mTaa3FzpxVjpTy1kKR2gYGctNWg==",
865 "license": "Apache-2.0",
866 "dependencies": {
867 "hyperschema": "^1.19.0"
868 }
869 },
870 "node_modules/error-stack-parser": {
871 "version": "2.1.4",
872 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
873 "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
874 "dev": true,
875 "license": "MIT",
876 "dependencies": {
877 "stackframe": "^1.3.4"
878 }
879 },
880 "node_modules/events-universal": {
881 "version": "1.0.1",
882 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
883 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
884 "license": "Apache-2.0",
885 "dependencies": {
886 "bare-events": "^2.7.0"
887 }
888 },
889 "node_modules/fast-fifo": {
890 "version": "1.3.2",
891 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
892 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
893 "license": "MIT"
894 },
895 "node_modules/fd-lock": {
896 "version": "2.1.1",
897 "resolved": "https://registry.npmjs.org/fd-lock/-/fd-lock-2.1.1.tgz",
898 "integrity": "sha512-H3VkcWFl39Rk0xBokDcUyBcVs6VrYTUo/DhDWMzJOF99wXWDAvmvq/GlLTS2NTehsznZhp3fXVyrtB2ldDXhwg==",
899 "license": "Apache-2.0",
900 "dependencies": {
901 "bare-fs": "^4.5.0",
902 "fs-native-extensions": "^1.4.4",
903 "ready-resource": "^1.2.0",
904 "resource-on-exit": "^1.0.0"
905 }
906 },
907 "node_modules/flat-tree": {
908 "version": "1.13.0",
909 "resolved": "https://registry.npmjs.org/flat-tree/-/flat-tree-1.13.0.tgz",
910 "integrity": "sha512-fT3HIuCPwHhFgJ20QYzDHgUG0zMmFg5cHvFiFo5h+QMSJ28TihsEVY0f8HGliuO+pOzmvjMx1odToeaEWkTnyQ==",
911 "license": "MIT"
912 },
913 "node_modules/fs-native-extensions": {
914 "version": "1.4.5",
915 "resolved": "https://registry.npmjs.org/fs-native-extensions/-/fs-native-extensions-1.4.5.tgz",
916 "integrity": "sha512-ekV0T//iDm4AvhOcuPaHpxub4DI7HvY5ucLJVDvi7T2J+NZkQ9S6MuvgP0yeQvoqNUaAGyLjVYb1905BF9bpmg==",
917 "license": "Apache-2.0",
918 "dependencies": {
919 "require-addon": "^1.1.0",
920 "which-runtime": "^1.2.0"
921 }
922 },
923 "node_modules/generate-object-property": {
924 "version": "2.0.0",
925 "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-2.0.0.tgz",
926 "integrity": "sha512-KwuURPyqn2Mz8DdV29pJwQu0Y7tcsbkULr82eeOcY/ZllFK6I9Wm8dsRByIu7CKWlFi9BdW1b3mcXMp/kQBQsw==",
927 "license": "MIT",
928 "dependencies": {
929 "is-property": "^1.0.0"
930 }
931 },
932 "node_modules/generate-string": {
933 "version": "1.0.1",
934 "resolved": "https://registry.npmjs.org/generate-string/-/generate-string-1.0.1.tgz",
935 "integrity": "sha512-IfTY0dKZM43ACyGvXkbG7De7WY7MxTS5VO6Juhe8oJKpCmrYYXoqp/cJMskkpi0k9H8wuXq0H+eI898/BCqvXg==",
936 "license": "MIT"
937 },
938 "node_modules/globbie": {
939 "version": "1.0.3",
940 "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz",
941 "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==",
942 "dev": true,
943 "license": "Apache-2.0",
944 "dependencies": {
945 "bare-fs": "^4.1.2",
946 "bare-path": "^3.0.0",
947 "bare-process": "^4.2.1",
948 "picomatch": "^4.0.2"
949 }
950 },
951 "node_modules/hyperbee": {
952 "version": "2.27.3",
953 "resolved": "https://registry.npmjs.org/hyperbee/-/hyperbee-2.27.3.tgz",
954 "integrity": "sha512-PXURH2U4juUZyJRKHTrY5z1zX851pmI1Q0jfv5F/hCIErDt/ND8jOZuxc3hfOLM9f0W3qJEDTMlV5AJBkVPy8w==",
955 "license": "MIT",
956 "dependencies": {
957 "b4a": "^1.6.0",
958 "codecs": "^3.0.0",
959 "debounceify": "^1.0.0",
960 "hypercore-errors": "^1.0.0",
961 "mutexify": "^1.4.0",
962 "protocol-buffers-encodings": "^1.2.0",
963 "rache": "^1.0.0",
964 "ready-resource": "^1.0.0",
965 "resolve-reject-promise": "^1.1.0",
966 "safety-catch": "^1.0.2",
967 "streamx": "^2.12.4",
968 "unslab": "^1.2.0"
969 }
970 },
971 "node_modules/hypercore": {
972 "version": "11.28.1",
973 "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-11.28.1.tgz",
974 "integrity": "sha512-d8jqUilG7Gri8wT+sRdkFTYRPMHGyvBz/NCYHf5rX7+LoivRNRxQm5nWChT7caiUKbcsDak6h7IiEQ5wbD1wCg==",
975 "license": "MIT",
976 "dependencies": {
977 "@hyperswarm/secret-stream": "^6.0.0",
978 "b4a": "^1.1.0",
979 "bare-events": "^2.2.0",
980 "big-sparse-array": "^1.0.3",
981 "compact-encoding": "^2.11.0",
982 "fast-fifo": "^1.3.0",
983 "flat-tree": "^1.9.0",
984 "hypercore-crypto": "^3.2.1",
985 "hypercore-errors": "^1.5.0",
986 "hypercore-id-encoding": "^1.2.0",
987 "hypercore-storage": "^2.8.0",
988 "is-options": "^1.0.1",
989 "nanoassert": "^2.0.0",
990 "protomux": "^3.5.0",
991 "quickbit-universal": "^2.2.0",
992 "random-array-iterator": "^1.0.0",
993 "safety-catch": "^1.0.1",
994 "sodium-universal": "^5.0.1",
995 "streamx": "^2.12.4",
996 "unslab": "^1.3.0",
997 "z32": "^1.0.0"
998 }
999 },
1000 "node_modules/hypercore-crypto": {
1001 "version": "3.6.1",
1002 "resolved": "https://registry.npmjs.org/hypercore-crypto/-/hypercore-crypto-3.6.1.tgz",
1003 "integrity": "sha512-ltIz2uDwy9pO/ZGTvqcjzyBkvt6O4cVm4r/nNxh0GFs/RbQtqP/i4wCvLEdmU7ptgtnw7fI67WYD1aHPuv4OVA==",
1004 "license": "MIT",
1005 "dependencies": {
1006 "b4a": "^1.6.6",
1007 "compact-encoding": "^2.15.0",
1008 "sodium-universal": "^5.0.0"
1009 }
1010 },
1011 "node_modules/hypercore-errors": {
1012 "version": "1.5.0",
1013 "resolved": "https://registry.npmjs.org/hypercore-errors/-/hypercore-errors-1.5.0.tgz",
1014 "integrity": "sha512-5KQ/SuDxsvet+7qWA35Ay6zdD9WyAHQoyWHGcPUTbmJBd300gvNIJoi3oma7kp4TTCSzii6qYumNZe/s0j/saQ==",
1015 "license": "Apache-2.0",
1016 "dependencies": {
1017 "hypercore-id-encoding": "^1.3.0"
1018 }
1019 },
1020 "node_modules/hypercore-id-encoding": {
1021 "version": "1.3.0",
1022 "resolved": "https://registry.npmjs.org/hypercore-id-encoding/-/hypercore-id-encoding-1.3.0.tgz",
1023 "integrity": "sha512-W6sHdGo5h7LXEsoWfKf/KfuROZmZRQDlGqJF2EPHW+noCK66Vvr0+zE6cL0vqQi18s0kQPeN7Sq3QyR0Ytc2VQ==",
1024 "license": "Apache-2.0",
1025 "dependencies": {
1026 "b4a": "^1.5.3",
1027 "z32": "^1.0.0"
1028 }
1029 },
1030 "node_modules/hypercore-storage": {
1031 "version": "2.8.0",
1032 "resolved": "https://registry.npmjs.org/hypercore-storage/-/hypercore-storage-2.8.0.tgz",
1033 "integrity": "sha512-s35HllFMYvrkSHvkIIklVFmCI/bRVNO7H3pi4mKvWl0XLyhGGGM+NtJ7fpsh82oPRCmL9XtMRO+pT6LiMAO8MA==",
1034 "license": "Apache-2.0",
1035 "dependencies": {
1036 "b4a": "^1.6.7",
1037 "bare-fs": "^4.0.1",
1038 "bare-path": "^3.0.0",
1039 "compact-encoding": "^2.16.0",
1040 "device-file": "^2.1.2",
1041 "flat-tree": "^1.12.1",
1042 "hypercore-crypto": "^3.4.2",
1043 "hyperschema": "^1.7.0",
1044 "index-encoder": "^3.3.2",
1045 "resolve-reject-promise": "^1.0.0",
1046 "rocksdb-native": "^3.11.0",
1047 "scope-lock": "^1.2.4",
1048 "streamx": "^2.21.1"
1049 }
1050 },
1051 "node_modules/hyperschema": {
1052 "version": "1.20.1",
1053 "resolved": "https://registry.npmjs.org/hyperschema/-/hyperschema-1.20.1.tgz",
1054 "integrity": "sha512-7gnvaTNUs8FqdrU3NNE6nzhOneGyDlUlpROu9YFlHA61fE8ppKKf2gLZjZjsx/enNt+VFN4ITxy8ijypfyWBLw==",
1055 "license": "Apache-2.0",
1056 "dependencies": {
1057 "bare-fs": "^4.0.1",
1058 "compact-encoding": "^2.19.0",
1059 "generate-object-property": "^2.0.0",
1060 "generate-string": "^1.0.1"
1061 }
1062 },
1063 "node_modules/index-encoder": {
1064 "version": "3.5.0",
1065 "resolved": "https://registry.npmjs.org/index-encoder/-/index-encoder-3.5.0.tgz",
1066 "integrity": "sha512-idZ1cxtZz2dRV6rUiaP9Xo99UjXbSzjcMacoQmxUMu/A7fEQcNPngvwDJYeWelQUS5XFlY71/or70lKn6XnwbQ==",
1067 "license": "Apache-2.0",
1068 "dependencies": {
1069 "b4a": "^1.6.4"
1070 }
1071 },
1072 "node_modules/is-options": {
1073 "version": "1.0.2",
1074 "resolved": "https://registry.npmjs.org/is-options/-/is-options-1.0.2.tgz",
1075 "integrity": "sha512-u+Ai74c8Q74aS8BuHwPdI1jptGOT1FQXgCq8/zv0xRuE+wRgSMEJLj8lVO8Zp9BeGb29BXY6AsNPinfqjkr7Fg==",
1076 "license": "MIT",
1077 "dependencies": {
1078 "b4a": "^1.1.1"
1079 }
1080 },
1081 "node_modules/is-property": {
1082 "version": "1.0.2",
1083 "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
1084 "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
1085 "license": "MIT"
1086 },
1087 "node_modules/mutexify": {
1088 "version": "1.4.0",
1089 "resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz",
1090 "integrity": "sha512-pbYSsOrSB/AKN5h/WzzLRMFgZhClWccf2XIB4RSMC8JbquiB0e0/SH5AIfdQMdyHmYtv4seU7yV/TvAwPLJ1Yg==",
1091 "license": "MIT",
1092 "dependencies": {
1093 "queue-tick": "^1.0.0"
1094 }
1095 },
1096 "node_modules/nanoassert": {
1097 "version": "2.0.0",
1098 "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz",
1099 "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==",
1100 "license": "ISC"
1101 },
1102 "node_modules/noise-curve-ed": {
1103 "version": "2.1.0",
1104 "resolved": "https://registry.npmjs.org/noise-curve-ed/-/noise-curve-ed-2.1.0.tgz",
1105 "integrity": "sha512-zAzJx+VwZM3w6EA1hTmDhJfvAnCeBQn/1FAeZ0LtGxCcCtlAK/uJXQVF/eDVUOaAZ286lHlx77WJ+qj9SmsRRg==",
1106 "license": "ISC",
1107 "dependencies": {
1108 "b4a": "^1.1.0",
1109 "nanoassert": "^2.0.0",
1110 "sodium-universal": "^5.0.0"
1111 }
1112 },
1113 "node_modules/noise-handshake": {
1114 "version": "4.2.0",
1115 "resolved": "https://registry.npmjs.org/noise-handshake/-/noise-handshake-4.2.0.tgz",
1116 "integrity": "sha512-9O/VTNX/E2/AToyMTTDU0J/4WhaXMTdqc2DHs9vf+snoZ0cenSBq0dNYTVV1snYYEkmo6QeRrYMxtqtoYnY+LA==",
1117 "license": "Apache-2.0",
1118 "dependencies": {
1119 "b4a": "^1.1.0",
1120 "nanoassert": "^2.0.0",
1121 "sodium-universal": "^5.0.0"
1122 }
1123 },
1124 "node_modules/paparam": {
1125 "version": "1.10.1",
1126 "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.1.tgz",
1127 "integrity": "sha512-viyQI64VIja0Za3njIzhoEP8ZVkgPowhZPuG0E96NwBfYJ6ZIyrrlhWGtFPkdN7eYLl2L8CTWL+wla0evm1KKQ==",
1128 "dev": true,
1129 "license": "Apache-2.0"
1130 },
1131 "node_modules/picomatch": {
1132 "version": "4.0.4",
1133 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
1134 "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
1135 "dev": true,
1136 "license": "MIT",
1137 "engines": {
1138 "node": ">=12"
1139 },
1140 "funding": {
1141 "url": "https://github.com/sponsors/jonschlinkert"
1142 }
1143 },
1144 "node_modules/protocol-buffers-encodings": {
1145 "version": "1.2.0",
1146 "resolved": "https://registry.npmjs.org/protocol-buffers-encodings/-/protocol-buffers-encodings-1.2.0.tgz",
1147 "integrity": "sha512-daeNPuKh1NlLD1uDfbLpD+xyUTc07nEtfHwmBZmt/vH0B7VOM+JOCOpDcx9ZRpqHjAiIkGqyTDi+wfGSl17R9w==",
1148 "license": "MIT",
1149 "dependencies": {
1150 "b4a": "^1.6.0",
1151 "signed-varint": "^2.0.1",
1152 "varint": "5.0.0"
1153 }
1154 },
1155 "node_modules/protomux": {
1156 "version": "3.10.1",
1157 "resolved": "https://registry.npmjs.org/protomux/-/protomux-3.10.1.tgz",
1158 "integrity": "sha512-jgBqx8ZyaBWea/DFG4eOu1scOaeBwcnagiRC1XFVrjeGt7oAb0Pk5udPpBUpJ4DJBRjra50jD6YcZiQQTRqaaA==",
1159 "license": "MIT",
1160 "dependencies": {
1161 "b4a": "^1.3.1",
1162 "compact-encoding": "^2.5.1",
1163 "queue-tick": "^1.0.0",
1164 "safety-catch": "^1.0.1",
1165 "unslab": "^1.3.0"
1166 }
1167 },
1168 "node_modules/protomux-wakeup": {
1169 "version": "2.9.0",
1170 "resolved": "https://registry.npmjs.org/protomux-wakeup/-/protomux-wakeup-2.9.0.tgz",
1171 "integrity": "sha512-K93WhS9qIRL8WAQPU2V3PxdpQ1oRlQCFyCyIYInoq/iRkcuO1BegRERWo7SS1Tcme14VKGWOr/IivjjYnHan3Q==",
1172 "license": "Apache-2.0",
1173 "dependencies": {
1174 "b4a": "^1.6.7",
1175 "hypercore-crypto": "^3.5.0",
1176 "hyperschema": "^1.10.4",
1177 "protomux": "^3.10.1"
1178 }
1179 },
1180 "node_modules/queue-tick": {
1181 "version": "1.0.1",
1182 "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
1183 "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
1184 "license": "MIT"
1185 },
1186 "node_modules/quickbit-native": {
1187 "version": "2.4.8",
1188 "resolved": "https://registry.npmjs.org/quickbit-native/-/quickbit-native-2.4.8.tgz",
1189 "integrity": "sha512-FcCcqI+nIAWGknqhtrYT5TSD7t/N+Xd8ctM+2PrIIBuwOi5hx0SxAvuPtzLIEMfT/2h9+fhBakUe2uALOHX6yw==",
1190 "license": "Apache-2.0",
1191 "optional": true,
1192 "dependencies": {
1193 "require-addon": "^1.1.0"
1194 }
1195 },
1196 "node_modules/quickbit-universal": {
1197 "version": "2.2.0",
1198 "resolved": "https://registry.npmjs.org/quickbit-universal/-/quickbit-universal-2.2.0.tgz",
1199 "integrity": "sha512-w02i1R8n7+6pEKTud8DfF8zbFY9o7RtPlUc3jWbtCkDKvhbx/AvV7oNnz4/TcmsPGpSJS+fq5Ud6RH6+YPvSGg==",
1200 "license": "ISC",
1201 "dependencies": {
1202 "b4a": "^1.6.0",
1203 "simdle-universal": "^1.1.0"
1204 },
1205 "optionalDependencies": {
1206 "quickbit-native": "^2.2.0"
1207 }
1208 },
1209 "node_modules/rache": {
1210 "version": "1.0.0",
1211 "resolved": "https://registry.npmjs.org/rache/-/rache-1.0.0.tgz",
1212 "integrity": "sha512-e0k0g0w/8jOCB+7YqCIlOa+OJ38k0wrYS4x18pMSmqOvLKoyhmMhmQyCcvfY6VaP8D75cqkEnlakXs+RYYLqNg==",
1213 "license": "Apache-2.0"
1214 },
1215 "node_modules/random-array-iterator": {
1216 "version": "1.0.0",
1217 "resolved": "https://registry.npmjs.org/random-array-iterator/-/random-array-iterator-1.0.0.tgz",
1218 "integrity": "sha512-u7xCM93XqKEvPTP6xZp2ehttcAemKnh73oKNf1FvzuVCfpt6dILDt1Kxl1LeBjm2iNIeR49VGFhy4Iz3yOun+Q==",
1219 "license": "MIT"
1220 },
1221 "node_modules/ready-resource": {
1222 "version": "1.2.0",
1223 "resolved": "https://registry.npmjs.org/ready-resource/-/ready-resource-1.2.0.tgz",
1224 "integrity": "sha512-nfcco/8iAFV0M+2PYnmIc+/xY0iRb35d42HFHQ7AfjulbGEAFa+XWpByfwSyeVeiBoMLLFVMv1HixxNCqzSQ1g==",
1225 "license": "MIT",
1226 "dependencies": {
1227 "bare-events": "^2.2.0"
1228 }
1229 },
1230 "node_modules/refcounter": {
1231 "version": "1.0.0",
1232 "resolved": "https://registry.npmjs.org/refcounter/-/refcounter-1.0.0.tgz",
1233 "integrity": "sha512-1WosVzUy0kPUaPMEtlNDwm99UsteALIhXXR8rerELoa63WkYIXAl0hxgwPFrIYBRWZPGUyekQ04FRtPJ7dHk9w==",
1234 "license": "Apache-2.0"
1235 },
1236 "node_modules/require-addon": {
1237 "version": "1.2.0",
1238 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
1239 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
1240 "license": "Apache-2.0",
1241 "dependencies": {
1242 "bare-addon-resolve": "^1.3.0"
1243 },
1244 "engines": {
1245 "bare": ">=1.10.0"
1246 }
1247 },
1248 "node_modules/resolve-reject-promise": {
1249 "version": "1.1.0",
1250 "resolved": "https://registry.npmjs.org/resolve-reject-promise/-/resolve-reject-promise-1.1.0.tgz",
1251 "integrity": "sha512-LWsTOA91AqzBTjSGgX79Tc130pwcBK6xjpJEO+qRT5IKZ6bGnHKcc8QL3upUBcWuU8OTIDzKK2VNSwmmlqvAVg==",
1252 "license": "MIT"
1253 },
1254 "node_modules/resource-on-exit": {
1255 "version": "1.0.0",
1256 "resolved": "https://registry.npmjs.org/resource-on-exit/-/resource-on-exit-1.0.0.tgz",
1257 "integrity": "sha512-ViJwJAknCkLRJRPR+9SISQQ7R5eRgtdIHLJsM2hHx1MweAJbJxJ5XnMjjq0Lc7ZGv44ufzAqds1nKxiVkdy4ag==",
1258 "license": "Apache-2.0"
1259 },
1260 "node_modules/rocksdb-native": {
1261 "version": "3.15.0",
1262 "resolved": "https://registry.npmjs.org/rocksdb-native/-/rocksdb-native-3.15.0.tgz",
1263 "integrity": "sha512-czYx0hOzKAy+HYmJROZm7thvJ1tHFWHNkGI3nomumXWzrJghJdrWo4MIhehGX4OTff0lnOCoB2zu6ekzIKY0rQ==",
1264 "license": "Apache-2.0",
1265 "dependencies": {
1266 "compact-encoding": "^2.15.0",
1267 "ready-resource": "^1.0.0",
1268 "refcounter": "^1.0.0",
1269 "require-addon": "^1.0.2",
1270 "resolve-reject-promise": "^1.1.0",
1271 "signal-promise": "^1.0.3",
1272 "streamx": "^2.16.1"
1273 },
1274 "engines": {
1275 "bare": ">=1.16.0"
1276 }
1277 },
1278 "node_modules/safety-catch": {
1279 "version": "1.0.3",
1280 "resolved": "https://registry.npmjs.org/safety-catch/-/safety-catch-1.0.3.tgz",
1281 "integrity": "sha512-Zq+J1TefpoEq/HTUabo0YXX5MNvttjWYODGohgPBO2jfko8Wqx3JYMgE823szDFVamdH5PlpByvfiWScTdSYDA==",
1282 "license": "MIT"
1283 },
1284 "node_modules/same-object": {
1285 "version": "1.0.2",
1286 "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz",
1287 "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==",
1288 "dev": true,
1289 "license": "MIT"
1290 },
1291 "node_modules/scope-lock": {
1292 "version": "1.2.4",
1293 "resolved": "https://registry.npmjs.org/scope-lock/-/scope-lock-1.2.4.tgz",
1294 "integrity": "sha512-BpSd8VCuCxW9ZitcdIC/vjs3gMaP9bRBL5nkHcyfX2VrS52n13/rHuBA2xJ/S/4DPuRdAO/Bk8pWd8eD/gHCIA==",
1295 "license": "Apache-2.0"
1296 },
1297 "node_modules/signal-promise": {
1298 "version": "1.0.3",
1299 "resolved": "https://registry.npmjs.org/signal-promise/-/signal-promise-1.0.3.tgz",
1300 "integrity": "sha512-WBgv0UnIq2C+Aeh0/n+IRpP6967eIx9WpynTUoiW3isPpfe1zu2LJzyfXdo9Tgef8yR/sGjcMvoUXD7EYdiz+g==",
1301 "license": "MIT"
1302 },
1303 "node_modules/signed-varint": {
1304 "version": "2.0.1",
1305 "resolved": "https://registry.npmjs.org/signed-varint/-/signed-varint-2.0.1.tgz",
1306 "integrity": "sha512-abgDPg1106vuZZOvw7cFwdCABddfJRz5akcCcchzTbhyhYnsG31y4AlZEgp315T7W3nQq5P4xeOm186ZiPVFzw==",
1307 "license": "MIT",
1308 "dependencies": {
1309 "varint": "~5.0.0"
1310 }
1311 },
1312 "node_modules/simdle-native": {
1313 "version": "1.3.9",
1314 "resolved": "https://registry.npmjs.org/simdle-native/-/simdle-native-1.3.9.tgz",
1315 "integrity": "sha512-Isc8sP4OiiIU0mpslD4GHEnR0VQWvR/54WN7YtwEDkNdTJVWtpmvsSvsgRlw5BNGxdYXlVRegdnrSu10H/PhvA==",
1316 "license": "Apache-2.0",
1317 "optional": true,
1318 "dependencies": {
1319 "b4a": "^1.6.0",
1320 "require-addon": "^1.1.0"
1321 }
1322 },
1323 "node_modules/simdle-universal": {
1324 "version": "1.1.2",
1325 "resolved": "https://registry.npmjs.org/simdle-universal/-/simdle-universal-1.1.2.tgz",
1326 "integrity": "sha512-3n3w1bs+uwgHKQjt6arez83EywNlhZzYvNOhvAASTl/8KqNIcqr6aHyGt3JRlfuUC7iB0tomJRPlJ2cRGIpBzA==",
1327 "license": "ISC",
1328 "dependencies": {
1329 "b4a": "^1.6.0"
1330 },
1331 "optionalDependencies": {
1332 "simdle-native": "^1.1.1"
1333 }
1334 },
1335 "node_modules/sodium-native": {
1336 "version": "5.1.0",
1337 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.1.0.tgz",
1338 "integrity": "sha512-3RxgyWyJlhTsABPnJVpCI5CoTDANZTqqFrEPqr+kjfnRaBihpVtMUE3yTF40ukdoB1APXeoBNKF3MzZAIHg39g==",
1339 "license": "MIT",
1340 "dependencies": {
1341 "bare-assert": "^1.2.0",
1342 "require-addon": "^1.1.0",
1343 "which-runtime": "^1.2.1"
1344 },
1345 "engines": {
1346 "bare": ">=1.16.0"
1347 }
1348 },
1349 "node_modules/sodium-secretstream": {
1350 "version": "1.2.0",
1351 "resolved": "https://registry.npmjs.org/sodium-secretstream/-/sodium-secretstream-1.2.0.tgz",
1352 "integrity": "sha512-q/DbraNFXm1KfCiiZvapmz5UC3OlpirYFIvBK2MhGaOFSb3gRyk8OXTi17UI9SGfshQNCpsVvlopogbzZNyW6Q==",
1353 "license": "MIT",
1354 "dependencies": {
1355 "b4a": "^1.1.1",
1356 "sodium-universal": "^5.0.0"
1357 }
1358 },
1359 "node_modules/sodium-universal": {
1360 "version": "5.0.1",
1361 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1362 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1363 "license": "MIT",
1364 "dependencies": {
1365 "sodium-native": "^5.0.1"
1366 },
1367 "peerDependencies": {
1368 "sodium-javascript": "~0.8.0"
1369 },
1370 "peerDependenciesMeta": {
1371 "sodium-javascript": {
1372 "optional": true
1373 }
1374 }
1375 },
1376 "node_modules/stackframe": {
1377 "version": "1.3.4",
1378 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
1379 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
1380 "dev": true,
1381 "license": "MIT"
1382 },
1383 "node_modules/streamx": {
1384 "version": "2.25.0",
1385 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
1386 "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
1387 "license": "MIT",
1388 "dependencies": {
1389 "events-universal": "^1.0.0",
1390 "fast-fifo": "^1.3.2",
1391 "text-decoder": "^1.1.0"
1392 }
1393 },
1394 "node_modules/sub-encoder": {
1395 "version": "2.1.3",
1396 "resolved": "https://registry.npmjs.org/sub-encoder/-/sub-encoder-2.1.3.tgz",
1397 "integrity": "sha512-Xxx04ygZo/1J3yHvaSA6VhDmiSaBQkw/PmO3YnnYFXle+tfOGToC6FcDpIfMztWZXJzuKG14b/57HMkiL58C6A==",
1398 "license": "Apache-2.0",
1399 "dependencies": {
1400 "b4a": "^1.6.0",
1401 "codecs": "^3.1.0"
1402 }
1403 },
1404 "node_modules/teex": {
1405 "version": "1.0.1",
1406 "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
1407 "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
1408 "license": "MIT",
1409 "dependencies": {
1410 "streamx": "^2.12.5"
1411 }
1412 },
1413 "node_modules/test-tmp": {
1414 "version": "1.4.0",
1415 "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz",
1416 "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==",
1417 "dev": true,
1418 "license": "MIT",
1419 "dependencies": {
1420 "bare-fs": "^4.0.1",
1421 "bare-os": "^3.3.0",
1422 "bare-path": "^3.0.0"
1423 }
1424 },
1425 "node_modules/text-decoder": {
1426 "version": "1.2.7",
1427 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
1428 "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
1429 "license": "Apache-2.0",
1430 "dependencies": {
1431 "b4a": "^1.6.4"
1432 }
1433 },
1434 "node_modules/timeout-refresh": {
1435 "version": "2.0.1",
1436 "resolved": "https://registry.npmjs.org/timeout-refresh/-/timeout-refresh-2.0.1.tgz",
1437 "integrity": "sha512-SVqEcMZBsZF9mA78rjzCrYrUs37LMJk3ShZ851ygZYW1cMeIjs9mL57KO6Iv5mmjSQnOe/29/VAfGXo+oRCiVw==",
1438 "license": "MIT"
1439 },
1440 "node_modules/tiny-buffer-map": {
1441 "version": "1.1.1",
1442 "resolved": "https://registry.npmjs.org/tiny-buffer-map/-/tiny-buffer-map-1.1.1.tgz",
1443 "integrity": "sha512-C1eDw6ks9CmkDbWVCPHobuixPTkxGa7IDERlaVk98dv4tOUdz42o3haHBr0uhNxbj0gczBTVIyS2uQsu+1vc2Q==",
1444 "license": "MIT",
1445 "dependencies": {
1446 "b4a": "^1.6.0"
1447 }
1448 },
1449 "node_modules/tmatch": {
1450 "version": "5.0.0",
1451 "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz",
1452 "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==",
1453 "dev": true,
1454 "license": "ISC",
1455 "engines": {
1456 "node": ">=8"
1457 }
1458 },
1459 "node_modules/unslab": {
1460 "version": "1.3.0",
1461 "resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz",
1462 "integrity": "sha512-YATkfKAFj47kTzmiQrWXMyRvaVrHsW6MEALa4bm+FhiA2YG4oira+Z3DXN6LrYOYn2Y8eO94Lwl9DOHjs1FpoQ==",
1463 "license": "Apache-2.0",
1464 "dependencies": {
1465 "b4a": "^1.6.6"
1466 }
1467 },
1468 "node_modules/v8-to-istanbul": {
1469 "version": "9.3.0",
1470 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
1471 "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
1472 "dev": true,
1473 "license": "ISC",
1474 "dependencies": {
1475 "@jridgewell/trace-mapping": "^0.3.12",
1476 "@types/istanbul-lib-coverage": "^2.0.1",
1477 "convert-source-map": "^2.0.0"
1478 },
1479 "engines": {
1480 "node": ">=10.12.0"
1481 }
1482 },
1483 "node_modules/varint": {
1484 "version": "5.0.0",
1485 "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz",
1486 "integrity": "sha512-gC13b/bWrqQoKY2EmROCZ+AR0jitc6DnDGaQ6Ls9QpKmuSgJB1eQ7H3KETtQm7qSdMWMKCmsshyCmUwMLh3OAA==",
1487 "license": "MIT"
1488 },
1489 "node_modules/which-runtime": {
1490 "version": "1.3.2",
1491 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
1492 "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
1493 "license": "Apache-2.0"
1494 },
1495 "node_modules/z32": {
1496 "version": "1.1.0",
1497 "resolved": "https://registry.npmjs.org/z32/-/z32-1.1.0.tgz",
1498 "integrity": "sha512-1WUHy+VS6d0HPNspDxvLssBbeQjXMjSnpv0vH82vRAUfg847NmX3OXozp/hRP5jPhxBbrVzrgvAt+UsGNzRFQQ==",
1499 "license": "MIT",
1500 "dependencies": {
1501 "b4a": "^1.5.3"
1502 }
1503 }
1504 }
1505}
package.json +22 -0
……@@ -0,0 +1,22 @@
1{
2 "name": "hyper-rooms",
3 "version": "0.1.0",
4 "description": "Multiwriter rooms on Autobase with pluggable schemas — the substrate for hyper-issues, hyper-orgs, etc.",
5 "type": "module",
6 "main": "index.js",
7 "scripts": {
8 "test": "brittle test/*.test.js"
9 },
10 "dependencies": {
11 "autobase": "^7.20.0",
12 "b4a": "^1.6.0",
13 "hyperbee": "^2.20.0",
14 "hypercore-crypto": "^3.4.0",
15 "ready-resource": "^1.1.0"
16 },
17 "devDependencies": {
18 "autobase-test-helpers": "^2.0.0",
19 "brittle": "^3.0.0",
20 "corestore": "^7.0.0"
21 }
22}
test/room.test.js +310 -0
……@@ -0,0 +1,310 @@
1import test from 'brittle'
2import Corestore from 'corestore'
3import b4a from 'b4a'
4import { replicateAndSync } from 'autobase-test-helpers'
5import { Room, jsonSchema } from '../index.js'
6
7// ── Test fixture: a tiny "todos" schema ──
8
9const todosSchema = jsonSchema({
10 async apply (batch, op, host) {
11 if (op.type === 'add-todo') {
12 await batch.put(`todo:${op.id}`, {
13 id: op.id,
14 text: op.text,
15 done: false,
16 author: op.author || null,
17 createdAt: op.timestamp || Date.now()
18 })
19 } else if (op.type === 'check-todo') {
20 const entry = await batch.get(`todo:${op.id}`)
21 if (entry) {
22 await batch.put(`todo:${op.id}`, { ...entry.value, done: true })
23 }
24 } else if (op.type === 'delete-todo') {
25 await batch.del(`todo:${op.id}`)
26 }
27 },
28 validate (op) {
29 if (op.type === 'add-todo') {
30 if (typeof op.id !== 'number') throw new Error('add-todo: id must be number')
31 if (typeof op.text !== 'string') throw new Error('add-todo: text must be string')
32 }
33 }
34})
35
36async function makeRoom (t, opts = {}) {
37 const dir = '/tmp/hyper-rooms-test-' + Math.random().toString(36).slice(2)
38 const store = new Corestore(dir)
39 await store.ready()
40 const room = new Room(store, { schema: todosSchema, namespace: 'test', ...opts })
41 await room.ready()
42 t.teardown(async () => {
43 try { await room.close() } catch {}
44 try { await store.close() } catch {}
45 })
46 return { room, store, dir }
47}
48
49// ── Single-writer (basic) ──────────────────────────────────────
50
51test('single-writer: append → view round-trip', async (t) => {
52 const { room } = await makeRoom(t)
53
54 await room.append({ type: 'add-todo', id: 1, text: 'buy milk' })
55 await room.append({ type: 'add-todo', id: 2, text: 'walk dog' })
56 await room.append({ type: 'check-todo', id: 1 })
57 await room.base.update()
58
59 const t1 = await room.view.get('todo:1')
60 const t2 = await room.view.get('todo:2')
61
62 t.is(t1.value.text, 'buy milk')
63 t.is(t1.value.done, true, 'check-todo applied')
64 t.is(t2.value.text, 'walk dog')
65 t.is(t2.value.done, false)
66})
67
68test('append + delete', async (t) => {
69 const { room } = await makeRoom(t)
70
71 await room.append({ type: 'add-todo', id: 1, text: 'temporary' })
72 await room.append({ type: 'delete-todo', id: 1 })
73 await room.base.update()
74
75 const t1 = await room.view.get('todo:1')
76 t.absent(t1, 'deleted')
77})
78
79test('schema validation rejects bad ops', async (t) => {
80 const { room } = await makeRoom(t)
81
82 await t.exception(
83 room.append({ type: 'add-todo', text: 'no id' }),
84 /id must be number/
85 )
86
87 await t.exception(
88 room.append({ type: 'add-todo', id: 1, text: 42 }),
89 /text must be string/
90 )
91
92 await t.exception(
93 room.append({ }), // no type
94 /op\.type must be a string/
95 )
96})
97
98test('append rejects writer ops directly', async (t) => {
99 const { room } = await makeRoom(t)
100 await t.exception(
101 room.append({ type: 'add-writer', userId: 'x' }),
102 /Use addWriter/
103 )
104})
105
106test('persistence across reopen', async (t) => {
107 const dir = '/tmp/hyper-rooms-persist-' + Math.random().toString(36).slice(2)
108
109 const store1 = new Corestore(dir)
110 await store1.ready()
111 const room1 = new Room(store1, { schema: todosSchema, namespace: 'test' })
112 await room1.ready()
113
114 const key = room1.keyHex
115 await room1.append({ type: 'add-todo', id: 99, text: 'persistent' })
116 await room1.base.update()
117
118 await room1.close()
119 await store1.close()
120
121 // Reopen
122 const store2 = new Corestore(dir)
123 await store2.ready()
124 const room2 = new Room(store2, { schema: todosSchema, namespace: 'test' })
125 await room2.ready()
126
127 t.is(room2.keyHex, key, 'same room key after reopen')
128
129 const entry = await room2.view.get('todo:99')
130 t.ok(entry, 'todo persisted')
131 t.is(entry.value.text, 'persistent')
132
133 await room2.close()
134 await store2.close()
135})
136
137// ── Writer management ─────────────────────────────────────────
138
139test('addWriter / listWriters / removeWriter', async (t) => {
140 const { room } = await makeRoom(t)
141
142 // Use a fake writer key (32 bytes hex)
143 const aliceKey = '11'.repeat(32)
144 const bobKey = '22'.repeat(32)
145
146 await room.addWriter('alice', { writerKey: aliceKey, role: 'owner' })
147 await room.addWriter('bob', { writerKey: bobKey, role: 'writer' })
148 await room.base.update()
149
150 const writers = await room.listWriters()
151 t.is(writers.length, 2)
152 t.ok(writers.find(w => w.userId === 'alice' && w.role === 'owner'))
153 t.ok(writers.find(w => w.userId === 'bob' && w.role === 'writer'))
154
155 const alice = await room.getWriter('alice')
156 t.is(alice.role, 'owner')
157 t.is(alice.writerKey, aliceKey)
158
159 await room.removeWriter('bob')
160 await room.base.update()
161
162 const after = await room.listWriters()
163 t.is(after.length, 1)
164 t.is(after[0].userId, 'alice')
165})
166
167test('addWriter rejects bad role', async (t) => {
168 const { room } = await makeRoom(t)
169 await t.exception(
170 room.addWriter('x', { writerKey: '11'.repeat(32), role: 'admin' }),
171 /role must be/
172 )
173})
174
175test('addWriter requires writerKey', async (t) => {
176 const { room } = await makeRoom(t)
177 await t.exception(
178 room.addWriter('x', { role: 'writer' }),
179 /requires opts\.writerKey/
180 )
181})
182
183// ── Multi-writer replication ──────────────────────────────────
184
185test('two rooms can replicate via stream pair', async (t) => {
186 // Room A creates the room, room B joins via the bootstrap key.
187 // Uses the canonical autobase replication pattern: base.replicate() +
188 // replicateAndSync() helper that polls until heads converge.
189 const dirA = '/tmp/hyper-rooms-mwA-' + Math.random().toString(36).slice(2)
190 const dirB = '/tmp/hyper-rooms-mwB-' + Math.random().toString(36).slice(2)
191
192 const storeA = new Corestore(dirA)
193 const storeB = new Corestore(dirB)
194 await storeA.ready()
195 await storeB.ready()
196
197 // A creates the room
198 const roomA = new Room(storeA, { schema: todosSchema, namespace: 'shared' })
199 await roomA.ready()
200 const bootstrapKey = roomA.keyHex
201
202 // A appends a todo
203 await roomA.append({ type: 'add-todo', id: 1, text: 'from A' })
204 await roomA.base.update()
205
206 // B joins the same room via the bootstrap key
207 const roomB = new Room(storeB, {
208 schema: todosSchema,
209 namespace: 'shared',
210 bootstrapKey
211 })
212 await roomB.ready()
213
214 // Sync both bases using the canonical helper.
215 // (This sets up replication streams and polls until heads converge.)
216 await replicateAndSync([roomA.base, roomB.base])
217
218 // B should now see A's todo
219 const entry = await roomB.view.get('todo:1')
220 t.ok(entry, 'B received A\'s todo via replication')
221 t.is(entry.value.text, 'from A')
222
223 t.teardown(async () => {
224 try { await roomA.close() } catch {}
225 try { await roomB.close() } catch {}
226 try { await storeA.close() } catch {}
227 try { await storeB.close() } catch {}
228 })
229})
230
231test('two writers: B added by A, both append, both see all', async (t) => {
232 // Full multi-writer test: A adds B as a writer, B appends, both see both.
233 const dirA = '/tmp/hyper-rooms-2wA-' + Math.random().toString(36).slice(2)
234 const dirB = '/tmp/hyper-rooms-2wB-' + Math.random().toString(36).slice(2)
235
236 const storeA = new Corestore(dirA)
237 const storeB = new Corestore(dirB)
238 await storeA.ready()
239 await storeB.ready()
240
241 const roomA = new Room(storeA, { schema: todosSchema, namespace: 'shared' })
242 await roomA.ready()
243 const bootstrapKey = roomA.keyHex
244
245 const roomB = new Room(storeB, {
246 schema: todosSchema,
247 namespace: 'shared',
248 bootstrapKey
249 })
250 await roomB.ready()
251
252 // First sync so B is connected and knows about the room structure
253 await replicateAndSync([roomA.base, roomB.base])
254
255 // A adds B as a writer (using B's local writer key)
256 await roomA.addWriter('bob', { writerKey: roomB.localKeyHex, role: 'writer' })
257 await replicateAndSync([roomA.base, roomB.base])
258
259 // B should now be writable
260 t.ok(roomB.writable, 'B is now writable after addWriter')
261
262 // Both append
263 await roomA.append({ type: 'add-todo', id: 1, text: 'from A' })
264 await roomB.append({ type: 'add-todo', id: 2, text: 'from B' })
265
266 // Sync and verify both see both
267 await replicateAndSync([roomA.base, roomB.base])
268
269 const entry1A = await roomA.view.get('todo:1')
270 const entry2A = await roomA.view.get('todo:2')
271 const entry1B = await roomB.view.get('todo:1')
272 const entry2B = await roomB.view.get('todo:2')
273
274 t.is(entry1A.value.text, 'from A', 'A sees own todo')
275 t.is(entry2A.value.text, 'from B', 'A sees B\'s todo')
276 t.is(entry1B.value.text, 'from A', 'B sees A\'s todo')
277 t.is(entry2B.value.text, 'from B', 'B sees own todo')
278
279 // Both should also see the writer entry
280 const writers = await roomA.listWriters()
281 t.ok(writers.find(w => w.userId === 'bob'), 'A has bob in writers')
282
283 t.teardown(async () => {
284 try { await roomA.close() } catch {}
285 try { await roomB.close() } catch {}
286 try { await storeA.close() } catch {}
287 try { await storeB.close() } catch {}
288 })
289})
290
291// ── jsonSchema helper ─────────────────────────────────────────
292
293test('jsonSchema requires an apply function', (t) => {
294 t.exception(() => jsonSchema({}), /requires an apply function/)
295})
296
297test('jsonSchema validateOp catches non-object ops', async (t) => {
298 const schema = jsonSchema({ apply: async () => {} })
299 t.exception(() => schema.validateOp(null), /op must be an object/)
300 t.exception(() => schema.validateOp({}), /op\.type must be a string/)
301})
302
303test('Room rejects bad schema', (t) => {
304 const store = new Corestore('/tmp/hyper-rooms-bad-' + Math.random().toString(36).slice(2))
305 t.exception(
306 () => new Room(store, { schema: { encodeOp: () => {} } }),
307 /Schema missing required function/
308 )
309 t.exception(() => new Room(store, {}), /Room requires a schema/)
310})