OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-orgs commits / e0e9e49

Initial commit — hyper-orgs
P2P organization management as a hyper-rooms schema. Models orgs (user + group types), members, resources, grants, invites, and access requests with index keys for fast prefix-scan lookups. Ops: create-org, set-org-type, add-member, remove-member, add-resource, remove-resource, move-resource, add-grant, revoke-grant, invite, accept-invite, revoke-invite, request-access, grant-access, deny-access. add-resource performs cross-org cleanup in apply so 'move by re-adding' behaves the same as the explicit move-resource op. Content-derived IDs sidestep the auto-increment collision gotcha that affects hyper-issues. Renamed from the v2 namespace-centric naming: namespace:→org:, add-repo→add-resource, nsType→orgType, grantType=repo→=resource, etc. 29 tests / 78 asserts passing. Phase 5 of the pear-git-v3 bottom-up rebuild. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
e0e9e497d61e9ca070470d9465f407d581c3d0de
by plankatron <fleeky@gmail.com> · 2026-04-15T03:41:09.000Z

Changed files (9) +3865 -0

.gitignore +15 -0
……@@ -0,0 +1,15 @@
1# Dependencies
2node_modules/
3
4# Storage directories created by example CLIs
5.hyper-orgs-*/
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 +350 -0
……@@ -0,0 +1,350 @@
1#!/usr/bin/env node
2/**
3 * Mini CLI example for hyper-orgs
4 *
5 * Usage:
6 * node examples/cli.js init Create an orgs store
7 * node examples/cli.js info Show key + counts + writers
8 * node examples/cli.js create <name> [group|user] Create an org
9 * node examples/cli.js list [group|user|all] List orgs
10 * node examples/cli.js show <name> Show org details
11 * node examples/cli.js add-member <org> <userId> [role] Add a member
12 * node examples/cli.js rm-member <org> <userId> Remove a member
13 * node examples/cli.js add-resource <org> <resource> Attach a resource
14 * node examples/cli.js rm-resource <org> <resource> Detach a resource
15 * node examples/cli.js grant <subject> <org> [perm] Grant a user access to an org
16 * node examples/cli.js invite <org> [perm] Create an invite
17 * node examples/cli.js request <org> <userId> [perm] Request access
18 * node examples/cli.js check <userId> <resource> Check access
19 * node examples/cli.js delete <name> Delete empty org
20 * node examples/cli.js demo Run a full demo
21 *
22 * Storage: HYPER_ORGS_STORE env var (default: ./.hyper-orgs-cli)
23 *
24 * NOTE: this CLI uses its own storage path. The web example uses a different
25 * default. They don't share state — RocksDB single-writer lock.
26 */
27
28import Corestore from 'corestore'
29import { HyperOrgs } from '../index.js'
30
31const storagePath = process.env.HYPER_ORGS_STORE || './.hyper-orgs-cli'
32const actor = process.env.USER || 'me'
33const args = process.argv.slice(2)
34const cmd = args[0]
35
36async function open () {
37 const store = new Corestore(storagePath)
38 await store.ready()
39 const orgs = new HyperOrgs(store, { namespace: 'orgs' })
40 await orgs.ready()
41 return { orgs, store }
42}
43
44async function close ({ orgs, store }) {
45 await orgs.close()
46 await store.close()
47}
48
49function printOrg (org) {
50 console.log('Org: %s (%s)', org.name, org.type)
51 console.log(' displayName: %s', org.displayName)
52 if (org.description) console.log(' description: %s', org.description)
53 console.log(' visibility: %s', org.visibility)
54 if (org.type === 'user' && org.userId) console.log(' userId: %s', org.userId)
55 if (org.members.length > 0) {
56 console.log(' members:')
57 for (const m of org.members) console.log(' - %s (%s)', m.userId, m.role)
58 }
59 if (org.resources.length > 0) {
60 console.log(' resources: %s', org.resources.join(', '))
61 }
62}
63
64async function main () {
65 if (cmd === 'init') {
66 const ctx = await open()
67 console.log('HyperOrgs initialized at: %s', storagePath)
68 console.log(' key: %s', ctx.orgs.keyHex)
69 console.log(' localKey: %s', ctx.orgs.localKeyHex)
70 console.log(' writable: %s', ctx.orgs.writable)
71 await close(ctx)
72 return
73 }
74
75 if (cmd === 'info') {
76 const ctx = await open()
77 const all = await ctx.orgs.list()
78 const writers = await ctx.orgs.listWriters()
79 const grants = await ctx.orgs.listGrants()
80 const invites = await ctx.orgs.listInvites()
81 const requests = await ctx.orgs.listAccessRequests()
82 console.log('HyperOrgs: %s', storagePath)
83 console.log(' key: %s', ctx.orgs.keyHex)
84 console.log(' localKey: %s', ctx.orgs.localKeyHex)
85 console.log(' writable: %s', ctx.orgs.writable)
86 console.log(' orgs: %d', all.length)
87 console.log(' writers: %d', writers.length)
88 console.log(' grants: %d', grants.length)
89 console.log(' invites: %d', invites.length)
90 console.log(' requests: %d', requests.length)
91 await close(ctx)
92 return
93 }
94
95 if (cmd === 'create') {
96 const name = args[1]
97 const type = args[2] || 'group'
98 if (!name) { console.error('Usage: cli.js create <name> [group|user]'); process.exit(1) }
99 const ctx = await open()
100 const org = await ctx.orgs.create(name, type, {
101 userId: type === 'user' ? actor : null
102 })
103 console.log('Created %s "%s"', type, name)
104 printOrg(org)
105 await close(ctx)
106 return
107 }
108
109 if (cmd === 'list') {
110 const filter = args[1]
111 const opts = {}
112 if (filter === 'group' || filter === 'user') opts.type = filter
113 const ctx = await open()
114 const all = await ctx.orgs.list(opts)
115 if (all.length === 0) {
116 console.log('(no orgs)')
117 } else {
118 for (const o of all) {
119 const icon = o.type === 'user' ? 'user' : 'group'
120 const name = (o.name + ' '.repeat(20)).slice(0, 20)
121 console.log('[%s] %s %s', icon, name, o.displayName)
122 }
123 console.log('\n%d orgs', all.length)
124 }
125 await close(ctx)
126 return
127 }
128
129 if (cmd === 'show') {
130 const name = args[1]
131 if (!name) { console.error('Usage: cli.js show <name>'); process.exit(1) }
132 const ctx = await open()
133 const org = await ctx.orgs.get(name)
134 if (!org) {
135 console.error('Org "%s" not found', name)
136 await close(ctx)
137 process.exit(1)
138 }
139 printOrg(org)
140 await close(ctx)
141 return
142 }
143
144 if (cmd === 'add-member') {
145 const [_, org, userId, role = 'member'] = args
146 if (!org || !userId) { console.error('Usage: cli.js add-member <org> <userId> [role]'); process.exit(1) }
147 const ctx = await open()
148 const updated = await ctx.orgs.addMember(org, userId, role)
149 console.log('Added %s as %s to %s', userId, role, org)
150 printOrg(updated)
151 await close(ctx)
152 return
153 }
154
155 if (cmd === 'rm-member') {
156 const [_, org, userId] = args
157 if (!org || !userId) { console.error('Usage: cli.js rm-member <org> <userId>'); process.exit(1) }
158 const ctx = await open()
159 await ctx.orgs.removeMember(org, userId)
160 console.log('Removed %s from %s', userId, org)
161 await close(ctx)
162 return
163 }
164
165 if (cmd === 'add-resource') {
166 const [_, org, resource] = args
167 if (!org || !resource) { console.error('Usage: cli.js add-resource <org> <resource>'); process.exit(1) }
168 const ctx = await open()
169 const updated = await ctx.orgs.addResource(org, resource)
170 console.log('Added resource %s to %s', resource, org)
171 printOrg(updated)
172 await close(ctx)
173 return
174 }
175
176 if (cmd === 'rm-resource') {
177 const [_, org, resource] = args
178 if (!org || !resource) { console.error('Usage: cli.js rm-resource <org> <resource>'); process.exit(1) }
179 const ctx = await open()
180 await ctx.orgs.removeResource(org, resource)
181 console.log('Removed resource %s from %s', resource, org)
182 await close(ctx)
183 return
184 }
185
186 if (cmd === 'grant') {
187 const [_, subject, org, permission = 'write'] = args
188 if (!subject || !org) { console.error('Usage: cli.js grant <subject> <org> [perm]'); process.exit(1) }
189 const ctx = await open()
190 const grant = await ctx.orgs.createGrant({
191 subjectType: 'user',
192 subjectId: subject,
193 grantType: 'group',
194 groupName: org,
195 permission,
196 grantedBy: actor
197 })
198 console.log('Grant %s: %s → %s (%s)', grant.grantId, subject, org, permission)
199 await close(ctx)
200 return
201 }
202
203 if (cmd === 'invite') {
204 const [_, org, permission = 'write'] = args
205 if (!org) { console.error('Usage: cli.js invite <org> [perm]'); process.exit(1) }
206 const ctx = await open()
207 const invite = await ctx.orgs.createInvite({
208 inviteType: 'group',
209 targetGroup: org,
210 permission,
211 maxUses: 1,
212 createdBy: actor
213 })
214 console.log('Invite %s: %s (%s, 1 use)', invite.inviteId, org, permission)
215 console.log('Share this inviteId with the target subject.')
216 await close(ctx)
217 return
218 }
219
220 if (cmd === 'request') {
221 const [_, org, userId, permission = 'write'] = args
222 if (!org || !userId) { console.error('Usage: cli.js request <org> <userId> [perm]'); process.exit(1) }
223 const ctx = await open()
224 const req = await ctx.orgs.createAccessRequest({
225 subjectType: 'user',
226 subjectId: userId,
227 requestType: 'group',
228 groupName: org,
229 permission
230 })
231 console.log('Request %s: %s → %s (%s) status=%s', req.requestId, userId, org, permission, req.status)
232 await close(ctx)
233 return
234 }
235
236 if (cmd === 'check') {
237 const [_, userId, resource] = args
238 if (!userId || !resource) { console.error('Usage: cli.js check <userId> <resource>'); process.exit(1) }
239 const ctx = await open()
240 const allowed = await ctx.orgs.checkAccess({
241 userId,
242 resourceName: resource,
243 requiredPermission: 'write'
244 })
245 console.log('%s can write %s? %s', userId, resource, allowed ? 'YES' : 'NO')
246 await close(ctx)
247 return
248 }
249
250 if (cmd === 'delete') {
251 const name = args[1]
252 if (!name) { console.error('Usage: cli.js delete <name>'); process.exit(1) }
253 const ctx = await open()
254 await ctx.orgs.delete(name)
255 console.log('Deleted %s', name)
256 await close(ctx)
257 return
258 }
259
260 if (cmd === 'demo') {
261 const ctx = await open()
262 console.log('--- hyper-orgs demo ---\n')
263
264 console.log('1. Create a user org and a group org')
265 await ctx.orgs.create('alice', 'user', { userId: 'alice-key-hex' })
266 await ctx.orgs.create('my-team', 'group', {
267 displayName: 'My Team',
268 description: 'A test team'
269 })
270
271 console.log('2. Add members to the group')
272 await ctx.orgs.addMember('my-team', 'alice', 'admin')
273 await ctx.orgs.addMember('my-team', 'bob', 'member')
274
275 console.log('3. Attach two resources to the team')
276 await ctx.orgs.addResource('my-team', 'project-a')
277 await ctx.orgs.addResource('my-team', 'project-b')
278
279 console.log('4. Grant alice "write" on the whole team')
280 await ctx.orgs.createGrant({
281 subjectType: 'user', subjectId: 'alice',
282 grantType: 'group', groupName: 'my-team',
283 permission: 'write',
284 grantedBy: 'demo'
285 })
286
287 console.log('5. Create an invite for anyone who wants to join')
288 const invite = await ctx.orgs.createInvite({
289 inviteType: 'group', targetGroup: 'my-team',
290 permission: 'write', maxUses: 3,
291 createdBy: 'demo'
292 })
293 console.log(' invite: %s', invite.inviteId)
294
295 console.log('6. Someone requests access')
296 const req = await ctx.orgs.createAccessRequest({
297 subjectType: 'user', subjectId: 'carol',
298 requestType: 'group', groupName: 'my-team',
299 permission: 'write', reason: 'want to help'
300 })
301
302 console.log('7. Admin approves the request')
303 await ctx.orgs.approveAccessRequest(req.requestId, 'demo')
304
305 console.log('\n8. Summary:')
306 const team = await ctx.orgs.get('my-team')
307 printOrg(team)
308 console.log()
309
310 const allowed = await ctx.orgs.checkAccess({
311 userId: 'alice',
312 resourceName: 'project-a',
313 requiredPermission: 'write'
314 })
315 console.log('alice can write project-a? %s', allowed ? 'YES' : 'NO')
316
317 const carolAllowed = await ctx.orgs.checkAccess({
318 userId: 'carol',
319 resourceName: 'project-a',
320 requiredPermission: 'write'
321 })
322 console.log('carol can write project-a? %s', carolAllowed ? 'YES' : 'NO')
323
324 await close(ctx)
325 return
326 }
327
328 console.log('hyper-orgs example CLI')
329 console.log(' init Create an orgs store')
330 console.log(' info Show key + counts + writers')
331 console.log(' create <name> [group|user] Create an org')
332 console.log(' list [group|user|all] List orgs')
333 console.log(' show <name> Show org details')
334 console.log(' add-member <org> <userId> [role] Add a member')
335 console.log(' rm-member <org> <userId> Remove a member')
336 console.log(' add-resource <org> <resource> Attach a resource')
337 console.log(' rm-resource <org> <resource> Detach a resource')
338 console.log(' grant <subject> <org> [perm] Grant access to an org')
339 console.log(' invite <org> [perm] Create an invite')
340 console.log(' request <org> <userId> [perm] Request access')
341 console.log(' check <userId> <resource> Check access')
342 console.log(' delete <name> Delete empty org')
343 console.log(' demo Run a full demo')
344 console.log('')
345 console.log('Storage: HYPER_ORGS_STORE env var (default: ./.hyper-orgs-cli)')
346 console.log('Note: this CLI does NOT share state with examples/web.js — they use')
347 console.log('different storage paths because RocksDB only allows one writer.')
348}
349
350main().catch((err) => { console.error(err.message); process.exit(1) })
examples/web.js +314 -0
……@@ -0,0 +1,314 @@
1#!/usr/bin/env node
2/**
3 * Mini web example for hyper-orgs
4 *
5 * SSR org browser. Pages:
6 * GET / → list all orgs + create-org form
7 * GET /:name → show org + members + resources + grant form
8 * POST /create → create an org
9 * POST /:name/member → add a member
10 * POST /:name/resource → add a resource
11 * POST /:name/grant → create a group grant for a user
12 *
13 * Usage: node examples/web.js [--port 9097] [--storage <path>]
14 *
15 * NOTE: Uses its own storage path by default (./.hyper-orgs-web). Does NOT
16 * share state with examples/cli.js — RocksDB single-writer lock.
17 */
18
19import http from 'http'
20import Corestore from 'corestore'
21import b4a from 'b4a'
22import { HyperOrgs } from '../index.js'
23
24const args = process.argv.slice(2)
25function getFlag (name) {
26 const i = args.indexOf('--' + name)
27 return i !== -1 && args[i + 1] ? args[i + 1] : null
28}
29
30const port = parseInt(getFlag('port')) || 9097
31const storagePath = getFlag('storage') || './.hyper-orgs-web'
32
33const store = new Corestore(storagePath)
34await store.ready()
35const orgs = new HyperOrgs(store, { namespace: 'orgs' })
36await orgs.ready()
37
38function escapeHtml (s) {
39 return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
40}
41
42function collectBody (req) {
43 return new Promise((resolve, reject) => {
44 const parts = []
45 req.on('data', chunk => parts.push(chunk))
46 req.on('end', () => resolve(b4a.concat(parts).toString('utf8')))
47 req.on('error', reject)
48 })
49}
50
51const httpServer = http.createServer(async (req, res) => {
52 const url = req.url.split('?')[0]
53 try {
54 if (req.method === 'GET' && url === '/') {
55 const all = await orgs.list()
56 res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
57 res.end(renderIndex(all))
58 return
59 }
60
61 const showMatch = url.match(/^\/([a-z0-9][a-z0-9-]*[a-z0-9])$/)
62 if (req.method === 'GET' && showMatch) {
63 const name = showMatch[1]
64 const org = await orgs.get(name)
65 if (!org) {
66 res.writeHead(404, { 'Content-Type': 'text/plain' })
67 res.end('Not found')
68 return
69 }
70 const grants = await orgs.listGrants({ grantType: 'group', groupName: name })
71 res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
72 res.end(renderOrg(org, grants))
73 return
74 }
75
76 if (req.method === 'POST' && url === '/create') {
77 const params = new URLSearchParams(await collectBody(req))
78 const name = params.get('name')
79 const type = params.get('type') || 'group'
80 const displayName = params.get('displayName') || name
81 const description = params.get('description') || ''
82 if (name) {
83 try {
84 await orgs.create(name, type, { displayName, description })
85 res.writeHead(303, { Location: '/' + name })
86 res.end()
87 return
88 } catch (err) {
89 res.writeHead(400, { 'Content-Type': 'text/plain' })
90 res.end(err.message)
91 return
92 }
93 }
94 res.writeHead(400)
95 res.end('name required')
96 return
97 }
98
99 const memberMatch = url.match(/^\/([a-z0-9][a-z0-9-]*[a-z0-9])\/member$/)
100 if (req.method === 'POST' && memberMatch) {
101 const name = memberMatch[1]
102 const params = new URLSearchParams(await collectBody(req))
103 const userId = params.get('userId')
104 const role = params.get('role') || 'member'
105 if (userId) await orgs.addMember(name, userId, role)
106 res.writeHead(303, { Location: '/' + name })
107 res.end()
108 return
109 }
110
111 const resourceMatch = url.match(/^\/([a-z0-9][a-z0-9-]*[a-z0-9])\/resource$/)
112 if (req.method === 'POST' && resourceMatch) {
113 const name = resourceMatch[1]
114 const params = new URLSearchParams(await collectBody(req))
115 const resource = params.get('resource')
116 if (resource) await orgs.addResource(name, resource)
117 res.writeHead(303, { Location: '/' + name })
118 res.end()
119 return
120 }
121
122 const grantMatch = url.match(/^\/([a-z0-9][a-z0-9-]*[a-z0-9])\/grant$/)
123 if (req.method === 'POST' && grantMatch) {
124 const name = grantMatch[1]
125 const params = new URLSearchParams(await collectBody(req))
126 const subjectId = params.get('subjectId')
127 const permission = params.get('permission') || 'write'
128 if (subjectId) {
129 await orgs.createGrant({
130 subjectType: 'user',
131 subjectId,
132 grantType: 'group',
133 groupName: name,
134 permission,
135 grantedBy: 'web'
136 })
137 }
138 res.writeHead(303, { Location: '/' + name })
139 res.end()
140 return
141 }
142
143 res.writeHead(404, { 'Content-Type': 'text/plain' })
144 res.end('Not found')
145 } catch (err) {
146 console.error(err)
147 res.writeHead(500, { 'Content-Type': 'text/plain' })
148 res.end(err.message)
149 }
150})
151
152function styles () {
153 return `
154 body { font-family: monospace; background: #0d1117; color: #c9d1d9; padding: 2rem; max-width: 800px; margin: 0 auto; }
155 h1 { color: #58a6ff; }
156 h2 { font-size: 1rem; color: #c9d1d9; margin-top: 0; }
157 a { color: #58a6ff; text-decoration: none; }
158 a:hover { text-decoration: underline; }
159 .card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 1rem; margin: 1rem 0; }
160 .card h2 { margin: 0 0 0.5rem; font-size: 0.75rem; text-transform: uppercase; color: #8b949e; }
161 .row { display: flex; justify-content: space-between; padding: 0.3rem 0; border-bottom: 1px solid #21262d; }
162 .row:last-child { border-bottom: none; }
163 .label { color: #8b949e; }
164 .org-row { padding: 0.6rem 0.5rem; border-bottom: 1px solid #21262d; }
165 .org-row:last-child { border-bottom: none; }
166 .org-row .kind { display: inline-block; width: 4rem; color: #8b949e; font-size: 0.75rem; }
167 .org-row .name { color: #c9d1d9; font-weight: bold; }
168 .org-row .meta { color: #6e7681; font-size: 0.75rem; margin-top: 0.2rem; margin-left: 4rem; }
169 .tag { display: inline-block; padding: 0.05rem 0.4rem; margin-right: 0.3rem; border-radius: 999px; font-size: 0.7rem; background: #1f6feb22; border: 1px solid #1f6feb44; color: #58a6ff; }
170 .role-admin { background: #2ea04322; color: #3fb950; border: 1px solid #2ea04344; padding: 0.05rem 0.4rem; border-radius: 4px; font-size: 0.7rem; }
171 .role-member { background: #30363d; color: #8b949e; border: 1px solid #484f58; padding: 0.05rem 0.4rem; border-radius: 4px; font-size: 0.7rem; }
172 input, textarea, button, select { background: #0d1117; color: #c9d1d9; border: 1px solid #30363d; padding: 0.4rem 0.6rem; border-radius: 4px; font-family: monospace; font-size: 0.85rem; }
173 textarea { width: 100%; min-height: 3em; }
174 input, select { width: 100%; box-sizing: border-box; }
175 button { background: #21262d; cursor: pointer; }
176 button:hover { background: #30363d; }
177 button.btn-primary { background: #1f6feb; border-color: #1f6feb; color: white; }
178 button.btn-primary:hover { background: #1158c7; }
179 .field { margin-bottom: 0.5rem; }
180 .field label { display: block; color: #8b949e; font-size: 0.75rem; margin-bottom: 0.2rem; }
181 .empty { color: #484f58; font-style: italic; padding: 1rem 0; }
182 form { margin: 0; }
183 .hstack { display: flex; gap: 0.5rem; }
184 `
185}
186
187function renderIndex (all) {
188 const groups = all.filter(o => o.type === 'group')
189 const users = all.filter(o => o.type === 'user')
190
191 return `<!DOCTYPE html>
192<html><head><title>hyper-orgs</title><style>${styles()}</style></head><body>
193<h1><a href="/">hyper-orgs</a></h1>
194
195<div class="card">
196 <h2>${groups.length} Group${groups.length === 1 ? '' : 's'}</h2>
197 ${groups.length === 0 ? '<div class="empty">(no groups)</div>' : groups.map(o => `
198 <div class="org-row">
199 <div>
200 <span class="kind">group</span>
201 <a class="name" href="/${o.name}">${escapeHtml(o.name)}</a>
202 ${o.visibility === 'private' ? '<span class="tag">private</span>' : ''}
203 </div>
204 <div class="meta">${escapeHtml(o.displayName)} · ${o.members.length} members · ${o.resources.length} resources</div>
205 </div>`).join('')}
206</div>
207
208<div class="card">
209 <h2>${users.length} User${users.length === 1 ? '' : 's'}</h2>
210 ${users.length === 0 ? '<div class="empty">(no user orgs)</div>' : users.map(o => `
211 <div class="org-row">
212 <div>
213 <span class="kind">user</span>
214 <a class="name" href="/${o.name}">${escapeHtml(o.name)}</a>
215 </div>
216 <div class="meta">${escapeHtml(o.displayName)} · ${o.resources.length} resources</div>
217 </div>`).join('')}
218</div>
219
220<div class="card">
221 <h2>Create org</h2>
222 <form action="/create" method="POST">
223 <div class="field"><label>Name</label><input name="name" placeholder="my-team" required pattern="[a-z0-9][a-z0-9-]*[a-z0-9]"></div>
224 <div class="field"><label>Type</label>
225 <select name="type">
226 <option value="group">group</option>
227 <option value="user">user</option>
228 </select>
229 </div>
230 <div class="field"><label>Display name</label><input name="displayName"></div>
231 <div class="field"><label>Description</label><textarea name="description"></textarea></div>
232 <button class="btn-primary" type="submit">Create</button>
233 </form>
234</div>
235
236</body></html>`
237}
238
239function renderOrg (org, grants) {
240 return `<!DOCTYPE html>
241<html><head><title>hyper-orgs / ${escapeHtml(org.name)}</title><style>${styles()}</style></head><body>
242<h1><a href="/">hyper-orgs</a> / ${escapeHtml(org.name)}</h1>
243
244<div class="card">
245 <h2>Org</h2>
246 <div><strong>${escapeHtml(org.displayName)}</strong> <span class="tag">${org.type}</span> ${org.visibility === 'private' ? '<span class="tag">private</span>' : ''}</div>
247 ${org.description ? '<div style="margin-top:0.5rem;color:#8b949e">' + escapeHtml(org.description) + '</div>' : ''}
248 <div class="meta" style="color:#6e7681;font-size:0.75rem;margin-top:0.5rem">
249 created ${new Date(org.createdAt).toLocaleString()}
250 </div>
251</div>
252
253${org.type === 'group' ? `
254<div class="card">
255 <h2>${org.members.length} Member${org.members.length === 1 ? '' : 's'}</h2>
256 ${org.members.length === 0 ? '<div class="empty">(no members)</div>' : org.members.map(m => `
257 <div class="row">
258 <div>${escapeHtml(m.userId)}</div>
259 <div><span class="role-${m.role}">${m.role}</span></div>
260 </div>`).join('')}
261 <form action="/${org.name}/member" method="POST" style="margin-top:0.8rem">
262 <div class="hstack">
263 <input name="userId" placeholder="userId" required>
264 <select name="role" style="width:auto">
265 <option value="member">member</option>
266 <option value="admin">admin</option>
267 </select>
268 <button type="submit">Add</button>
269 </div>
270 </form>
271</div>
272` : ''}
273
274<div class="card">
275 <h2>${org.resources.length} Resource${org.resources.length === 1 ? '' : 's'}</h2>
276 ${org.resources.length === 0 ? '<div class="empty">(no resources)</div>' : org.resources.map(r => `
277 <div class="row"><div>${escapeHtml(r)}</div></div>`).join('')}
278 <form action="/${org.name}/resource" method="POST" style="margin-top:0.8rem">
279 <div class="hstack">
280 <input name="resource" placeholder="resource-name" required>
281 <button type="submit">Add</button>
282 </div>
283 </form>
284</div>
285
286<div class="card">
287 <h2>${grants.length} Active grant${grants.length === 1 ? '' : 's'}</h2>
288 ${grants.length === 0 ? '<div class="empty">(no grants)</div>' : grants.map(g => `
289 <div class="row">
290 <div>${escapeHtml(g.subjectId)} <span class="tag">${g.subjectType}</span></div>
291 <div><span class="tag">${g.permission}</span></div>
292 </div>`).join('')}
293 <form action="/${org.name}/grant" method="POST" style="margin-top:0.8rem">
294 <div class="hstack">
295 <input name="subjectId" placeholder="userId" required>
296 <select name="permission" style="width:auto">
297 <option value="write">write</option>
298 <option value="moderator">moderator</option>
299 <option value="owner">owner</option>
300 </select>
301 <button type="submit">Grant</button>
302 </div>
303 </form>
304</div>
305
306</body></html>`
307}
308
309httpServer.listen(port, () => {
310 console.log('hyper-orgs dashboard:')
311 console.log(' http://localhost:%d', port)
312 console.log(' storage: %s', storagePath)
313 console.log(' orgs key: %s', orgs.keyHex)
314})
index.js +6 -0
……@@ -0,0 +1,6 @@
1/**
2 * hyper-orgs — P2P organization management on hyper-rooms
3 */
4
5export { HyperOrgs } from './lib/hyper-orgs.js'
6export { orgsSchema, RESERVED_NAMES, validateName } from './lib/schema.js'
lib/hyper-orgs.js +637 -0
……@@ -0,0 +1,637 @@
1/**
2 * HyperOrgs — P2P organization management built on hyper-rooms
3 *
4 * Multi-writer, replicating, offline-first org management. Wraps a Room with
5 * the orgsSchema and provides a high-level API for orgs, members, resources,
6 * grants, invites, and access requests.
7 *
8 * Features:
9 * - Organizations (user or group) with visibility
10 * - Members with roles (admin, member) — groups only
11 * - Resources (repos, channels, services — app decides what)
12 * - Access grants (per-resource or per-group permissions)
13 * - Invitations (with expiry and max uses)
14 * - Access requests (pending → approved/denied workflow)
15 * - Multi-writer via Room (Autobase under the hood)
16 *
17 * Usage:
18 *
19 * const orgs = new HyperOrgs(corestore, { namespace: 'my-forge' })
20 * await orgs.ready()
21 * await orgs.create('my-team', 'group', { displayName: 'My Team' })
22 * await orgs.addMember('my-team', 'alice', 'admin')
23 * await orgs.addResource('my-team', 'project-a')
24 */
25
26import { Room } from 'hyper-rooms'
27import { orgsSchema, validateName } from './schema.js'
28
29export class HyperOrgs {
30 /**
31 * @param {Corestore} corestore
32 * @param {object} options
33 * @param {string} [options.namespace='orgs']
34 * @param {Buffer|string|null} [options.bootstrapKey=null]
35 */
36 constructor (corestore, options = {}) {
37 this._room = new Room(corestore, {
38 schema: orgsSchema,
39 namespace: options.namespace || 'orgs',
40 bootstrapKey: options.bootstrapKey || null
41 })
42 }
43
44 // ── Lifecycle / properties ──────────────────────────────────
45
46 async ready () {
47 await this._room.ready()
48 }
49
50 async sync () { await this._room.base.update() }
51 async close () { await this._room.close() }
52
53 get key () { return this._room.key }
54 get keyHex () { return this._room.keyHex }
55 get discoveryKey () { return this._room.discoveryKey }
56 get localKey () { return this._room.localKey }
57 get localKeyHex () { return this._room.localKeyHex }
58 get writable () { return this._room.writable }
59 get view () { return this._room.view }
60 get base () { return this._room.base }
61 get room () { return this._room }
62
63 validateName (name) { return validateName(name) }
64
65 // ── Writer management (delegated to Room) ───────────────────
66
67 async addWriter (userId, options = {}) {
68 return this._room.addWriter(userId, options)
69 }
70
71 async removeWriter (userId) {
72 return this._room.removeWriter(userId)
73 }
74
75 async listWriters () {
76 return this._room.listWriters()
77 }
78
79 async getWriter (userId) {
80 return this._room.getWriter(userId)
81 }
82
83 // ── Org CRUD ────────────────────────────────────────────────
84
85 /**
86 * Create an organization.
87 * @param {string} name - unique, lowercase alphanumeric + hyphens
88 * @param {'user'|'group'} [type='group']
89 * @param {object} [data]
90 * @param {string} [data.displayName]
91 * @param {string} [data.description]
92 * @param {string} [data.userId] - owner userId (for user-type orgs)
93 * @param {Array} [data.members] - initial members (group-type)
94 * @param {string} [data.visibility='public']
95 */
96 async create (name, type = 'group', data = {}) {
97 const v = validateName(name)
98 if (!v.valid) throw new Error(v.error)
99 if (type !== 'user' && type !== 'group') throw new Error('Type must be "user" or "group"')
100
101 const existing = await this.get(name)
102 if (existing) throw new Error(`Organization "${name}" already exists`)
103
104 await this._room.append({
105 type: 'create-org',
106 name,
107 orgType: type,
108 displayName: data.displayName || name,
109 description: data.description || '',
110 userId: type === 'user' ? (data.userId || null) : null,
111 members: type === 'group' ? (data.members || []) : [],
112 visibility: data.visibility || 'public',
113 timestamp: Date.now()
114 })
115 await this._room.base.update()
116 return this.get(name)
117 }
118
119 async get (name) {
120 try {
121 await this._room.base.update()
122 const entry = await this._room.view.get(`org:${name}`)
123 return entry ? entry.value : null
124 } catch {
125 return null
126 }
127 }
128
129 async update (name, updates) {
130 const org = await this.get(name)
131 if (!org) throw new Error(`Organization "${name}" not found`)
132
133 const op = { type: 'update-org', name, timestamp: Date.now() }
134 if (updates.displayName !== undefined) op.displayName = updates.displayName
135 if (updates.description !== undefined) op.description = updates.description
136 if (updates.visibility !== undefined) op.visibility = updates.visibility
137
138 await this._room.append(op)
139 await this._room.base.update()
140 return this.get(name)
141 }
142
143 async delete (name) {
144 const org = await this.get(name)
145 if (!org) throw new Error(`Organization "${name}" not found`)
146 if (org.resources.length > 0) {
147 throw new Error(`Cannot delete "${name}": still has ${org.resources.length} resources`)
148 }
149
150 await this._room.append({ type: 'delete-org', name, timestamp: Date.now() })
151 await this._room.base.update()
152 }
153
154 /**
155 * List orgs with optional filtering.
156 * @param {object} [filters]
157 * @param {'user'|'group'} [filters.type]
158 * @param {string} [filters.visibility]
159 */
160 async list (filters = {}) {
161 await this._room.base.update()
162 const orgs = []
163 for await (const entry of this._room.view.createReadStream({
164 gt: 'org:', lt: 'org:\xff'
165 })) {
166 const org = entry.value
167 if (filters.type && org.type !== filters.type) continue
168 if (filters.visibility && org.visibility !== filters.visibility) continue
169 orgs.push(org)
170 }
171 return orgs
172 }
173
174 // ── Resources ───────────────────────────────────────────────
175
176 async addResource (orgName, resourceName) {
177 const org = await this.get(orgName)
178 if (!org) throw new Error(`Organization "${orgName}" not found`)
179 if (org.resources.includes(resourceName)) return org
180
181 await this._room.append({
182 type: 'add-resource',
183 orgName,
184 resourceName,
185 timestamp: Date.now()
186 })
187 await this._room.base.update()
188 return this.get(orgName)
189 }
190
191 async removeResource (orgName, resourceName) {
192 const org = await this.get(orgName)
193 if (!org) throw new Error(`Organization "${orgName}" not found`)
194 if (!org.resources.includes(resourceName)) {
195 throw new Error(`Resource "${resourceName}" is not in "${orgName}"`)
196 }
197
198 await this._room.append({
199 type: 'remove-resource',
200 orgName,
201 resourceName,
202 timestamp: Date.now()
203 })
204 await this._room.base.update()
205 return this.get(orgName)
206 }
207
208 async moveResource (resourceName, fromOrg, toOrg) {
209 const from = await this.get(fromOrg)
210 if (!from) throw new Error(`Organization "${fromOrg}" not found`)
211 const to = await this.get(toOrg)
212 if (!to) throw new Error(`Organization "${toOrg}" not found`)
213 if (!from.resources.includes(resourceName)) {
214 throw new Error(`Resource "${resourceName}" is not in "${fromOrg}"`)
215 }
216
217 await this._room.append({
218 type: 'move-resource',
219 resourceName,
220 fromOrg,
221 toOrg,
222 timestamp: Date.now()
223 })
224 await this._room.base.update()
225 }
226
227 // ── Members ─────────────────────────────────────────────────
228
229 async addMember (orgName, userId, role = 'member') {
230 const org = await this.get(orgName)
231 if (!org) throw new Error(`Organization "${orgName}" not found`)
232 if (org.type !== 'group') throw new Error('Can only add members to group organizations')
233 if (!['admin', 'member'].includes(role)) throw new Error('Role must be "admin" or "member"')
234
235 await this._room.append({
236 type: 'add-member',
237 orgName,
238 memberId: userId,
239 role,
240 timestamp: Date.now()
241 })
242 await this._room.base.update()
243 return this.get(orgName)
244 }
245
246 async removeMember (orgName, userId) {
247 const org = await this.get(orgName)
248 if (!org) throw new Error(`Organization "${orgName}" not found`)
249 if (org.type !== 'group') throw new Error('Can only remove members from group organizations')
250
251 await this._room.append({
252 type: 'remove-member',
253 orgName,
254 memberId: userId,
255 timestamp: Date.now()
256 })
257 await this._room.base.update()
258 return this.get(orgName)
259 }
260
261 async getUserOrgs (userId) {
262 const all = await this.list()
263 return all.filter(org => {
264 if (org.type === 'user' && org.userId === userId) return true
265 if (org.type === 'group' && org.members.some(m => m.userId === userId)) return true
266 return false
267 })
268 }
269
270 // ── Grants ──────────────────────────────────────────────────
271
272 async createGrant (opts) {
273 const grantId = opts.grantId ||
274 `${opts.subjectType}-${opts.subjectId.slice(0, 12)}-${Date.now()}`
275
276 await this._room.append({
277 type: 'create-grant',
278 grantId,
279 subjectType: opts.subjectType,
280 subjectId: opts.subjectId,
281 grantType: opts.grantType,
282 groupName: opts.groupName || null,
283 resourceName: opts.resourceName || null,
284 permission: opts.permission,
285 grantedBy: opts.grantedBy || null,
286 inviteId: opts.inviteId || null,
287 timestamp: Date.now()
288 })
289 await this._room.base.update()
290 return this.getGrant(grantId)
291 }
292
293 async revokeGrant (grantId, revokedBy = null) {
294 const grant = await this.getGrant(grantId)
295 if (!grant) throw new Error(`Grant "${grantId}" not found`)
296 if (grant.revokedAt) throw new Error(`Grant "${grantId}" already revoked`)
297
298 await this._room.append({
299 type: 'revoke-grant',
300 grantId,
301 revokedBy,
302 timestamp: Date.now()
303 })
304 await this._room.base.update()
305 }
306
307 async getGrant (grantId) {
308 await this._room.base.update()
309 const entry = await this._room.view.get(`grant:${grantId}`)
310 return entry ? entry.value : null
311 }
312
313 /**
314 * List grants. If a specific index key applies (subjectId + subjectType,
315 * groupName, or resourceName), uses the index for efficiency. Otherwise
316 * scans all grants.
317 */
318 async listGrants (filters = {}) {
319 await this._room.base.update()
320 const activeOnly = filters.activeOnly !== false
321
322 if (filters.subjectType === 'user' && filters.subjectId) {
323 return this._listByIndex(`user-grants:${filters.subjectId}:`, 'grant', filters, activeOnly)
324 }
325 if (filters.subjectType === 'server' && filters.subjectId) {
326 return this._listByIndex(`server-grants:${filters.subjectId}:`, 'grant', filters, activeOnly)
327 }
328 if (filters.grantType === 'resource' && filters.resourceName) {
329 return this._listByIndex(`resource-grants:${filters.resourceName}:`, 'grant', filters, activeOnly)
330 }
331 if (filters.grantType === 'group' && filters.groupName) {
332 return this._listByIndex(`group-grants:${filters.groupName}:`, 'grant', filters, activeOnly)
333 }
334
335 const grants = []
336 for await (const entry of this._room.view.createReadStream({
337 gt: 'grant:', lt: 'grant:\xff'
338 })) {
339 const g = entry.value
340 if (activeOnly && g.revokedAt) continue
341 if (filters.subjectType && g.subjectType !== filters.subjectType) continue
342 if (filters.subjectId && g.subjectId !== filters.subjectId) continue
343 if (filters.grantType && g.grantType !== filters.grantType) continue
344 if (filters.groupName && g.groupName !== filters.groupName) continue
345 if (filters.resourceName && g.resourceName !== filters.resourceName) continue
346 grants.push(g)
347 }
348 return grants
349 }
350
351 // ── Invites ─────────────────────────────────────────────────
352
353 async createInvite (opts) {
354 const inviteId = opts.inviteId ||
355 `inv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
356
357 await this._room.append({
358 type: 'create-invite',
359 inviteId,
360 inviteType: opts.inviteType,
361 targetGroup: opts.targetGroup || null,
362 targetResource: opts.targetResource || null,
363 permission: opts.permission,
364 maxUses: opts.maxUses || 1,
365 createdBy: opts.createdBy || null,
366 expiresAt: opts.expiresAt || null,
367 timestamp: Date.now()
368 })
369 await this._room.base.update()
370 return this.getInvite(inviteId)
371 }
372
373 async consumeInvite (inviteId, opts) {
374 const invite = await this.getInvite(inviteId)
375 if (!invite) throw new Error(`Invite "${inviteId}" not found`)
376 if (invite.revoked) throw new Error('Invite has been revoked')
377 if (invite.maxUses > 0 && invite.usedCount >= invite.maxUses) {
378 throw new Error('Invite fully used')
379 }
380 if (invite.expiresAt && Date.now() > invite.expiresAt) throw new Error('Invite expired')
381
382 const grantId = `${opts.subjectType}-${opts.subjectId.slice(0, 12)}-${Date.now()}`
383 await this._room.append({
384 type: 'consume-invite',
385 inviteId,
386 grantId,
387 subjectType: opts.subjectType,
388 subjectId: opts.subjectId,
389 timestamp: Date.now()
390 })
391 await this._room.base.update()
392 return this.getGrant(grantId)
393 }
394
395 async revokeInvite (inviteId) {
396 const invite = await this.getInvite(inviteId)
397 if (!invite) throw new Error(`Invite "${inviteId}" not found`)
398 await this._room.append({ type: 'revoke-invite', inviteId, timestamp: Date.now() })
399 await this._room.base.update()
400 }
401
402 async getInvite (inviteId) {
403 await this._room.base.update()
404 const entry = await this._room.view.get(`invite:${inviteId}`)
405 return entry ? entry.value : null
406 }
407
408 async listInvites (filters = {}) {
409 await this._room.base.update()
410 const activeOnly = filters.activeOnly !== false
411 const invites = []
412 for await (const entry of this._room.view.createReadStream({
413 gt: 'invite:', lt: 'invite:\xff'
414 })) {
415 const inv = entry.value
416 if (activeOnly && inv.revoked) continue
417 if (activeOnly && inv.expiresAt && Date.now() > inv.expiresAt) continue
418 if (activeOnly && inv.maxUses > 0 && inv.usedCount >= inv.maxUses) continue
419 invites.push(inv)
420 }
421 return invites
422 }
423
424 // ── Access Requests ─────────────────────────────────────────
425
426 async createAccessRequest (opts) {
427 const requestId = opts.requestId ||
428 `req-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
429
430 const pending = await this.listAccessRequests({
431 subjectId: opts.subjectId,
432 status: 'pending'
433 })
434 const target = opts.requestType === 'group' ? opts.groupName : opts.resourceName
435 const duplicate = pending.find(r =>
436 r.requestType === opts.requestType &&
437 ((r.requestType === 'group' && r.groupName === target) ||
438 (r.requestType === 'resource' && r.resourceName === target))
439 )
440 if (duplicate) throw new Error('Already have a pending request for this target')
441
442 await this._room.append({
443 type: 'create-access-request',
444 requestId,
445 subjectType: opts.subjectType,
446 subjectId: opts.subjectId,
447 requestType: opts.requestType,
448 groupName: opts.groupName || null,
449 resourceName: opts.resourceName || null,
450 permission: opts.permission,
451 reason: opts.reason || null,
452 timestamp: Date.now()
453 })
454 await this._room.base.update()
455 return this.getAccessRequest(requestId)
456 }
457
458 async approveAccessRequest (requestId, approvedBy = null) {
459 const request = await this.getAccessRequest(requestId)
460 if (!request) throw new Error(`Request "${requestId}" not found`)
461 if (request.status !== 'pending') throw new Error(`Request is already ${request.status}`)
462
463 const grantId = `${request.subjectType}-${request.subjectId.slice(0, 12)}-${Date.now()}`
464 await this._room.append({
465 type: 'approve-access-request',
466 requestId,
467 grantId,
468 approvedBy,
469 timestamp: Date.now()
470 })
471 await this._room.base.update()
472 return this.getGrant(grantId)
473 }
474
475 async denyAccessRequest (requestId, deniedBy = null, denyReason = null) {
476 const request = await this.getAccessRequest(requestId)
477 if (!request) throw new Error(`Request "${requestId}" not found`)
478 if (request.status !== 'pending') throw new Error(`Request is already ${request.status}`)
479
480 await this._room.append({
481 type: 'deny-access-request',
482 requestId,
483 deniedBy,
484 denyReason,
485 timestamp: Date.now()
486 })
487 await this._room.base.update()
488 }
489
490 async getAccessRequest (requestId) {
491 await this._room.base.update()
492 const entry = await this._room.view.get(`request:${requestId}`)
493 return entry ? entry.value : null
494 }
495
496 async listAccessRequests (filters = {}) {
497 await this._room.base.update()
498
499 if (filters.subjectId) {
500 return this._listByIndex(`user-requests:${filters.subjectId}:`, 'request', filters, false)
501 }
502 if (filters.requestType === 'group' && filters.groupName) {
503 return this._listByIndex(`group-requests:${filters.groupName}:`, 'request', filters, false)
504 }
505 if (filters.requestType === 'resource' && filters.resourceName) {
506 return this._listByIndex(`resource-requests:${filters.resourceName}:`, 'request', filters, false)
507 }
508
509 const requests = []
510 for await (const entry of this._room.view.createReadStream({
511 gt: 'request:', lt: 'request:\xff'
512 })) {
513 const r = entry.value
514 if (filters.status && r.status !== filters.status) continue
515 if (filters.subjectId && r.subjectId !== filters.subjectId) continue
516 if (filters.requestType && r.requestType !== filters.requestType) continue
517 requests.push(r)
518 }
519 return requests
520 }
521
522 // ── Access Checking ─────────────────────────────────────────
523
524 static permissionLevel (perm) {
525 switch (perm) {
526 case 'owner': return 3
527 case 'moderator': return 2
528 case 'write': return 1
529 default: return 0
530 }
531 }
532
533 /**
534 * Check whether a subject has the required permission on a resource.
535 * Walks direct grants first, then group grants (via the orgs the resource
536 * belongs to).
537 *
538 * @param {object} opts
539 * @param {string} [opts.userId]
540 * @param {string} [opts.serverKey]
541 * @param {string} opts.resourceName
542 * @param {'write'|'moderator'|'owner'} opts.requiredPermission
543 */
544 async checkAccess (opts) {
545 const required = HyperOrgs.permissionLevel(opts.requiredPermission)
546 if (required === 0) return false
547
548 // Direct user resource grant
549 if (opts.userId) {
550 const grants = await this.listGrants({
551 subjectType: 'user', subjectId: opts.userId,
552 grantType: 'resource', resourceName: opts.resourceName
553 })
554 for (const g of grants) {
555 if (HyperOrgs.permissionLevel(g.permission) >= required) return true
556 }
557 }
558
559 // User group grant (for orgs containing this resource)
560 if (opts.userId) {
561 const orgs = await this.list()
562 for (const org of orgs) {
563 if (!org.resources.includes(opts.resourceName)) continue
564 const grants = await this.listGrants({
565 subjectType: 'user', subjectId: opts.userId,
566 grantType: 'group', groupName: org.name
567 })
568 for (const g of grants) {
569 if (HyperOrgs.permissionLevel(g.permission) >= required) return true
570 }
571 }
572 }
573
574 // Direct server resource grant
575 if (opts.serverKey) {
576 const grants = await this.listGrants({
577 subjectType: 'server', subjectId: opts.serverKey,
578 grantType: 'resource', resourceName: opts.resourceName
579 })
580 for (const g of grants) {
581 if (HyperOrgs.permissionLevel(g.permission) >= required) return true
582 }
583 }
584
585 // Server group grant
586 if (opts.serverKey) {
587 const orgs = await this.list()
588 for (const org of orgs) {
589 if (!org.resources.includes(opts.resourceName)) continue
590 const grants = await this.listGrants({
591 subjectType: 'server', subjectId: opts.serverKey,
592 grantType: 'group', groupName: org.name
593 })
594 for (const g of grants) {
595 if (HyperOrgs.permissionLevel(g.permission) >= required) return true
596 }
597 }
598 }
599
600 return false
601 }
602
603 // ── Meta ────────────────────────────────────────────────────
604
605 async setMeta (key, value) {
606 await this._room.append({ type: 'set-meta', key, value, timestamp: Date.now() })
607 }
608
609 async getMeta (key) {
610 await this._room.base.update()
611 const entry = await this._room.view.get(`meta:${key}`)
612 return entry ? entry.value.value : null
613 }
614
615 // ── Private ─────────────────────────────────────────────────
616
617 async _listByIndex (indexPrefix, entityPrefix, filters, activeOnly) {
618 const results = []
619 for await (const entry of this._room.view.createReadStream({
620 gt: indexPrefix, lt: indexPrefix + '\xff'
621 })) {
622 const idField = entityPrefix === 'grant' ? 'grantId' : 'requestId'
623 const id = entry.value[idField]
624 const entityEntry = await this._room.view.get(`${entityPrefix}:${id}`)
625 if (!entityEntry) continue
626 const e = entityEntry.value
627 if (activeOnly && e.revokedAt) continue
628 if (filters.status && e.status !== filters.status) continue
629 if (filters.subjectType && e.subjectType !== filters.subjectType) continue
630 if (filters.grantType && e.grantType !== filters.grantType) continue
631 if (filters.groupName && e.groupName !== filters.groupName) continue
632 if (filters.resourceName && e.resourceName !== filters.resourceName) continue
633 results.push(e)
634 }
635 return results
636 }
637}
lib/schema.js +515 -0
……@@ -0,0 +1,515 @@
1/**
2 * orgsSchema — hyper-rooms schema for P2P organization management
3 *
4 * Domain ops (writer ops are handled by Room):
5 * create-org, update-org, delete-org
6 * add-resource, remove-resource, move-resource
7 * add-member, remove-member
8 * create-grant, revoke-grant
9 * create-invite, consume-invite, revoke-invite
10 * create-access-request, approve-access-request, deny-access-request
11 * set-meta
12 *
13 * View schema (Hyperbee keys):
14 * org:{name} → { name, displayName, description, type, userId, members, resources, visibility, createdAt, updatedAt }
15 * grant:{grantId} → { grantId, subjectType, subjectId, grantType, groupName, resourceName, permission, ... }
16 * invite:{inviteId} → { inviteId, inviteType, targetGroup, targetResource, permission, maxUses, ... }
17 * request:{requestId} → { requestId, subjectType, subjectId, requestType, groupName, resourceName, permission, status, ... }
18 * user-grants:{subjectId}:{grantId} → { grantId }
19 * server-grants:{subjectId}:{grantId} → { grantId }
20 * group-grants:{groupName}:{grantId} → { grantId }
21 * resource-grants:{resourceName}:{grantId} → { grantId }
22 * user-requests:{subjectId}:{requestId} → { requestId }
23 * server-requests:{subjectId}:{requestId} → { requestId }
24 * group-requests:{groupName}:{requestId} → { requestId }
25 * resource-requests:{resourceName}:{requestId} → { requestId }
26 * meta:{key} → { value }
27 */
28
29import { jsonSchema } from 'hyper-rooms'
30
31// Reserved names that cannot be used as org names
32export const RESERVED_NAMES = new Set([
33 'admin', 'api', 'git', 'settings', 'login', 'logout',
34 'register', 'auth', 'static', 'assets', 'public',
35 'local', 'system', 'root', 'null', 'undefined'
36])
37
38export function validateName (name) {
39 if (!name || typeof name !== 'string') return { valid: false, error: 'Name is required' }
40 if (name.length < 2) return { valid: false, error: 'Name must be at least 2 characters' }
41 if (name.length > 64) return { valid: false, error: 'Name must be at most 64 characters' }
42 if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(name)) {
43 return { valid: false, error: 'Name must be lowercase alphanumeric with hyphens, not starting/ending with hyphen' }
44 }
45 if (RESERVED_NAMES.has(name)) return { valid: false, error: `"${name}" is a reserved name` }
46 return { valid: true }
47}
48
49export const orgsSchema = jsonSchema({
50 validate (op) {
51 switch (op.type) {
52 case 'create-org':
53 if (!op.name) throw new Error('create-org requires name')
54 if (!op.orgType || !['user', 'group'].includes(op.orgType)) {
55 throw new Error('create-org requires orgType (user|group)')
56 }
57 break
58 case 'update-org':
59 if (!op.name) throw new Error('update-org requires name')
60 break
61 case 'delete-org':
62 if (!op.name) throw new Error('delete-org requires name')
63 break
64
65 case 'add-resource':
66 if (!op.orgName) throw new Error('add-resource requires orgName')
67 if (!op.resourceName) throw new Error('add-resource requires resourceName')
68 break
69 case 'remove-resource':
70 if (!op.orgName) throw new Error('remove-resource requires orgName')
71 if (!op.resourceName) throw new Error('remove-resource requires resourceName')
72 break
73 case 'move-resource':
74 if (!op.resourceName) throw new Error('move-resource requires resourceName')
75 if (!op.fromOrg) throw new Error('move-resource requires fromOrg')
76 if (!op.toOrg) throw new Error('move-resource requires toOrg')
77 break
78
79 case 'add-member':
80 if (!op.orgName) throw new Error('add-member requires orgName')
81 if (!op.memberId) throw new Error('add-member requires memberId')
82 if (!op.role || !['admin', 'member'].includes(op.role)) {
83 throw new Error('add-member requires role (admin|member)')
84 }
85 break
86 case 'remove-member':
87 if (!op.orgName) throw new Error('remove-member requires orgName')
88 if (!op.memberId) throw new Error('remove-member requires memberId')
89 break
90
91 case 'create-grant':
92 if (!op.grantId) throw new Error('create-grant requires grantId')
93 if (!op.subjectType || !['user', 'server'].includes(op.subjectType)) {
94 throw new Error('create-grant requires subjectType (user|server)')
95 }
96 if (!op.subjectId) throw new Error('create-grant requires subjectId')
97 if (!op.grantType || !['group', 'resource'].includes(op.grantType)) {
98 throw new Error('create-grant requires grantType (group|resource)')
99 }
100 if (op.grantType === 'group' && !op.groupName) {
101 throw new Error('create-grant with grantType=group requires groupName')
102 }
103 if (op.grantType === 'resource' && !op.resourceName) {
104 throw new Error('create-grant with grantType=resource requires resourceName')
105 }
106 if (!op.permission || !['write', 'moderator', 'owner'].includes(op.permission)) {
107 throw new Error('create-grant requires permission (write|moderator|owner)')
108 }
109 break
110 case 'revoke-grant':
111 if (!op.grantId) throw new Error('revoke-grant requires grantId')
112 break
113
114 case 'create-invite':
115 if (!op.inviteId) throw new Error('create-invite requires inviteId')
116 if (!op.inviteType || !['group', 'resource'].includes(op.inviteType)) {
117 throw new Error('create-invite requires inviteType (group|resource)')
118 }
119 if (op.inviteType === 'group' && !op.targetGroup) {
120 throw new Error('create-invite with inviteType=group requires targetGroup')
121 }
122 if (op.inviteType === 'resource' && !op.targetResource) {
123 throw new Error('create-invite with inviteType=resource requires targetResource')
124 }
125 if (!op.permission || !['write', 'moderator', 'owner'].includes(op.permission)) {
126 throw new Error('create-invite requires permission (write|moderator|owner)')
127 }
128 break
129 case 'consume-invite':
130 if (!op.inviteId) throw new Error('consume-invite requires inviteId')
131 if (!op.grantId) throw new Error('consume-invite requires grantId')
132 if (!op.subjectType || !['user', 'server'].includes(op.subjectType)) {
133 throw new Error('consume-invite requires subjectType (user|server)')
134 }
135 if (!op.subjectId) throw new Error('consume-invite requires subjectId')
136 break
137 case 'revoke-invite':
138 if (!op.inviteId) throw new Error('revoke-invite requires inviteId')
139 break
140
141 case 'create-access-request':
142 if (!op.requestId) throw new Error('create-access-request requires requestId')
143 if (!op.subjectType || !['user', 'server'].includes(op.subjectType)) {
144 throw new Error('create-access-request requires subjectType (user|server)')
145 }
146 if (!op.subjectId) throw new Error('create-access-request requires subjectId')
147 if (!op.requestType || !['group', 'resource'].includes(op.requestType)) {
148 throw new Error('create-access-request requires requestType (group|resource)')
149 }
150 if (op.requestType === 'group' && !op.groupName) {
151 throw new Error('create-access-request with requestType=group requires groupName')
152 }
153 if (op.requestType === 'resource' && !op.resourceName) {
154 throw new Error('create-access-request with requestType=resource requires resourceName')
155 }
156 if (!op.permission || !['write', 'moderator', 'owner'].includes(op.permission)) {
157 throw new Error('create-access-request requires permission (write|moderator|owner)')
158 }
159 break
160 case 'approve-access-request':
161 if (!op.requestId) throw new Error('approve-access-request requires requestId')
162 if (!op.grantId) throw new Error('approve-access-request requires grantId')
163 break
164 case 'deny-access-request':
165 if (!op.requestId) throw new Error('deny-access-request requires requestId')
166 break
167
168 case 'set-meta':
169 if (!op.key) throw new Error('set-meta requires key')
170 break
171 }
172 },
173
174 async apply (batch, op, host) {
175 switch (op.type) {
176 // ── Orgs ─────────────────────────────────────────────────
177 case 'create-org': {
178 const existing = await batch.get(`org:${op.name}`)
179 if (existing) break
180
181 await batch.put(`org:${op.name}`, {
182 name: op.name,
183 displayName: op.displayName || op.name,
184 description: op.description || '',
185 type: op.orgType,
186 userId: op.orgType === 'user' ? (op.userId || null) : null,
187 members: op.members || [],
188 resources: [],
189 visibility: op.visibility || 'public',
190 createdAt: op.timestamp,
191 updatedAt: op.timestamp
192 })
193 break
194 }
195
196 case 'update-org': {
197 const entry = await batch.get(`org:${op.name}`)
198 if (!entry) break
199 const org = entry.value
200 if (op.displayName !== undefined) org.displayName = op.displayName
201 if (op.description !== undefined) org.description = op.description
202 if (op.visibility !== undefined) org.visibility = op.visibility
203 org.updatedAt = op.timestamp
204 await batch.put(`org:${op.name}`, org)
205 break
206 }
207
208 case 'delete-org': {
209 const entry = await batch.get(`org:${op.name}`)
210 if (!entry) break
211 if (entry.value.resources.length > 0) break
212 await batch.del(`org:${op.name}`)
213 break
214 }
215
216 // ── Resources ────────────────────────────────────────────
217 case 'add-resource': {
218 const orgEntry = await batch.get(`org:${op.orgName}`)
219 if (!orgEntry) break
220
221 // Remove the resource from any other org that currently holds it
222 for await (const entry of batch.createReadStream({
223 gt: 'org:', lt: 'org:\xff'
224 })) {
225 const other = entry.value
226 if (other.name !== op.orgName && other.resources.includes(op.resourceName)) {
227 other.resources = other.resources.filter(r => r !== op.resourceName)
228 other.updatedAt = op.timestamp
229 await batch.put(`org:${other.name}`, other)
230 }
231 }
232
233 const refreshed = await batch.get(`org:${op.orgName}`)
234 const org = refreshed.value
235 if (!org.resources.includes(op.resourceName)) {
236 org.resources.push(op.resourceName)
237 org.updatedAt = op.timestamp
238 await batch.put(`org:${op.orgName}`, org)
239 }
240 break
241 }
242
243 case 'remove-resource': {
244 const entry = await batch.get(`org:${op.orgName}`)
245 if (!entry) break
246 const org = entry.value
247 org.resources = org.resources.filter(r => r !== op.resourceName)
248 org.updatedAt = op.timestamp
249 await batch.put(`org:${op.orgName}`, org)
250 break
251 }
252
253 case 'move-resource': {
254 const fromEntry = await batch.get(`org:${op.fromOrg}`)
255 const toEntry = await batch.get(`org:${op.toOrg}`)
256 if (!fromEntry || !toEntry) break
257
258 const from = fromEntry.value
259 const to = toEntry.value
260
261 from.resources = from.resources.filter(r => r !== op.resourceName)
262 from.updatedAt = op.timestamp
263 await batch.put(`org:${op.fromOrg}`, from)
264
265 if (!to.resources.includes(op.resourceName)) to.resources.push(op.resourceName)
266 to.updatedAt = op.timestamp
267 await batch.put(`org:${op.toOrg}`, to)
268 break
269 }
270
271 // ── Members ──────────────────────────────────────────────
272 case 'add-member': {
273 const entry = await batch.get(`org:${op.orgName}`)
274 if (!entry) break
275 const org = entry.value
276 const idx = org.members.findIndex(m => m.userId === op.memberId)
277 if (idx >= 0) {
278 org.members[idx].role = op.role
279 } else {
280 org.members.push({ userId: op.memberId, role: op.role, addedAt: op.timestamp })
281 }
282 org.updatedAt = op.timestamp
283 await batch.put(`org:${op.orgName}`, org)
284 break
285 }
286
287 case 'remove-member': {
288 const entry = await batch.get(`org:${op.orgName}`)
289 if (!entry) break
290 const org = entry.value
291 org.members = org.members.filter(m => m.userId !== op.memberId)
292 org.updatedAt = op.timestamp
293 await batch.put(`org:${op.orgName}`, org)
294 break
295 }
296
297 // ── Grants ───────────────────────────────────────────────
298 case 'create-grant': {
299 const grantKey = `grant:${op.grantId}`
300 const existing = await batch.get(grantKey)
301 if (existing) break
302
303 await batch.put(grantKey, {
304 grantId: op.grantId,
305 subjectType: op.subjectType,
306 subjectId: op.subjectId,
307 grantType: op.grantType,
308 groupName: op.groupName || null,
309 resourceName: op.resourceName || null,
310 permission: op.permission,
311 grantedBy: op.grantedBy || null,
312 grantedAt: op.timestamp,
313 revokedAt: null,
314 revokedBy: null,
315 inviteId: op.inviteId || null
316 })
317
318 if (op.subjectType === 'user') {
319 await batch.put(`user-grants:${op.subjectId}:${op.grantId}`, { grantId: op.grantId })
320 } else {
321 await batch.put(`server-grants:${op.subjectId}:${op.grantId}`, { grantId: op.grantId })
322 }
323 if (op.grantType === 'resource') {
324 await batch.put(`resource-grants:${op.resourceName}:${op.grantId}`, { grantId: op.grantId })
325 } else {
326 await batch.put(`group-grants:${op.groupName}:${op.grantId}`, { grantId: op.grantId })
327 }
328 break
329 }
330
331 case 'revoke-grant': {
332 const entry = await batch.get(`grant:${op.grantId}`)
333 if (!entry) break
334 const grant = entry.value
335 if (grant.revokedAt) break
336 grant.revokedAt = op.timestamp
337 grant.revokedBy = op.revokedBy || null
338 await batch.put(`grant:${op.grantId}`, grant)
339 break
340 }
341
342 // ── Invites ──────────────────────────────────────────────
343 case 'create-invite': {
344 const inviteKey = `invite:${op.inviteId}`
345 const existing = await batch.get(inviteKey)
346 if (existing) break
347
348 await batch.put(inviteKey, {
349 inviteId: op.inviteId,
350 inviteType: op.inviteType,
351 targetGroup: op.targetGroup || null,
352 targetResource: op.targetResource || null,
353 permission: op.permission,
354 maxUses: op.maxUses || 1,
355 usedCount: 0,
356 createdBy: op.createdBy || null,
357 createdAt: op.timestamp,
358 expiresAt: op.expiresAt || null,
359 revoked: false
360 })
361 break
362 }
363
364 case 'consume-invite': {
365 const inviteKey = `invite:${op.inviteId}`
366 const inviteEntry = await batch.get(inviteKey)
367 if (!inviteEntry) break
368
369 const invite = inviteEntry.value
370 if (invite.revoked) break
371 if (invite.maxUses > 0 && invite.usedCount >= invite.maxUses) break
372 if (invite.expiresAt && op.timestamp > invite.expiresAt) break
373
374 invite.usedCount++
375 await batch.put(inviteKey, invite)
376
377 const grantKey = `grant:${op.grantId}`
378 const existingGrant = await batch.get(grantKey)
379 if (existingGrant) break
380
381 await batch.put(grantKey, {
382 grantId: op.grantId,
383 subjectType: op.subjectType,
384 subjectId: op.subjectId,
385 grantType: invite.inviteType,
386 groupName: invite.targetGroup || null,
387 resourceName: invite.targetResource || null,
388 permission: invite.permission,
389 grantedBy: invite.createdBy || null,
390 grantedAt: op.timestamp,
391 revokedAt: null,
392 revokedBy: null,
393 inviteId: op.inviteId
394 })
395
396 if (op.subjectType === 'user') {
397 await batch.put(`user-grants:${op.subjectId}:${op.grantId}`, { grantId: op.grantId })
398 } else {
399 await batch.put(`server-grants:${op.subjectId}:${op.grantId}`, { grantId: op.grantId })
400 }
401 if (invite.inviteType === 'resource') {
402 await batch.put(`resource-grants:${invite.targetResource}:${op.grantId}`, { grantId: op.grantId })
403 } else {
404 await batch.put(`group-grants:${invite.targetGroup}:${op.grantId}`, { grantId: op.grantId })
405 }
406 break
407 }
408
409 case 'revoke-invite': {
410 const entry = await batch.get(`invite:${op.inviteId}`)
411 if (!entry) break
412 entry.value.revoked = true
413 await batch.put(`invite:${op.inviteId}`, entry.value)
414 break
415 }
416
417 // ── Access requests ──────────────────────────────────────
418 case 'create-access-request': {
419 const reqKey = `request:${op.requestId}`
420 const existing = await batch.get(reqKey)
421 if (existing) break
422
423 await batch.put(reqKey, {
424 requestId: op.requestId,
425 subjectType: op.subjectType,
426 subjectId: op.subjectId,
427 requestType: op.requestType,
428 groupName: op.groupName || null,
429 resourceName: op.resourceName || null,
430 permission: op.permission,
431 reason: op.reason || null,
432 status: 'pending',
433 createdAt: op.timestamp,
434 respondedAt: null,
435 respondedBy: null,
436 denyReason: null
437 })
438
439 const target = op.requestType === 'group' ? op.groupName : op.resourceName
440 await batch.put(`${op.requestType}-requests:${target}:${op.requestId}`, { requestId: op.requestId })
441 if (op.subjectType === 'user') {
442 await batch.put(`user-requests:${op.subjectId}:${op.requestId}`, { requestId: op.requestId })
443 } else {
444 await batch.put(`server-requests:${op.subjectId}:${op.requestId}`, { requestId: op.requestId })
445 }
446 break
447 }
448
449 case 'approve-access-request': {
450 const reqKey = `request:${op.requestId}`
451 const reqEntry = await batch.get(reqKey)
452 if (!reqEntry) break
453 const request = reqEntry.value
454 if (request.status !== 'pending') break
455
456 request.status = 'approved'
457 request.respondedAt = op.timestamp
458 request.respondedBy = op.approvedBy || null
459 await batch.put(reqKey, request)
460
461 const grantKey = `grant:${op.grantId}`
462 const existingGrant = await batch.get(grantKey)
463 if (existingGrant) break
464
465 await batch.put(grantKey, {
466 grantId: op.grantId,
467 subjectType: request.subjectType,
468 subjectId: request.subjectId,
469 grantType: request.requestType,
470 groupName: request.groupName || null,
471 resourceName: request.resourceName || null,
472 permission: request.permission,
473 grantedBy: op.approvedBy || null,
474 grantedAt: op.timestamp,
475 revokedAt: null,
476 revokedBy: null,
477 requestId: op.requestId
478 })
479
480 if (request.subjectType === 'user') {
481 await batch.put(`user-grants:${request.subjectId}:${op.grantId}`, { grantId: op.grantId })
482 } else {
483 await batch.put(`server-grants:${request.subjectId}:${op.grantId}`, { grantId: op.grantId })
484 }
485 if (request.requestType === 'resource') {
486 await batch.put(`resource-grants:${request.resourceName}:${op.grantId}`, { grantId: op.grantId })
487 } else {
488 await batch.put(`group-grants:${request.groupName}:${op.grantId}`, { grantId: op.grantId })
489 }
490 break
491 }
492
493 case 'deny-access-request': {
494 const reqKey = `request:${op.requestId}`
495 const reqEntry = await batch.get(reqKey)
496 if (!reqEntry) break
497 const request = reqEntry.value
498 if (request.status !== 'pending') break
499
500 request.status = 'denied'
501 request.respondedAt = op.timestamp
502 request.respondedBy = op.deniedBy || null
503 request.denyReason = op.denyReason || null
504 await batch.put(reqKey, request)
505 break
506 }
507
508 // ── Meta ─────────────────────────────────────────────────
509 case 'set-meta': {
510 await batch.put(`meta:${op.key}`, { value: op.value })
511 break
512 }
513 }
514 }
515})
package-lock.json +1428 -0
……@@ -0,0 +1,1428 @@
1{
2 "name": "hyper-orgs",
3 "version": "0.2.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "hyper-orgs",
9 "version": "0.2.0",
10 "dependencies": {
11 "hyper-rooms": "file:../hyper-rooms"
12 },
13 "devDependencies": {
14 "autobase-test-helpers": "^2.0.0",
15 "brittle": "^3.0.0",
16 "corestore": "^7.0.0"
17 }
18 },
19 "../hyper-rooms": {
20 "version": "0.1.0",
21 "dependencies": {
22 "autobase": "^7.20.0",
23 "b4a": "^1.6.0",
24 "hyperbee": "^2.20.0",
25 "hypercore-crypto": "^3.4.0",
26 "ready-resource": "^1.1.0"
27 },
28 "devDependencies": {
29 "autobase-test-helpers": "^2.0.0",
30 "brittle": "^3.0.0",
31 "corestore": "^7.0.0"
32 }
33 },
34 "node_modules/@hyperswarm/secret-stream": {
35 "version": "6.9.1",
36 "resolved": "https://registry.npmjs.org/@hyperswarm/secret-stream/-/secret-stream-6.9.1.tgz",
37 "integrity": "sha512-xb0S5y3YJwBakD77JOGBHlBxdp63mHClZoXBYoLv+9wH8e054ESKlmQptWqjJK5dv5VMUIVYOJB4MaOpB0JdGw==",
38 "dev": true,
39 "license": "Apache-2.0",
40 "dependencies": {
41 "b4a": "^1.1.0",
42 "hypercore-crypto": "^3.3.1",
43 "noise-curve-ed": "^2.0.1",
44 "noise-handshake": "^4.0.0",
45 "sodium-secretstream": "^1.1.0",
46 "sodium-universal": "^5.0.0",
47 "streamx": "^2.14.0",
48 "timeout-refresh": "^2.0.0",
49 "unslab": "^1.3.0"
50 }
51 },
52 "node_modules/@jridgewell/resolve-uri": {
53 "version": "3.1.2",
54 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
55 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
56 "dev": true,
57 "license": "MIT",
58 "engines": {
59 "node": ">=6.0.0"
60 }
61 },
62 "node_modules/@jridgewell/sourcemap-codec": {
63 "version": "1.5.5",
64 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
65 "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
66 "dev": true,
67 "license": "MIT"
68 },
69 "node_modules/@jridgewell/trace-mapping": {
70 "version": "0.3.31",
71 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
72 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
73 "dev": true,
74 "license": "MIT",
75 "dependencies": {
76 "@jridgewell/resolve-uri": "^3.1.0",
77 "@jridgewell/sourcemap-codec": "^1.4.14"
78 }
79 },
80 "node_modules/@types/istanbul-lib-coverage": {
81 "version": "2.0.6",
82 "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
83 "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
84 "dev": true,
85 "license": "MIT"
86 },
87 "node_modules/autobase-test-helpers": {
88 "version": "2.1.3",
89 "resolved": "https://registry.npmjs.org/autobase-test-helpers/-/autobase-test-helpers-2.1.3.tgz",
90 "integrity": "sha512-DYaBLakwMeWv32VcW0StyKZ6mQjA2HV8qRlnIx0XeM93/nw8l13DdiGXZf9EAB14nhKG5drHQIOQrJAiW5BOfg==",
91 "dev": true,
92 "license": "Apache-2.0",
93 "dependencies": {
94 "b4a": "^1.6.4"
95 }
96 },
97 "node_modules/b4a": {
98 "version": "1.8.0",
99 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
100 "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
101 "dev": true,
102 "license": "Apache-2.0",
103 "peerDependencies": {
104 "react-native-b4a": "*"
105 },
106 "peerDependenciesMeta": {
107 "react-native-b4a": {
108 "optional": true
109 }
110 }
111 },
112 "node_modules/bare-abort": {
113 "version": "2.0.13",
114 "resolved": "https://registry.npmjs.org/bare-abort/-/bare-abort-2.0.13.tgz",
115 "integrity": "sha512-zdc8l88eB11Jsz5rDd6sCAgv2kUFXgdrZWoMlgU6JMkfAi1/uuGFC3IEHswKbIRQTk5H3T5CMuechsXYxiaHlQ==",
116 "dev": true,
117 "license": "Apache-2.0"
118 },
119 "node_modules/bare-addon-resolve": {
120 "version": "1.10.0",
121 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
122 "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
123 "dev": true,
124 "license": "Apache-2.0",
125 "dependencies": {
126 "bare-module-resolve": "^1.10.0",
127 "bare-semver": "^1.0.0"
128 },
129 "peerDependencies": {
130 "bare-url": "*"
131 },
132 "peerDependenciesMeta": {
133 "bare-url": {
134 "optional": true
135 }
136 }
137 },
138 "node_modules/bare-ansi-escapes": {
139 "version": "2.2.3",
140 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
141 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
142 "dev": true,
143 "license": "Apache-2.0",
144 "dependencies": {
145 "bare-stream": "^2.6.5"
146 },
147 "peerDependencies": {
148 "bare-buffer": "*"
149 },
150 "peerDependenciesMeta": {
151 "bare-buffer": {
152 "optional": true
153 }
154 }
155 },
156 "node_modules/bare-assert": {
157 "version": "1.2.0",
158 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
159 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
160 "dev": true,
161 "license": "Apache-2.0",
162 "dependencies": {
163 "bare-inspect": "^3.1.2"
164 }
165 },
166 "node_modules/bare-bundle": {
167 "version": "1.10.0",
168 "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
169 "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
170 "dev": true,
171 "license": "Apache-2.0",
172 "peerDependencies": {
173 "bare-buffer": "*",
174 "bare-url": "*"
175 },
176 "peerDependenciesMeta": {
177 "bare-buffer": {
178 "optional": true
179 },
180 "bare-url": {
181 "optional": true
182 }
183 }
184 },
185 "node_modules/bare-cov": {
186 "version": "1.2.1",
187 "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz",
188 "integrity": "sha512-SHNqb/WG9udUEj8m5qGSehoUQ9o0Xzv8nrf4HIdmG+KNU/XdhPLhTP5VPrHnSAMAXhAzEB4Q+2IYZ0x9qPzN1g==",
189 "dev": true,
190 "license": "Apache-2.0",
191 "dependencies": {
192 "bare-fs": "^4.1.2",
193 "bare-inspector": "^4.0.1",
194 "bare-path": "^3.0.0",
195 "bare-process": "^4.2.1",
196 "bare-url": "^2.1.5",
197 "bare-v8-to-istanbul": "^1.0.2",
198 "picomatch": "^4.0.2",
199 "which-runtime": "^1.2.1"
200 }
201 },
202 "node_modules/bare-crypto": {
203 "version": "1.13.4",
204 "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.4.tgz",
205 "integrity": "sha512-JiCZ5l2YOG1y8J7yy1BCAKTCZrPnHLb7pDRIdurBTOn5oIwBQDIv8iH5Pl2V85vzjl1NZXRfNY4HZLsE942jJA==",
206 "dev": true,
207 "license": "Apache-2.0",
208 "dependencies": {
209 "bare-assert": "^1.2.0",
210 "bare-stream": "^2.6.3"
211 },
212 "peerDependencies": {
213 "bare-buffer": "*"
214 },
215 "peerDependenciesMeta": {
216 "bare-buffer": {
217 "optional": true
218 }
219 }
220 },
221 "node_modules/bare-debug-log": {
222 "version": "2.0.0",
223 "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
224 "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
225 "dev": true,
226 "license": "Apache-2.0",
227 "dependencies": {
228 "bare-os": "^3.0.1"
229 }
230 },
231 "node_modules/bare-dns": {
232 "version": "2.1.4",
233 "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
234 "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==",
235 "dev": true,
236 "license": "Apache-2.0",
237 "engines": {
238 "bare": ">=1.7.0"
239 }
240 },
241 "node_modules/bare-encoding": {
242 "version": "1.0.3",
243 "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
244 "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
245 "dev": true,
246 "license": "Apache-2.0",
247 "peerDependencies": {
248 "bare-buffer": "*"
249 },
250 "peerDependenciesMeta": {
251 "bare-buffer": {
252 "optional": true
253 }
254 }
255 },
256 "node_modules/bare-env": {
257 "version": "3.0.0",
258 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
259 "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
260 "dev": true,
261 "license": "Apache-2.0",
262 "dependencies": {
263 "bare-os": "^3.0.1"
264 }
265 },
266 "node_modules/bare-events": {
267 "version": "2.8.2",
268 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
269 "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
270 "dev": true,
271 "license": "Apache-2.0",
272 "peerDependencies": {
273 "bare-abort-controller": "*"
274 },
275 "peerDependenciesMeta": {
276 "bare-abort-controller": {
277 "optional": true
278 }
279 }
280 },
281 "node_modules/bare-format": {
282 "version": "1.0.2",
283 "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.2.tgz",
284 "integrity": "sha512-GswdhnOnP9QtwRbrf4wLApw5widkaLMsLe2XOs35fQD2YfEN1ApoGka+cZ7PfvzxMgfYXmMhj/2OGlVn5/Dxgw==",
285 "dev": true,
286 "license": "Apache-2.0",
287 "dependencies": {
288 "bare-inspect": "^3.0.0"
289 }
290 },
291 "node_modules/bare-fs": {
292 "version": "4.7.0",
293 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.0.tgz",
294 "integrity": "sha512-xzqKsCFxAek9aezYhjJuJRXBIaYlg/0OGDTZp+T8eYmYMlm66cs6cYko02drIyjN2CBbi+I6L7YfXyqpqtKRXA==",
295 "dev": true,
296 "license": "Apache-2.0",
297 "dependencies": {
298 "bare-events": "^2.5.4",
299 "bare-path": "^3.0.0",
300 "bare-stream": "^2.6.4",
301 "bare-url": "^2.2.2",
302 "fast-fifo": "^1.3.2"
303 },
304 "engines": {
305 "bare": ">=1.16.0"
306 },
307 "peerDependencies": {
308 "bare-buffer": "*"
309 },
310 "peerDependenciesMeta": {
311 "bare-buffer": {
312 "optional": true
313 }
314 }
315 },
316 "node_modules/bare-hrtime": {
317 "version": "2.1.1",
318 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
319 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
320 "dev": true,
321 "license": "Apache-2.0"
322 },
323 "node_modules/bare-http-parser": {
324 "version": "1.1.3",
325 "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.1.3.tgz",
326 "integrity": "sha512-+dhVvQi6brHq14L/XHNRQ+TLuVE76VjRmMt61wVEtS+Od8xUslfMHWJN/ZjIIt3RtTG6vPuA+x9cOh7KrkBJsA==",
327 "dev": true,
328 "license": "Apache-2.0"
329 },
330 "node_modules/bare-http1": {
331 "version": "4.5.6",
332 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.5.6.tgz",
333 "integrity": "sha512-31OAwMkSU+z1VuUOCk65hx3aWQgzCfH/zQ6LGxbJtmiy2Czsw0+uvOBM9YkqaL6zUSTSYG2pLbL0v/TjME3Buw==",
334 "dev": true,
335 "license": "Apache-2.0",
336 "dependencies": {
337 "bare-events": "^2.6.0",
338 "bare-http-parser": "^1.1.1",
339 "bare-stream": "^2.10.0",
340 "bare-tcp": "^2.2.0"
341 },
342 "peerDependencies": {
343 "bare-buffer": "*",
344 "bare-url": "*"
345 },
346 "peerDependenciesMeta": {
347 "bare-buffer": {
348 "optional": true
349 },
350 "bare-url": {
351 "optional": true
352 }
353 }
354 },
355 "node_modules/bare-https": {
356 "version": "2.1.3",
357 "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.3.tgz",
358 "integrity": "sha512-0TI/mJXQGYXmG7UUyWEG+KCJusayIAQLywUjFAskDoKuxfqVnGF+M/mTMrEV8J64DaIdU+5x761FvgmT7M68tA==",
359 "dev": true,
360 "license": "Apache-2.0",
361 "dependencies": {
362 "bare-http1": "^4.4.0",
363 "bare-tcp": "^2.2.0",
364 "bare-tls": "^2.0.0"
365 }
366 },
367 "node_modules/bare-inspect": {
368 "version": "3.1.4",
369 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
370 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
371 "dev": true,
372 "license": "Apache-2.0",
373 "dependencies": {
374 "bare-ansi-escapes": "^2.1.0",
375 "bare-type": "^1.0.0"
376 },
377 "engines": {
378 "bare": ">=1.18.0"
379 }
380 },
381 "node_modules/bare-inspector": {
382 "version": "4.0.3",
383 "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-4.0.3.tgz",
384 "integrity": "sha512-yYk6+7tTVKJKHOja5+QBn989ETv8oVsmwoH0nCM+QYC6w5MC03GC4aQNK+lipAZSz1EaAj42WZ8Wewjpy0L9TA==",
385 "dev": true,
386 "license": "Apache-2.0",
387 "dependencies": {
388 "bare-events": "^2.1.0",
389 "bare-http1": "^4.0.0",
390 "bare-stream": "^2.0.0",
391 "bare-url": "^2.0.0",
392 "bare-ws": "^2.0.0"
393 },
394 "engines": {
395 "bare": ">=1.2.0"
396 },
397 "peerDependencies": {
398 "bare-tcp": "*"
399 },
400 "peerDependenciesMeta": {
401 "bare-tcp": {
402 "optional": true
403 }
404 }
405 },
406 "node_modules/bare-module": {
407 "version": "6.1.3",
408 "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz",
409 "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==",
410 "dev": true,
411 "license": "Apache-2.0",
412 "dependencies": {
413 "bare-bundle": "^1.3.0",
414 "bare-module-lexer": "^1.0.0",
415 "bare-module-resolve": "^1.8.0",
416 "bare-path": "^3.0.0",
417 "bare-url": "^2.0.1"
418 },
419 "engines": {
420 "bare": ">=1.23.0"
421 },
422 "peerDependencies": {
423 "bare-buffer": "*"
424 },
425 "peerDependenciesMeta": {
426 "bare-buffer": {
427 "optional": true
428 }
429 }
430 },
431 "node_modules/bare-module-lexer": {
432 "version": "1.4.7",
433 "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
434 "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
435 "dev": true,
436 "license": "Apache-2.0",
437 "dependencies": {
438 "require-addon": "^1.0.2"
439 },
440 "peerDependencies": {
441 "bare-buffer": "*"
442 },
443 "peerDependenciesMeta": {
444 "bare-buffer": {
445 "optional": true
446 }
447 }
448 },
449 "node_modules/bare-module-resolve": {
450 "version": "1.12.1",
451 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz",
452 "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==",
453 "dev": true,
454 "license": "Apache-2.0",
455 "dependencies": {
456 "bare-semver": "^1.0.0"
457 },
458 "peerDependencies": {
459 "bare-url": "*"
460 },
461 "peerDependenciesMeta": {
462 "bare-url": {
463 "optional": true
464 }
465 }
466 },
467 "node_modules/bare-net": {
468 "version": "2.3.1",
469 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.3.1.tgz",
470 "integrity": "sha512-MypSqDKpDU2Xt7FIfazn5yGvRnV09gFcIPHGWstW0gxuzA4tucTcwJSZeos97C4F89vtU5oGwXDN/HrGN6Y4Jw==",
471 "dev": true,
472 "license": "Apache-2.0",
473 "dependencies": {
474 "bare-events": "^2.2.2",
475 "bare-pipe": "^4.0.0",
476 "bare-stream": "^2.0.0",
477 "bare-tcp": "^2.0.0"
478 }
479 },
480 "node_modules/bare-os": {
481 "version": "3.8.7",
482 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.7.tgz",
483 "integrity": "sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w==",
484 "dev": true,
485 "license": "Apache-2.0",
486 "engines": {
487 "bare": ">=1.14.0"
488 }
489 },
490 "node_modules/bare-path": {
491 "version": "3.0.0",
492 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
493 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
494 "dev": true,
495 "license": "Apache-2.0",
496 "dependencies": {
497 "bare-os": "^3.0.1"
498 }
499 },
500 "node_modules/bare-pipe": {
501 "version": "4.1.5",
502 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.5.tgz",
503 "integrity": "sha512-6OfxaG8JSkRh3Gc4hzHRsxNt+yu2PpN7lrv1V+T78GdknWQkVGwiEvu4m+1nbfk8cMVQ0TGxRvQ90XA4rhnTuw==",
504 "dev": true,
505 "license": "Apache-2.0",
506 "dependencies": {
507 "bare-events": "^2.0.0",
508 "bare-stream": "^2.0.0"
509 },
510 "engines": {
511 "bare": ">=1.16.0"
512 }
513 },
514 "node_modules/bare-process": {
515 "version": "4.4.1",
516 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.4.1.tgz",
517 "integrity": "sha512-JfcTtymq6akqM2bdyTsP4A4GYJceBzb91k/ECmFps75uFf6uO33SM1bOZsRAqyRXExabsQJLn5S41NBl8HTM4A==",
518 "dev": true,
519 "license": "Apache-2.0",
520 "dependencies": {
521 "bare-abort": "^2.0.13",
522 "bare-env": "^3.0.0",
523 "bare-events": "^2.3.1",
524 "bare-hrtime": "^2.0.0",
525 "bare-os": "^3.7.1",
526 "bare-signals": "^4.0.0",
527 "bare-stdio": "^1.0.1"
528 }
529 },
530 "node_modules/bare-semver": {
531 "version": "1.0.3",
532 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.3.tgz",
533 "integrity": "sha512-HS/A30bi2+PiRJfU6R4+Kp+6KeLSCSByjYM2iiobOKzLAvtu1CT+S8xWfiU7wz0erknjkUoC+yXy108tzIuP5Q==",
534 "dev": true,
535 "license": "Apache-2.0"
536 },
537 "node_modules/bare-signals": {
538 "version": "4.2.0",
539 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz",
540 "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==",
541 "dev": true,
542 "license": "Apache-2.0",
543 "dependencies": {
544 "bare-events": "^2.5.3",
545 "bare-os": "^3.3.1"
546 },
547 "engines": {
548 "bare": ">=1.7.0"
549 }
550 },
551 "node_modules/bare-stdio": {
552 "version": "1.0.2",
553 "resolved": "https://registry.npmjs.org/bare-stdio/-/bare-stdio-1.0.2.tgz",
554 "integrity": "sha512-3WJDqtvVGP4f+j68kyEC05umOYNwKJ1xG+YAXL8yZ605WgNqiRhVaFq+mVIhBt2eKNp7pa5vCQdhOt1pNh79SA==",
555 "dev": true,
556 "license": "Apache-2.0",
557 "dependencies": {
558 "bare-fs": "^4.5.2",
559 "bare-pipe": "^4.1.5",
560 "bare-tty": "^5.0.3"
561 }
562 },
563 "node_modules/bare-stream": {
564 "version": "2.13.0",
565 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.0.tgz",
566 "integrity": "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==",
567 "dev": true,
568 "license": "Apache-2.0",
569 "dependencies": {
570 "streamx": "^2.25.0",
571 "teex": "^1.0.1"
572 },
573 "peerDependencies": {
574 "bare-abort-controller": "*",
575 "bare-buffer": "*",
576 "bare-events": "*"
577 },
578 "peerDependenciesMeta": {
579 "bare-abort-controller": {
580 "optional": true
581 },
582 "bare-buffer": {
583 "optional": true
584 },
585 "bare-events": {
586 "optional": true
587 }
588 }
589 },
590 "node_modules/bare-stylize": {
591 "version": "0.0.1",
592 "resolved": "https://registry.npmjs.org/bare-stylize/-/bare-stylize-0.0.1.tgz",
593 "integrity": "sha512-l3MjmIl476bWijYWf3RbE+osl4iuXSOMudzp0vAqzIK7gPgn/+G3oAxp8Oin9CFF911KBP0LO9kts8Ci8mGZaQ==",
594 "dev": true,
595 "license": "Apache-2.0",
596 "dependencies": {
597 "bare-ansi-escapes": "^2.2.3",
598 "bare-process": "^4.2.1"
599 }
600 },
601 "node_modules/bare-subprocess": {
602 "version": "5.2.3",
603 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.3.tgz",
604 "integrity": "sha512-07wwswlV7M3sC9IykbZRZ/jHAkrXFWVLqdBWGv1y0ojCimtRD9hGwxdHmR5FUFmDUZLNsBmTYJNQqgio5+A85Q==",
605 "dev": true,
606 "license": "Apache-2.0",
607 "dependencies": {
608 "bare-env": "^3.0.0",
609 "bare-events": "^2.5.4",
610 "bare-os": "^3.0.1",
611 "bare-pipe": "^4.0.0",
612 "bare-url": "^2.2.2"
613 },
614 "engines": {
615 "bare": ">=1.7.0"
616 },
617 "peerDependencies": {
618 "bare-buffer": "*"
619 },
620 "peerDependenciesMeta": {
621 "bare-buffer": {
622 "optional": true
623 }
624 }
625 },
626 "node_modules/bare-tcp": {
627 "version": "2.2.7",
628 "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.7.tgz",
629 "integrity": "sha512-rjpqNQ2cOCkNo3NeYA/W4GTK3DRkl8sDHO3uos+AEswUjLC8XXMQF8WrJCSjlIowCbS6NUVxKE92X5RGXjyefg==",
630 "dev": true,
631 "license": "Apache-2.0",
632 "dependencies": {
633 "bare-dns": "^2.0.4",
634 "bare-events": "^2.5.4",
635 "bare-stream": "^2.6.4"
636 },
637 "engines": {
638 "bare": ">=1.16.0"
639 }
640 },
641 "node_modules/bare-tls": {
642 "version": "2.2.3",
643 "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.2.3.tgz",
644 "integrity": "sha512-dPYBGEXtgLceRFMfGaF2/rqR86/xMxMyrM9ootO/gaRKL/z2uNHJs7aP7IOBtnJF8eUCt5qwMzbKfrKgDIxPLg==",
645 "dev": true,
646 "license": "Apache-2.0",
647 "dependencies": {
648 "bare-net": "^2.0.1",
649 "bare-stream": "^2.6.4"
650 },
651 "engines": {
652 "bare": ">=1.7.0"
653 }
654 },
655 "node_modules/bare-tty": {
656 "version": "5.1.0",
657 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.1.0.tgz",
658 "integrity": "sha512-EZLvW4A+XiJgI3TW+e1pMME9PIJsfEXe/DA/WSKzIkq/v7Yarpv/rvG6Z5pGnpo4V/Bd+qopwnCLSR71hMMBYA==",
659 "dev": true,
660 "license": "Apache-2.0",
661 "dependencies": {
662 "bare-events": "^2.2.0",
663 "bare-signals": "^4.0.0",
664 "bare-stream": "^2.0.0"
665 },
666 "engines": {
667 "bare": ">=1.16.0"
668 }
669 },
670 "node_modules/bare-type": {
671 "version": "1.1.0",
672 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
673 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
674 "dev": true,
675 "license": "Apache-2.0",
676 "engines": {
677 "bare": ">=1.2.0"
678 }
679 },
680 "node_modules/bare-url": {
681 "version": "2.4.0",
682 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz",
683 "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==",
684 "dev": true,
685 "license": "Apache-2.0",
686 "peer": true,
687 "dependencies": {
688 "bare-path": "^3.0.0"
689 }
690 },
691 "node_modules/bare-utils": {
692 "version": "1.6.0",
693 "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.6.0.tgz",
694 "integrity": "sha512-WhQEIkkAxkSnW7u1QgrI0AfNm5JpMruETXeYsb5qnkBJ0TTfNKygZmsh6rkoHBANaV+C/7Jed7bJP9OmEHG7rQ==",
695 "dev": true,
696 "license": "Apache-2.0",
697 "dependencies": {
698 "bare-debug-log": "^2.0.0",
699 "bare-encoding": "^1.0.0",
700 "bare-format": "^1.0.0",
701 "bare-inspect": "^3.0.0",
702 "bare-stylize": "^0.0.1",
703 "bare-type": "^1.0.6"
704 }
705 },
706 "node_modules/bare-v8-to-istanbul": {
707 "version": "1.0.2",
708 "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
709 "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==",
710 "dev": true,
711 "license": "Apache-2.0",
712 "dependencies": {
713 "bare-assert": "^1.0.2",
714 "bare-fs": "^4.1.2",
715 "bare-module": "^6.1.2",
716 "bare-path": "^3.0.0",
717 "bare-process": "^4.2.0",
718 "bare-url": "^2.1.5",
719 "bare-utils": "^1.2.0",
720 "v8-to-istanbul": "^9.3.0",
721 "which-runtime": "^1.2.1"
722 }
723 },
724 "node_modules/bare-ws": {
725 "version": "2.1.0",
726 "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.1.0.tgz",
727 "integrity": "sha512-2gEWlPK9iyBchACdIY6oQXgmDz3KLrChdwrPgmU3IVXOFTnxTqSUT27WE/+izd4QojHj/SsqDkQiD2HDvuTdAA==",
728 "dev": true,
729 "license": "Apache-2.0",
730 "dependencies": {
731 "bare-crypto": "^1.2.0",
732 "bare-events": "^2.3.1",
733 "bare-http1": "^4.0.0",
734 "bare-https": "^2.0.0",
735 "bare-stream": "^2.1.2"
736 },
737 "peerDependencies": {
738 "bare-buffer": "*",
739 "bare-url": "*"
740 },
741 "peerDependenciesMeta": {
742 "bare-buffer": {
743 "optional": true
744 },
745 "bare-url": {
746 "optional": true
747 }
748 }
749 },
750 "node_modules/big-sparse-array": {
751 "version": "1.0.3",
752 "resolved": "https://registry.npmjs.org/big-sparse-array/-/big-sparse-array-1.0.3.tgz",
753 "integrity": "sha512-6RjV/3mSZORlMdpUaQ6rUSpG637cZm0//E54YYGtQg1c1O+AbZP8UTdJ/TchsDZcTVLmyWZcseBfp2HBeXUXOQ==",
754 "dev": true,
755 "license": "MIT"
756 },
757 "node_modules/brittle": {
758 "version": "3.19.1",
759 "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz",
760 "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==",
761 "dev": true,
762 "license": "Apache-2.0",
763 "dependencies": {
764 "b4a": "^1.6.0",
765 "bare-assert": "^1.0.2",
766 "bare-cov": "^1.1.0",
767 "bare-fs": "^4.1.6",
768 "bare-os": "^3.6.1",
769 "bare-path": "^3.0.0",
770 "bare-process": "^4.2.1",
771 "bare-subprocess": "^5.0.0",
772 "bare-url": "^2.1.6",
773 "error-stack-parser": "^2.1.4",
774 "globbie": "^1.0.2",
775 "paparam": "^1.6.2",
776 "same-object": "^1.0.2",
777 "test-tmp": "^1.4.0",
778 "tmatch": "^5.0.0"
779 },
780 "bin": {
781 "brittle": "bin/node.js",
782 "brittle-bare": "bin/bare.js",
783 "brittle-node": "bin/node.js",
784 "brittle-pear": "bin/pear.js"
785 }
786 },
787 "node_modules/compact-encoding": {
788 "version": "2.19.2",
789 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-2.19.2.tgz",
790 "integrity": "sha512-/YjhHQE/5L4F7l5Bht69dRbP9RV6zoJPeowi8bMKQxNKe3Nh6hOY8pBGoVE9fz5GaWfEd8fWJ2aU9sB4KZuMYg==",
791 "dev": true,
792 "license": "Apache-2.0",
793 "dependencies": {
794 "b4a": "^1.3.0"
795 }
796 },
797 "node_modules/convert-source-map": {
798 "version": "2.0.0",
799 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
800 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
801 "dev": true,
802 "license": "MIT"
803 },
804 "node_modules/corestore": {
805 "version": "7.9.2",
806 "resolved": "https://registry.npmjs.org/corestore/-/corestore-7.9.2.tgz",
807 "integrity": "sha512-dyIktVSVLu0skZ1UM4yODbNbZ46dBpTHypzwlwET1btVuS3YIf66Zz8Tx8h3Xa8nbLO2USGqrYtvPYzSRj2wCA==",
808 "dev": true,
809 "license": "MIT",
810 "dependencies": {
811 "b4a": "^1.6.7",
812 "hypercore": "^11.19.0",
813 "hypercore-crypto": "^3.4.2",
814 "hypercore-errors": "^1.4.0",
815 "hypercore-id-encoding": "^1.3.0",
816 "ready-resource": "^1.1.1",
817 "sodium-universal": "^5.0.1",
818 "which-runtime": "^1.2.1"
819 }
820 },
821 "node_modules/device-file": {
822 "version": "2.3.1",
823 "resolved": "https://registry.npmjs.org/device-file/-/device-file-2.3.1.tgz",
824 "integrity": "sha512-bmON44lwxJPle9N2OcH4tqM44pMGZKT8G6OkzXkz0urvqQ9LKkoQdTS6w0ztYfmLdUE27R4UUmx0+y2gEz4Jug==",
825 "dev": true,
826 "license": "Apache-2.0",
827 "dependencies": {
828 "b4a": "^1.6.7",
829 "bare-fs": "^4.0.1",
830 "bare-path": "^3.0.0",
831 "fd-lock": "^2.1.0",
832 "fs-native-extensions": "^1.4.0",
833 "ready-resource": "^1.2.0"
834 }
835 },
836 "node_modules/error-stack-parser": {
837 "version": "2.1.4",
838 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
839 "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
840 "dev": true,
841 "license": "MIT",
842 "dependencies": {
843 "stackframe": "^1.3.4"
844 }
845 },
846 "node_modules/events-universal": {
847 "version": "1.0.1",
848 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
849 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
850 "dev": true,
851 "license": "Apache-2.0",
852 "dependencies": {
853 "bare-events": "^2.7.0"
854 }
855 },
856 "node_modules/fast-fifo": {
857 "version": "1.3.2",
858 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
859 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
860 "dev": true,
861 "license": "MIT"
862 },
863 "node_modules/fd-lock": {
864 "version": "2.1.1",
865 "resolved": "https://registry.npmjs.org/fd-lock/-/fd-lock-2.1.1.tgz",
866 "integrity": "sha512-H3VkcWFl39Rk0xBokDcUyBcVs6VrYTUo/DhDWMzJOF99wXWDAvmvq/GlLTS2NTehsznZhp3fXVyrtB2ldDXhwg==",
867 "dev": true,
868 "license": "Apache-2.0",
869 "dependencies": {
870 "bare-fs": "^4.5.0",
871 "fs-native-extensions": "^1.4.4",
872 "ready-resource": "^1.2.0",
873 "resource-on-exit": "^1.0.0"
874 }
875 },
876 "node_modules/flat-tree": {
877 "version": "1.13.0",
878 "resolved": "https://registry.npmjs.org/flat-tree/-/flat-tree-1.13.0.tgz",
879 "integrity": "sha512-fT3HIuCPwHhFgJ20QYzDHgUG0zMmFg5cHvFiFo5h+QMSJ28TihsEVY0f8HGliuO+pOzmvjMx1odToeaEWkTnyQ==",
880 "dev": true,
881 "license": "MIT"
882 },
883 "node_modules/fs-native-extensions": {
884 "version": "1.4.5",
885 "resolved": "https://registry.npmjs.org/fs-native-extensions/-/fs-native-extensions-1.4.5.tgz",
886 "integrity": "sha512-ekV0T//iDm4AvhOcuPaHpxub4DI7HvY5ucLJVDvi7T2J+NZkQ9S6MuvgP0yeQvoqNUaAGyLjVYb1905BF9bpmg==",
887 "dev": true,
888 "license": "Apache-2.0",
889 "dependencies": {
890 "require-addon": "^1.1.0",
891 "which-runtime": "^1.2.0"
892 }
893 },
894 "node_modules/generate-object-property": {
895 "version": "2.0.0",
896 "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-2.0.0.tgz",
897 "integrity": "sha512-KwuURPyqn2Mz8DdV29pJwQu0Y7tcsbkULr82eeOcY/ZllFK6I9Wm8dsRByIu7CKWlFi9BdW1b3mcXMp/kQBQsw==",
898 "dev": true,
899 "license": "MIT",
900 "dependencies": {
901 "is-property": "^1.0.0"
902 }
903 },
904 "node_modules/generate-string": {
905 "version": "1.0.1",
906 "resolved": "https://registry.npmjs.org/generate-string/-/generate-string-1.0.1.tgz",
907 "integrity": "sha512-IfTY0dKZM43ACyGvXkbG7De7WY7MxTS5VO6Juhe8oJKpCmrYYXoqp/cJMskkpi0k9H8wuXq0H+eI898/BCqvXg==",
908 "dev": true,
909 "license": "MIT"
910 },
911 "node_modules/globbie": {
912 "version": "1.0.3",
913 "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz",
914 "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==",
915 "dev": true,
916 "license": "Apache-2.0",
917 "dependencies": {
918 "bare-fs": "^4.1.2",
919 "bare-path": "^3.0.0",
920 "bare-process": "^4.2.1",
921 "picomatch": "^4.0.2"
922 }
923 },
924 "node_modules/hyper-rooms": {
925 "resolved": "../hyper-rooms",
926 "link": true
927 },
928 "node_modules/hypercore": {
929 "version": "11.28.1",
930 "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-11.28.1.tgz",
931 "integrity": "sha512-d8jqUilG7Gri8wT+sRdkFTYRPMHGyvBz/NCYHf5rX7+LoivRNRxQm5nWChT7caiUKbcsDak6h7IiEQ5wbD1wCg==",
932 "dev": true,
933 "license": "MIT",
934 "dependencies": {
935 "@hyperswarm/secret-stream": "^6.0.0",
936 "b4a": "^1.1.0",
937 "bare-events": "^2.2.0",
938 "big-sparse-array": "^1.0.3",
939 "compact-encoding": "^2.11.0",
940 "fast-fifo": "^1.3.0",
941 "flat-tree": "^1.9.0",
942 "hypercore-crypto": "^3.2.1",
943 "hypercore-errors": "^1.5.0",
944 "hypercore-id-encoding": "^1.2.0",
945 "hypercore-storage": "^2.8.0",
946 "is-options": "^1.0.1",
947 "nanoassert": "^2.0.0",
948 "protomux": "^3.5.0",
949 "quickbit-universal": "^2.2.0",
950 "random-array-iterator": "^1.0.0",
951 "safety-catch": "^1.0.1",
952 "sodium-universal": "^5.0.1",
953 "streamx": "^2.12.4",
954 "unslab": "^1.3.0",
955 "z32": "^1.0.0"
956 }
957 },
958 "node_modules/hypercore-crypto": {
959 "version": "3.6.1",
960 "resolved": "https://registry.npmjs.org/hypercore-crypto/-/hypercore-crypto-3.6.1.tgz",
961 "integrity": "sha512-ltIz2uDwy9pO/ZGTvqcjzyBkvt6O4cVm4r/nNxh0GFs/RbQtqP/i4wCvLEdmU7ptgtnw7fI67WYD1aHPuv4OVA==",
962 "dev": true,
963 "license": "MIT",
964 "dependencies": {
965 "b4a": "^1.6.6",
966 "compact-encoding": "^2.15.0",
967 "sodium-universal": "^5.0.0"
968 }
969 },
970 "node_modules/hypercore-errors": {
971 "version": "1.5.0",
972 "resolved": "https://registry.npmjs.org/hypercore-errors/-/hypercore-errors-1.5.0.tgz",
973 "integrity": "sha512-5KQ/SuDxsvet+7qWA35Ay6zdD9WyAHQoyWHGcPUTbmJBd300gvNIJoi3oma7kp4TTCSzii6qYumNZe/s0j/saQ==",
974 "dev": true,
975 "license": "Apache-2.0",
976 "dependencies": {
977 "hypercore-id-encoding": "^1.3.0"
978 }
979 },
980 "node_modules/hypercore-id-encoding": {
981 "version": "1.3.0",
982 "resolved": "https://registry.npmjs.org/hypercore-id-encoding/-/hypercore-id-encoding-1.3.0.tgz",
983 "integrity": "sha512-W6sHdGo5h7LXEsoWfKf/KfuROZmZRQDlGqJF2EPHW+noCK66Vvr0+zE6cL0vqQi18s0kQPeN7Sq3QyR0Ytc2VQ==",
984 "dev": true,
985 "license": "Apache-2.0",
986 "dependencies": {
987 "b4a": "^1.5.3",
988 "z32": "^1.0.0"
989 }
990 },
991 "node_modules/hypercore-storage": {
992 "version": "2.8.0",
993 "resolved": "https://registry.npmjs.org/hypercore-storage/-/hypercore-storage-2.8.0.tgz",
994 "integrity": "sha512-s35HllFMYvrkSHvkIIklVFmCI/bRVNO7H3pi4mKvWl0XLyhGGGM+NtJ7fpsh82oPRCmL9XtMRO+pT6LiMAO8MA==",
995 "dev": true,
996 "license": "Apache-2.0",
997 "dependencies": {
998 "b4a": "^1.6.7",
999 "bare-fs": "^4.0.1",
1000 "bare-path": "^3.0.0",
1001 "compact-encoding": "^2.16.0",
1002 "device-file": "^2.1.2",
1003 "flat-tree": "^1.12.1",
1004 "hypercore-crypto": "^3.4.2",
1005 "hyperschema": "^1.7.0",
1006 "index-encoder": "^3.3.2",
1007 "resolve-reject-promise": "^1.0.0",
1008 "rocksdb-native": "^3.11.0",
1009 "scope-lock": "^1.2.4",
1010 "streamx": "^2.21.1"
1011 }
1012 },
1013 "node_modules/hyperschema": {
1014 "version": "1.20.1",
1015 "resolved": "https://registry.npmjs.org/hyperschema/-/hyperschema-1.20.1.tgz",
1016 "integrity": "sha512-7gnvaTNUs8FqdrU3NNE6nzhOneGyDlUlpROu9YFlHA61fE8ppKKf2gLZjZjsx/enNt+VFN4ITxy8ijypfyWBLw==",
1017 "dev": true,
1018 "license": "Apache-2.0",
1019 "dependencies": {
1020 "bare-fs": "^4.0.1",
1021 "compact-encoding": "^2.19.0",
1022 "generate-object-property": "^2.0.0",
1023 "generate-string": "^1.0.1"
1024 }
1025 },
1026 "node_modules/index-encoder": {
1027 "version": "3.5.0",
1028 "resolved": "https://registry.npmjs.org/index-encoder/-/index-encoder-3.5.0.tgz",
1029 "integrity": "sha512-idZ1cxtZz2dRV6rUiaP9Xo99UjXbSzjcMacoQmxUMu/A7fEQcNPngvwDJYeWelQUS5XFlY71/or70lKn6XnwbQ==",
1030 "dev": true,
1031 "license": "Apache-2.0",
1032 "dependencies": {
1033 "b4a": "^1.6.4"
1034 }
1035 },
1036 "node_modules/is-options": {
1037 "version": "1.0.2",
1038 "resolved": "https://registry.npmjs.org/is-options/-/is-options-1.0.2.tgz",
1039 "integrity": "sha512-u+Ai74c8Q74aS8BuHwPdI1jptGOT1FQXgCq8/zv0xRuE+wRgSMEJLj8lVO8Zp9BeGb29BXY6AsNPinfqjkr7Fg==",
1040 "dev": true,
1041 "license": "MIT",
1042 "dependencies": {
1043 "b4a": "^1.1.1"
1044 }
1045 },
1046 "node_modules/is-property": {
1047 "version": "1.0.2",
1048 "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
1049 "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
1050 "dev": true,
1051 "license": "MIT"
1052 },
1053 "node_modules/nanoassert": {
1054 "version": "2.0.0",
1055 "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz",
1056 "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==",
1057 "dev": true,
1058 "license": "ISC"
1059 },
1060 "node_modules/noise-curve-ed": {
1061 "version": "2.1.0",
1062 "resolved": "https://registry.npmjs.org/noise-curve-ed/-/noise-curve-ed-2.1.0.tgz",
1063 "integrity": "sha512-zAzJx+VwZM3w6EA1hTmDhJfvAnCeBQn/1FAeZ0LtGxCcCtlAK/uJXQVF/eDVUOaAZ286lHlx77WJ+qj9SmsRRg==",
1064 "dev": true,
1065 "license": "ISC",
1066 "dependencies": {
1067 "b4a": "^1.1.0",
1068 "nanoassert": "^2.0.0",
1069 "sodium-universal": "^5.0.0"
1070 }
1071 },
1072 "node_modules/noise-handshake": {
1073 "version": "4.2.0",
1074 "resolved": "https://registry.npmjs.org/noise-handshake/-/noise-handshake-4.2.0.tgz",
1075 "integrity": "sha512-9O/VTNX/E2/AToyMTTDU0J/4WhaXMTdqc2DHs9vf+snoZ0cenSBq0dNYTVV1snYYEkmo6QeRrYMxtqtoYnY+LA==",
1076 "dev": true,
1077 "license": "Apache-2.0",
1078 "dependencies": {
1079 "b4a": "^1.1.0",
1080 "nanoassert": "^2.0.0",
1081 "sodium-universal": "^5.0.0"
1082 }
1083 },
1084 "node_modules/paparam": {
1085 "version": "1.10.1",
1086 "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.1.tgz",
1087 "integrity": "sha512-viyQI64VIja0Za3njIzhoEP8ZVkgPowhZPuG0E96NwBfYJ6ZIyrrlhWGtFPkdN7eYLl2L8CTWL+wla0evm1KKQ==",
1088 "dev": true,
1089 "license": "Apache-2.0"
1090 },
1091 "node_modules/picomatch": {
1092 "version": "4.0.4",
1093 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
1094 "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
1095 "dev": true,
1096 "license": "MIT",
1097 "engines": {
1098 "node": ">=12"
1099 },
1100 "funding": {
1101 "url": "https://github.com/sponsors/jonschlinkert"
1102 }
1103 },
1104 "node_modules/protomux": {
1105 "version": "3.10.1",
1106 "resolved": "https://registry.npmjs.org/protomux/-/protomux-3.10.1.tgz",
1107 "integrity": "sha512-jgBqx8ZyaBWea/DFG4eOu1scOaeBwcnagiRC1XFVrjeGt7oAb0Pk5udPpBUpJ4DJBRjra50jD6YcZiQQTRqaaA==",
1108 "dev": true,
1109 "license": "MIT",
1110 "dependencies": {
1111 "b4a": "^1.3.1",
1112 "compact-encoding": "^2.5.1",
1113 "queue-tick": "^1.0.0",
1114 "safety-catch": "^1.0.1",
1115 "unslab": "^1.3.0"
1116 }
1117 },
1118 "node_modules/queue-tick": {
1119 "version": "1.0.1",
1120 "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
1121 "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
1122 "dev": true,
1123 "license": "MIT"
1124 },
1125 "node_modules/quickbit-native": {
1126 "version": "2.4.8",
1127 "resolved": "https://registry.npmjs.org/quickbit-native/-/quickbit-native-2.4.8.tgz",
1128 "integrity": "sha512-FcCcqI+nIAWGknqhtrYT5TSD7t/N+Xd8ctM+2PrIIBuwOi5hx0SxAvuPtzLIEMfT/2h9+fhBakUe2uALOHX6yw==",
1129 "dev": true,
1130 "license": "Apache-2.0",
1131 "optional": true,
1132 "dependencies": {
1133 "require-addon": "^1.1.0"
1134 }
1135 },
1136 "node_modules/quickbit-universal": {
1137 "version": "2.2.0",
1138 "resolved": "https://registry.npmjs.org/quickbit-universal/-/quickbit-universal-2.2.0.tgz",
1139 "integrity": "sha512-w02i1R8n7+6pEKTud8DfF8zbFY9o7RtPlUc3jWbtCkDKvhbx/AvV7oNnz4/TcmsPGpSJS+fq5Ud6RH6+YPvSGg==",
1140 "dev": true,
1141 "license": "ISC",
1142 "dependencies": {
1143 "b4a": "^1.6.0",
1144 "simdle-universal": "^1.1.0"
1145 },
1146 "optionalDependencies": {
1147 "quickbit-native": "^2.2.0"
1148 }
1149 },
1150 "node_modules/random-array-iterator": {
1151 "version": "1.0.0",
1152 "resolved": "https://registry.npmjs.org/random-array-iterator/-/random-array-iterator-1.0.0.tgz",
1153 "integrity": "sha512-u7xCM93XqKEvPTP6xZp2ehttcAemKnh73oKNf1FvzuVCfpt6dILDt1Kxl1LeBjm2iNIeR49VGFhy4Iz3yOun+Q==",
1154 "dev": true,
1155 "license": "MIT"
1156 },
1157 "node_modules/ready-resource": {
1158 "version": "1.2.0",
1159 "resolved": "https://registry.npmjs.org/ready-resource/-/ready-resource-1.2.0.tgz",
1160 "integrity": "sha512-nfcco/8iAFV0M+2PYnmIc+/xY0iRb35d42HFHQ7AfjulbGEAFa+XWpByfwSyeVeiBoMLLFVMv1HixxNCqzSQ1g==",
1161 "dev": true,
1162 "license": "MIT",
1163 "dependencies": {
1164 "bare-events": "^2.2.0"
1165 }
1166 },
1167 "node_modules/refcounter": {
1168 "version": "1.0.0",
1169 "resolved": "https://registry.npmjs.org/refcounter/-/refcounter-1.0.0.tgz",
1170 "integrity": "sha512-1WosVzUy0kPUaPMEtlNDwm99UsteALIhXXR8rerELoa63WkYIXAl0hxgwPFrIYBRWZPGUyekQ04FRtPJ7dHk9w==",
1171 "dev": true,
1172 "license": "Apache-2.0"
1173 },
1174 "node_modules/require-addon": {
1175 "version": "1.2.0",
1176 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
1177 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
1178 "dev": true,
1179 "license": "Apache-2.0",
1180 "dependencies": {
1181 "bare-addon-resolve": "^1.3.0"
1182 },
1183 "engines": {
1184 "bare": ">=1.10.0"
1185 }
1186 },
1187 "node_modules/resolve-reject-promise": {
1188 "version": "1.1.0",
1189 "resolved": "https://registry.npmjs.org/resolve-reject-promise/-/resolve-reject-promise-1.1.0.tgz",
1190 "integrity": "sha512-LWsTOA91AqzBTjSGgX79Tc130pwcBK6xjpJEO+qRT5IKZ6bGnHKcc8QL3upUBcWuU8OTIDzKK2VNSwmmlqvAVg==",
1191 "dev": true,
1192 "license": "MIT"
1193 },
1194 "node_modules/resource-on-exit": {
1195 "version": "1.0.0",
1196 "resolved": "https://registry.npmjs.org/resource-on-exit/-/resource-on-exit-1.0.0.tgz",
1197 "integrity": "sha512-ViJwJAknCkLRJRPR+9SISQQ7R5eRgtdIHLJsM2hHx1MweAJbJxJ5XnMjjq0Lc7ZGv44ufzAqds1nKxiVkdy4ag==",
1198 "dev": true,
1199 "license": "Apache-2.0"
1200 },
1201 "node_modules/rocksdb-native": {
1202 "version": "3.15.0",
1203 "resolved": "https://registry.npmjs.org/rocksdb-native/-/rocksdb-native-3.15.0.tgz",
1204 "integrity": "sha512-czYx0hOzKAy+HYmJROZm7thvJ1tHFWHNkGI3nomumXWzrJghJdrWo4MIhehGX4OTff0lnOCoB2zu6ekzIKY0rQ==",
1205 "dev": true,
1206 "license": "Apache-2.0",
1207 "dependencies": {
1208 "compact-encoding": "^2.15.0",
1209 "ready-resource": "^1.0.0",
1210 "refcounter": "^1.0.0",
1211 "require-addon": "^1.0.2",
1212 "resolve-reject-promise": "^1.1.0",
1213 "signal-promise": "^1.0.3",
1214 "streamx": "^2.16.1"
1215 },
1216 "engines": {
1217 "bare": ">=1.16.0"
1218 }
1219 },
1220 "node_modules/safety-catch": {
1221 "version": "1.0.3",
1222 "resolved": "https://registry.npmjs.org/safety-catch/-/safety-catch-1.0.3.tgz",
1223 "integrity": "sha512-Zq+J1TefpoEq/HTUabo0YXX5MNvttjWYODGohgPBO2jfko8Wqx3JYMgE823szDFVamdH5PlpByvfiWScTdSYDA==",
1224 "dev": true,
1225 "license": "MIT"
1226 },
1227 "node_modules/same-object": {
1228 "version": "1.0.2",
1229 "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz",
1230 "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==",
1231 "dev": true,
1232 "license": "MIT"
1233 },
1234 "node_modules/scope-lock": {
1235 "version": "1.2.4",
1236 "resolved": "https://registry.npmjs.org/scope-lock/-/scope-lock-1.2.4.tgz",
1237 "integrity": "sha512-BpSd8VCuCxW9ZitcdIC/vjs3gMaP9bRBL5nkHcyfX2VrS52n13/rHuBA2xJ/S/4DPuRdAO/Bk8pWd8eD/gHCIA==",
1238 "dev": true,
1239 "license": "Apache-2.0"
1240 },
1241 "node_modules/signal-promise": {
1242 "version": "1.0.3",
1243 "resolved": "https://registry.npmjs.org/signal-promise/-/signal-promise-1.0.3.tgz",
1244 "integrity": "sha512-WBgv0UnIq2C+Aeh0/n+IRpP6967eIx9WpynTUoiW3isPpfe1zu2LJzyfXdo9Tgef8yR/sGjcMvoUXD7EYdiz+g==",
1245 "dev": true,
1246 "license": "MIT"
1247 },
1248 "node_modules/simdle-native": {
1249 "version": "1.3.9",
1250 "resolved": "https://registry.npmjs.org/simdle-native/-/simdle-native-1.3.9.tgz",
1251 "integrity": "sha512-Isc8sP4OiiIU0mpslD4GHEnR0VQWvR/54WN7YtwEDkNdTJVWtpmvsSvsgRlw5BNGxdYXlVRegdnrSu10H/PhvA==",
1252 "dev": true,
1253 "license": "Apache-2.0",
1254 "optional": true,
1255 "dependencies": {
1256 "b4a": "^1.6.0",
1257 "require-addon": "^1.1.0"
1258 }
1259 },
1260 "node_modules/simdle-universal": {
1261 "version": "1.1.2",
1262 "resolved": "https://registry.npmjs.org/simdle-universal/-/simdle-universal-1.1.2.tgz",
1263 "integrity": "sha512-3n3w1bs+uwgHKQjt6arez83EywNlhZzYvNOhvAASTl/8KqNIcqr6aHyGt3JRlfuUC7iB0tomJRPlJ2cRGIpBzA==",
1264 "dev": true,
1265 "license": "ISC",
1266 "dependencies": {
1267 "b4a": "^1.6.0"
1268 },
1269 "optionalDependencies": {
1270 "simdle-native": "^1.1.1"
1271 }
1272 },
1273 "node_modules/sodium-native": {
1274 "version": "5.1.0",
1275 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.1.0.tgz",
1276 "integrity": "sha512-3RxgyWyJlhTsABPnJVpCI5CoTDANZTqqFrEPqr+kjfnRaBihpVtMUE3yTF40ukdoB1APXeoBNKF3MzZAIHg39g==",
1277 "dev": true,
1278 "license": "MIT",
1279 "dependencies": {
1280 "bare-assert": "^1.2.0",
1281 "require-addon": "^1.1.0",
1282 "which-runtime": "^1.2.1"
1283 },
1284 "engines": {
1285 "bare": ">=1.16.0"
1286 }
1287 },
1288 "node_modules/sodium-secretstream": {
1289 "version": "1.2.0",
1290 "resolved": "https://registry.npmjs.org/sodium-secretstream/-/sodium-secretstream-1.2.0.tgz",
1291 "integrity": "sha512-q/DbraNFXm1KfCiiZvapmz5UC3OlpirYFIvBK2MhGaOFSb3gRyk8OXTi17UI9SGfshQNCpsVvlopogbzZNyW6Q==",
1292 "dev": true,
1293 "license": "MIT",
1294 "dependencies": {
1295 "b4a": "^1.1.1",
1296 "sodium-universal": "^5.0.0"
1297 }
1298 },
1299 "node_modules/sodium-universal": {
1300 "version": "5.0.1",
1301 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1302 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1303 "dev": true,
1304 "license": "MIT",
1305 "dependencies": {
1306 "sodium-native": "^5.0.1"
1307 },
1308 "peerDependencies": {
1309 "sodium-javascript": "~0.8.0"
1310 },
1311 "peerDependenciesMeta": {
1312 "sodium-javascript": {
1313 "optional": true
1314 }
1315 }
1316 },
1317 "node_modules/stackframe": {
1318 "version": "1.3.4",
1319 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
1320 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
1321 "dev": true,
1322 "license": "MIT"
1323 },
1324 "node_modules/streamx": {
1325 "version": "2.25.0",
1326 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
1327 "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
1328 "dev": true,
1329 "license": "MIT",
1330 "dependencies": {
1331 "events-universal": "^1.0.0",
1332 "fast-fifo": "^1.3.2",
1333 "text-decoder": "^1.1.0"
1334 }
1335 },
1336 "node_modules/teex": {
1337 "version": "1.0.1",
1338 "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
1339 "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
1340 "dev": true,
1341 "license": "MIT",
1342 "dependencies": {
1343 "streamx": "^2.12.5"
1344 }
1345 },
1346 "node_modules/test-tmp": {
1347 "version": "1.4.0",
1348 "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz",
1349 "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==",
1350 "dev": true,
1351 "license": "MIT",
1352 "dependencies": {
1353 "bare-fs": "^4.0.1",
1354 "bare-os": "^3.3.0",
1355 "bare-path": "^3.0.0"
1356 }
1357 },
1358 "node_modules/text-decoder": {
1359 "version": "1.2.7",
1360 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
1361 "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
1362 "dev": true,
1363 "license": "Apache-2.0",
1364 "dependencies": {
1365 "b4a": "^1.6.4"
1366 }
1367 },
1368 "node_modules/timeout-refresh": {
1369 "version": "2.0.1",
1370 "resolved": "https://registry.npmjs.org/timeout-refresh/-/timeout-refresh-2.0.1.tgz",
1371 "integrity": "sha512-SVqEcMZBsZF9mA78rjzCrYrUs37LMJk3ShZ851ygZYW1cMeIjs9mL57KO6Iv5mmjSQnOe/29/VAfGXo+oRCiVw==",
1372 "dev": true,
1373 "license": "MIT"
1374 },
1375 "node_modules/tmatch": {
1376 "version": "5.0.0",
1377 "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz",
1378 "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==",
1379 "dev": true,
1380 "license": "ISC",
1381 "engines": {
1382 "node": ">=8"
1383 }
1384 },
1385 "node_modules/unslab": {
1386 "version": "1.3.0",
1387 "resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz",
1388 "integrity": "sha512-YATkfKAFj47kTzmiQrWXMyRvaVrHsW6MEALa4bm+FhiA2YG4oira+Z3DXN6LrYOYn2Y8eO94Lwl9DOHjs1FpoQ==",
1389 "dev": true,
1390 "license": "Apache-2.0",
1391 "dependencies": {
1392 "b4a": "^1.6.6"
1393 }
1394 },
1395 "node_modules/v8-to-istanbul": {
1396 "version": "9.3.0",
1397 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
1398 "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
1399 "dev": true,
1400 "license": "ISC",
1401 "dependencies": {
1402 "@jridgewell/trace-mapping": "^0.3.12",
1403 "@types/istanbul-lib-coverage": "^2.0.1",
1404 "convert-source-map": "^2.0.0"
1405 },
1406 "engines": {
1407 "node": ">=10.12.0"
1408 }
1409 },
1410 "node_modules/which-runtime": {
1411 "version": "1.3.2",
1412 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
1413 "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
1414 "dev": true,
1415 "license": "Apache-2.0"
1416 },
1417 "node_modules/z32": {
1418 "version": "1.1.0",
1419 "resolved": "https://registry.npmjs.org/z32/-/z32-1.1.0.tgz",
1420 "integrity": "sha512-1WUHy+VS6d0HPNspDxvLssBbeQjXMjSnpv0vH82vRAUfg847NmX3OXozp/hRP5jPhxBbrVzrgvAt+UsGNzRFQQ==",
1421 "dev": true,
1422 "license": "MIT",
1423 "dependencies": {
1424 "b4a": "^1.5.3"
1425 }
1426 }
1427 }
1428}
package.json +18 -0
……@@ -0,0 +1,18 @@
1{
2 "name": "hyper-orgs",
3 "version": "0.2.0",
4 "description": "P2P organization management — a hyper-rooms schema for orgs, members, resources, grants, invites, and access requests",
5 "type": "module",
6 "main": "index.js",
7 "scripts": {
8 "test": "brittle test/*.test.js"
9 },
10 "dependencies": {
11 "hyper-rooms": "file:../hyper-rooms"
12 },
13 "devDependencies": {
14 "autobase-test-helpers": "^2.0.0",
15 "brittle": "^3.0.0",
16 "corestore": "^7.0.0"
17 }
18}
test/basic.test.js +582 -0
……@@ -0,0 +1,582 @@
1import test from 'brittle'
2import Corestore from 'corestore'
3import { mkdtempSync } from 'fs'
4import { join } from 'path'
5import { tmpdir } from 'os'
6import { replicateAndSync } from 'autobase-test-helpers'
7import { HyperOrgs } from '../index.js'
8
9function tmpStore () {
10 const dir = mkdtempSync(join(tmpdir(), 'hyper-orgs-test-'))
11 return new Corestore(dir)
12}
13
14async function makeOrgs (t, namespace = 'test') {
15 const store = tmpStore()
16 const orgs = new HyperOrgs(store, { namespace })
17 await orgs.ready()
18 t.teardown(async () => {
19 try { await orgs.close() } catch {}
20 try { await store.close() } catch {}
21 })
22 return orgs
23}
24
25// ── Org CRUD ──────────────────────────────────────────────────
26
27test('create and get organization', async (t) => {
28 const orgs = await makeOrgs(t, 'test-1')
29
30 const org = await orgs.create('my-team', 'group', {
31 displayName: 'My Team',
32 description: 'A test team'
33 })
34
35 t.is(org.name, 'my-team')
36 t.is(org.displayName, 'My Team')
37 t.is(org.description, 'A test team')
38 t.is(org.type, 'group')
39 t.is(org.visibility, 'public')
40 t.alike(org.resources, [])
41})
42
43test('update organization', async (t) => {
44 const orgs = await makeOrgs(t, 'test-2')
45
46 await orgs.create('team', 'group')
47 const updated = await orgs.update('team', {
48 displayName: 'Team Alpha',
49 description: 'Updated'
50 })
51
52 t.is(updated.displayName, 'Team Alpha')
53 t.is(updated.description, 'Updated')
54})
55
56test('delete empty org', async (t) => {
57 const orgs = await makeOrgs(t, 'test-3')
58
59 await orgs.create('empty-team', 'group')
60 await orgs.delete('empty-team')
61
62 const deleted = await orgs.get('empty-team')
63 t.is(deleted, null)
64})
65
66test('cannot delete org with resources', async (t) => {
67 const orgs = await makeOrgs(t, 'test-4')
68
69 await orgs.create('team', 'group')
70 await orgs.addResource('team', 'project')
71
72 try {
73 await orgs.delete('team')
74 t.fail('should have thrown')
75 } catch (err) {
76 t.ok(err.message.includes('still has'))
77 }
78})
79
80test('reserved name rejected', async (t) => {
81 const orgs = await makeOrgs(t, 'test-5')
82
83 try {
84 await orgs.create('admin', 'group')
85 t.fail('should have thrown')
86 } catch (err) {
87 t.ok(err.message.includes('reserved'))
88 }
89})
90
91test('invalid name rejected', async (t) => {
92 const orgs = await makeOrgs(t, 'test-6')
93
94 try {
95 await orgs.create('-bad-', 'group')
96 t.fail('should have thrown')
97 } catch (err) {
98 t.ok(err.message.toLowerCase().includes('name'))
99 }
100})
101
102test('list orgs with filters', async (t) => {
103 const orgs = await makeOrgs(t, 'test-7')
104
105 await orgs.create('team-a', 'group')
106 await orgs.create('team-b', 'group', { visibility: 'private' })
107 await orgs.create('alice', 'user', { userId: 'alice-key' })
108
109 const all = await orgs.list()
110 t.is(all.length, 3)
111
112 const groups = await orgs.list({ type: 'group' })
113 t.is(groups.length, 2)
114
115 const users = await orgs.list({ type: 'user' })
116 t.is(users.length, 1)
117
118 const priv = await orgs.list({ visibility: 'private' })
119 t.is(priv.length, 1)
120})
121
122// ── Members ───────────────────────────────────────────────────
123
124test('add and remove members', async (t) => {
125 const orgs = await makeOrgs(t, 'test-8')
126
127 await orgs.create('team', 'group')
128 await orgs.addMember('team', 'alice', 'admin')
129 await orgs.addMember('team', 'bob', 'member')
130
131 let org = await orgs.get('team')
132 t.is(org.members.length, 2)
133 t.is(org.members[0].userId, 'alice')
134 t.is(org.members[0].role, 'admin')
135
136 await orgs.removeMember('team', 'bob')
137 org = await orgs.get('team')
138 t.is(org.members.length, 1)
139})
140
141test('update member role', async (t) => {
142 const orgs = await makeOrgs(t, 'test-9')
143
144 await orgs.create('team', 'group')
145 await orgs.addMember('team', 'alice', 'member')
146 await orgs.addMember('team', 'alice', 'admin') // upgrade
147
148 const org = await orgs.get('team')
149 t.is(org.members.length, 1)
150 t.is(org.members[0].role, 'admin')
151})
152
153test('cannot add members to user org', async (t) => {
154 const orgs = await makeOrgs(t, 'test-10')
155
156 await orgs.create('alice', 'user', { userId: 'alice-key' })
157 try {
158 await orgs.addMember('alice', 'bob', 'member')
159 t.fail('should have thrown')
160 } catch (err) {
161 t.ok(err.message.includes('group'))
162 }
163})
164
165test('getUserOrgs', async (t) => {
166 const orgs = await makeOrgs(t, 'test-11')
167
168 await orgs.create('alice-ns', 'user', { userId: 'alice' })
169 await orgs.create('team', 'group')
170 await orgs.create('other', 'group')
171 await orgs.addMember('team', 'alice', 'admin')
172
173 const aliceOrgs = await orgs.getUserOrgs('alice')
174 t.is(aliceOrgs.length, 2) // her personal ns + team
175})
176
177// ── Resources ─────────────────────────────────────────────────
178
179test('add and remove resources', async (t) => {
180 const orgs = await makeOrgs(t, 'test-12')
181
182 await orgs.create('team', 'group')
183 await orgs.addResource('team', 'project-a')
184 await orgs.addResource('team', 'project-b')
185
186 let org = await orgs.get('team')
187 t.is(org.resources.length, 2)
188 t.ok(org.resources.includes('project-a'))
189
190 await orgs.removeResource('team', 'project-a')
191 org = await orgs.get('team')
192 t.is(org.resources.length, 1)
193 t.is(org.resources[0], 'project-b')
194})
195
196test('move resource between orgs', async (t) => {
197 const orgs = await makeOrgs(t, 'test-13')
198
199 await orgs.create('team-a', 'group')
200 await orgs.create('team-b', 'group')
201 await orgs.addResource('team-a', 'project')
202
203 await orgs.moveResource('project', 'team-a', 'team-b')
204
205 const a = await orgs.get('team-a')
206 const b = await orgs.get('team-b')
207 t.is(a.resources.length, 0)
208 t.is(b.resources.length, 1)
209 t.is(b.resources[0], 'project')
210})
211
212test('adding a resource moves it from any previous owner', async (t) => {
213 const orgs = await makeOrgs(t, 'test-14')
214
215 await orgs.create('team-a', 'group')
216 await orgs.create('team-b', 'group')
217 await orgs.addResource('team-a', 'shared-repo')
218
219 // Add to team-b directly — should remove from team-a (apply-side logic)
220 await orgs.addResource('team-b', 'shared-repo')
221
222 const a = await orgs.get('team-a')
223 const b = await orgs.get('team-b')
224 t.is(a.resources.length, 0)
225 t.is(b.resources.length, 1)
226})
227
228// ── Grants ────────────────────────────────────────────────────
229
230test('grants', async (t) => {
231 const orgs = await makeOrgs(t, 'test-15')
232
233 await orgs.create('team', 'group')
234
235 const grant = await orgs.createGrant({
236 subjectType: 'user',
237 subjectId: 'alice',
238 grantType: 'group',
239 groupName: 'team',
240 permission: 'write'
241 })
242
243 t.ok(grant.grantId)
244 t.is(grant.permission, 'write')
245 t.is(grant.revokedAt, null)
246
247 const grants = await orgs.listGrants({ subjectType: 'user', subjectId: 'alice' })
248 t.is(grants.length, 1)
249
250 await orgs.revokeGrant(grant.grantId, 'admin')
251 const revoked = await orgs.getGrant(grant.grantId)
252 t.ok(revoked.revokedAt)
253 t.is(revoked.revokedBy, 'admin')
254})
255
256test('listGrants filters revoked by default', async (t) => {
257 const orgs = await makeOrgs(t, 'test-16')
258
259 await orgs.create('team', 'group')
260 const g1 = await orgs.createGrant({
261 subjectType: 'user', subjectId: 'alice',
262 grantType: 'group', groupName: 'team', permission: 'write'
263 })
264 await orgs.createGrant({
265 subjectType: 'user', subjectId: 'alice',
266 grantType: 'group', groupName: 'team', permission: 'moderator'
267 })
268 await orgs.revokeGrant(g1.grantId)
269
270 const active = await orgs.listGrants({ subjectType: 'user', subjectId: 'alice' })
271 t.is(active.length, 1)
272
273 const all = await orgs.listGrants({
274 subjectType: 'user', subjectId: 'alice', activeOnly: false
275 })
276 t.is(all.length, 2)
277})
278
279// ── Invites ───────────────────────────────────────────────────
280
281test('invites', async (t) => {
282 const orgs = await makeOrgs(t, 'test-17')
283
284 await orgs.create('team', 'group')
285
286 const invite = await orgs.createInvite({
287 inviteType: 'group',
288 targetGroup: 'team',
289 permission: 'write',
290 maxUses: 2
291 })
292
293 t.ok(invite.inviteId)
294 t.is(invite.usedCount, 0)
295 t.is(invite.maxUses, 2)
296
297 const grant = await orgs.consumeInvite(invite.inviteId, {
298 subjectType: 'user',
299 subjectId: 'bob'
300 })
301
302 t.ok(grant)
303 t.is(grant.permission, 'write')
304 t.is(grant.groupName, 'team')
305
306 const updatedInvite = await orgs.getInvite(invite.inviteId)
307 t.is(updatedInvite.usedCount, 1)
308})
309
310test('revoked invite cannot be consumed', async (t) => {
311 const orgs = await makeOrgs(t, 'test-18')
312
313 await orgs.create('team', 'group')
314 const invite = await orgs.createInvite({
315 inviteType: 'group', targetGroup: 'team', permission: 'write'
316 })
317 await orgs.revokeInvite(invite.inviteId)
318
319 try {
320 await orgs.consumeInvite(invite.inviteId, {
321 subjectType: 'user', subjectId: 'bob'
322 })
323 t.fail('should have thrown')
324 } catch (err) {
325 t.ok(err.message.toLowerCase().includes('revoked'))
326 }
327})
328
329// ── Access Requests ───────────────────────────────────────────
330
331test('access request approve flow', async (t) => {
332 const orgs = await makeOrgs(t, 'test-19')
333
334 await orgs.create('team', 'group')
335
336 const request = await orgs.createAccessRequest({
337 subjectType: 'user',
338 subjectId: 'charlie',
339 requestType: 'group',
340 groupName: 'team',
341 permission: 'write',
342 reason: 'Want to contribute'
343 })
344
345 t.is(request.status, 'pending')
346
347 const grant = await orgs.approveAccessRequest(request.requestId, 'admin')
348 t.ok(grant)
349 t.is(grant.permission, 'write')
350 t.is(grant.groupName, 'team')
351
352 const updatedReq = await orgs.getAccessRequest(request.requestId)
353 t.is(updatedReq.status, 'approved')
354})
355
356test('access request deny flow', async (t) => {
357 const orgs = await makeOrgs(t, 'test-20')
358
359 await orgs.create('team', 'group')
360
361 const request = await orgs.createAccessRequest({
362 subjectType: 'user',
363 subjectId: 'charlie',
364 requestType: 'group',
365 groupName: 'team',
366 permission: 'write'
367 })
368
369 await orgs.denyAccessRequest(request.requestId, 'admin', 'Not yet')
370 const updated = await orgs.getAccessRequest(request.requestId)
371 t.is(updated.status, 'denied')
372 t.is(updated.denyReason, 'Not yet')
373})
374
375test('duplicate pending access request rejected', async (t) => {
376 const orgs = await makeOrgs(t, 'test-21')
377
378 await orgs.create('team', 'group')
379 await orgs.createAccessRequest({
380 subjectType: 'user', subjectId: 'charlie',
381 requestType: 'group', groupName: 'team', permission: 'write'
382 })
383
384 try {
385 await orgs.createAccessRequest({
386 subjectType: 'user', subjectId: 'charlie',
387 requestType: 'group', groupName: 'team', permission: 'write'
388 })
389 t.fail('should have thrown')
390 } catch (err) {
391 t.ok(err.message.toLowerCase().includes('pending'))
392 }
393})
394
395// ── Access Checking ───────────────────────────────────────────
396
397test('checkAccess — denied when no grants', async (t) => {
398 const orgs = await makeOrgs(t, 'test-22')
399
400 await orgs.create('team', 'group')
401 await orgs.addResource('team', 'repo-a')
402
403 const denied = await orgs.checkAccess({
404 userId: 'alice',
405 resourceName: 'repo-a',
406 requiredPermission: 'write'
407 })
408 t.is(denied, false)
409})
410
411test('checkAccess — group grant allows resource access', async (t) => {
412 const orgs = await makeOrgs(t, 'test-23')
413
414 await orgs.create('team', 'group')
415 await orgs.addResource('team', 'repo-a')
416
417 await orgs.createGrant({
418 subjectType: 'user',
419 subjectId: 'alice',
420 grantType: 'group',
421 groupName: 'team',
422 permission: 'write'
423 })
424
425 const allowed = await orgs.checkAccess({
426 userId: 'alice',
427 resourceName: 'repo-a',
428 requiredPermission: 'write'
429 })
430 t.is(allowed, true)
431})
432
433test('checkAccess — direct resource grant', async (t) => {
434 const orgs = await makeOrgs(t, 'test-24')
435
436 await orgs.create('team', 'group')
437 await orgs.addResource('team', 'repo-a')
438
439 await orgs.createGrant({
440 subjectType: 'user',
441 subjectId: 'alice',
442 grantType: 'resource',
443 resourceName: 'repo-a',
444 permission: 'owner'
445 })
446
447 const allowed = await orgs.checkAccess({
448 userId: 'alice',
449 resourceName: 'repo-a',
450 requiredPermission: 'moderator'
451 })
452 t.is(allowed, true)
453})
454
455test('checkAccess — server grants', async (t) => {
456 const orgs = await makeOrgs(t, 'test-25')
457
458 await orgs.create('team', 'group')
459 await orgs.addResource('team', 'repo-a')
460
461 await orgs.createGrant({
462 subjectType: 'server',
463 subjectId: 'serverkey123',
464 grantType: 'resource',
465 resourceName: 'repo-a',
466 permission: 'write'
467 })
468
469 const allowed = await orgs.checkAccess({
470 serverKey: 'serverkey123',
471 resourceName: 'repo-a',
472 requiredPermission: 'write'
473 })
474 t.is(allowed, true)
475})
476
477// ── Meta ──────────────────────────────────────────────────────
478
479test('meta key-value storage', async (t) => {
480 const orgs = await makeOrgs(t, 'test-26')
481
482 await orgs.setMeta('homepage', 'https://example.com')
483 await orgs.setMeta('tagline', 'p2p forge')
484
485 t.is(await orgs.getMeta('homepage'), 'https://example.com')
486 t.is(await orgs.getMeta('tagline'), 'p2p forge')
487 t.is(await orgs.getMeta('missing'), null)
488})
489
490// ── Keys ──────────────────────────────────────────────────────
491
492test('key properties', async (t) => {
493 const orgs = await makeOrgs(t, 'test-27')
494
495 t.ok(orgs.key, 'has key')
496 t.is(typeof orgs.keyHex, 'string')
497 t.is(orgs.keyHex.length, 64, 'keyHex is 32 bytes hex')
498 t.ok(orgs.localKey, 'has localKey')
499 t.is(orgs.writable, true, 'first writer is writable')
500})
501
502// ── Multi-writer replication ──────────────────────────────────
503
504test('two orgs instances replicate via autobase', async (t) => {
505 const storeA = tmpStore()
506 const storeB = tmpStore()
507
508 const orgsA = new HyperOrgs(storeA, { namespace: 'shared' })
509 await orgsA.ready()
510
511 // A creates an org and a grant
512 await orgsA.create('team', 'group', { displayName: 'Team A' })
513 await orgsA.addMember('team', 'alice', 'admin')
514 await orgsA.addResource('team', 'repo-1')
515 await orgsA.base.update()
516
517 const orgsB = new HyperOrgs(storeB, {
518 namespace: 'shared',
519 bootstrapKey: orgsA.keyHex
520 })
521 await orgsB.ready()
522
523 await replicateAndSync([orgsA.base, orgsB.base])
524
525 // B should see A's data
526 const org = await orgsB.get('team')
527 t.ok(org, 'B received the org')
528 t.is(org.displayName, 'Team A')
529 t.is(org.members.length, 1)
530 t.is(org.members[0].userId, 'alice')
531 t.alike(org.resources, ['repo-1'])
532
533 t.teardown(async () => {
534 try { await orgsA.close() } catch {}
535 try { await orgsB.close() } catch {}
536 try { await storeA.close() } catch {}
537 try { await storeB.close() } catch {}
538 })
539})
540
541test('two writers can both append after addWriter', async (t) => {
542 const storeA = tmpStore()
543 const storeB = tmpStore()
544
545 const orgsA = new HyperOrgs(storeA, { namespace: 'shared2' })
546 await orgsA.ready()
547
548 const orgsB = new HyperOrgs(storeB, {
549 namespace: 'shared2',
550 bootstrapKey: orgsA.keyHex
551 })
552 await orgsB.ready()
553
554 // Initial sync so B knows the room structure
555 await replicateAndSync([orgsA.base, orgsB.base])
556
557 // A authorizes B as a writer
558 await orgsA.addWriter('bob', { writerKey: orgsB.localKeyHex, role: 'writer' })
559 await replicateAndSync([orgsA.base, orgsB.base])
560
561 t.ok(orgsB.writable, 'B is now writable')
562
563 // Each creates a distinct org (no ID collision — orgs are keyed by name)
564 await orgsA.create('team-alice', 'group')
565 await orgsB.create('team-bob', 'group')
566
567 await replicateAndSync([orgsA.base, orgsB.base])
568
569 const allA = await orgsA.list()
570 const allB = await orgsB.list()
571
572 t.is(allA.length, 2, 'A sees both orgs')
573 t.is(allB.length, 2, 'B sees both orgs')
574 t.alike(allA.map(o => o.name).sort(), ['team-alice', 'team-bob'])
575
576 t.teardown(async () => {
577 try { await orgsA.close() } catch {}
578 try { await orgsB.close() } catch {}
579 try { await storeA.close() } catch {}
580 try { await storeB.close() } catch {}
581 })
582})