| … | … | @@ -0,0 +1,15 @@ |
| 1 | # Dependencies | |
| 2 | node_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 | |
| 15 | coverage/ |
| … | … | @@ -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 | ||
| 28 | import Corestore from 'corestore' | |
| 29 | import { HyperOrgs } from '../index.js' | |
| 30 | ||
| 31 | const storagePath = process.env.HYPER_ORGS_STORE || './.hyper-orgs-cli' | |
| 32 | const actor = process.env.USER || 'me' | |
| 33 | const args = process.argv.slice(2) | |
| 34 | const cmd = args[0] | |
| 35 | ||
| 36 | async 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 | ||
| 44 | async function close ({ orgs, store }) { | |
| 45 | await orgs.close() | |
| 46 | await store.close() | |
| 47 | } | |
| 48 | ||
| 49 | function 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 | ||
| 64 | async 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 | ||
| 350 | main().catch((err) => { console.error(err.message); process.exit(1) }) |
| … | … | @@ -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 | ||
| 19 | import http from 'http' | |
| 20 | import Corestore from 'corestore' | |
| 21 | import b4a from 'b4a' | |
| 22 | import { HyperOrgs } from '../index.js' | |
| 23 | ||
| 24 | const args = process.argv.slice(2) | |
| 25 | function getFlag (name) { | |
| 26 | const i = args.indexOf('--' + name) | |
| 27 | return i !== -1 && args[i + 1] ? args[i + 1] : null | |
| 28 | } | |
| 29 | ||
| 30 | const port = parseInt(getFlag('port')) || 9097 | |
| 31 | const storagePath = getFlag('storage') || './.hyper-orgs-web' | |
| 32 | ||
| 33 | const store = new Corestore(storagePath) | |
| 34 | await store.ready() | |
| 35 | const orgs = new HyperOrgs(store, { namespace: 'orgs' }) | |
| 36 | await orgs.ready() | |
| 37 | ||
| 38 | function escapeHtml (s) { | |
| 39 | return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') | |
| 40 | } | |
| 41 | ||
| 42 | function 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 | ||
| 51 | const 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 | ||
| 152 | function 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 | ||
| 187 | function 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 | ||
| 239 | function 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 | ||
| 309 | httpServer.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 | }) |
| … | … | @@ -0,0 +1,6 @@ |
| 1 | /** | |
| 2 | * hyper-orgs — P2P organization management on hyper-rooms | |
| 3 | */ | |
| 4 | ||
| 5 | export { HyperOrgs } from './lib/hyper-orgs.js' | |
| 6 | export { orgsSchema, RESERVED_NAMES, validateName } from './lib/schema.js' |
| … | … | @@ -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 | ||
| 26 | import { Room } from 'hyper-rooms' | |
| 27 | import { orgsSchema, validateName } from './schema.js' | |
| 28 | ||
| 29 | export 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 | } |
| … | … | @@ -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 | ||
| 29 | import { jsonSchema } from 'hyper-rooms' | |
| 30 | ||
| 31 | // Reserved names that cannot be used as org names | |
| 32 | export 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 | ||
| 38 | export 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 | ||
| 49 | export 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 | }) |
| … | … | @@ -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 | } |
| … | … | @@ -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 | } |
| … | … | @@ -0,0 +1,582 @@ |
| 1 | import test from 'brittle' | |
| 2 | import Corestore from 'corestore' | |
| 3 | import { mkdtempSync } from 'fs' | |
| 4 | import { join } from 'path' | |
| 5 | import { tmpdir } from 'os' | |
| 6 | import { replicateAndSync } from 'autobase-test-helpers' | |
| 7 | import { HyperOrgs } from '../index.js' | |
| 8 | ||
| 9 | function tmpStore () { | |
| 10 | const dir = mkdtempSync(join(tmpdir(), 'hyper-orgs-test-')) | |
| 11 | return new Corestore(dir) | |
| 12 | } | |
| 13 | ||
| 14 | async 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 | ||
| 27 | test('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 | ||
| 43 | test('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 | ||
| 56 | test('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 | ||
| 66 | test('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 | ||
| 80 | test('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 | ||
| 91 | test('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 | ||
| 102 | test('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 | ||
| 124 | test('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 | ||
| 141 | test('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 | ||
| 153 | test('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 | ||
| 165 | test('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 | ||
| 179 | test('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 | ||
| 196 | test('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 | ||
| 212 | test('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 | ||
| 230 | test('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 | ||
| 256 | test('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 | ||
| 281 | test('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 | ||
| 310 | test('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 | ||
| 331 | test('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 | ||
| 356 | test('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 | ||
| 375 | test('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 | ||
| 397 | test('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 | ||
| 411 | test('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 | ||
| 433 | test('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 | ||
| 455 | test('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 | ||
| 479 | test('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 | ||
| 492 | test('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 | ||
| 504 | test('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 | ||
| 541 | test('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 | }) |