OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-social commits / 5c35042

Initial commit — P2P social ecosystem
12 projects (bridges, panels, daemons), 6805 LOC, 627 tests. bridge-irc live on Libera #ai-town. Design docs for graph navigation, community emergence, sheaf theory, and Blacksky case study. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5c350423facb3ac6fc9876ad79e4945ee406eb17
by plankatron <fleeky@gmail.com> · 2026-02-21T02:08:09.000Z

Changed files (127) +29446 -0

.gitignore +1 -0
……@@ -0,0 +1 @@
1node_modules/
bridge-discord/index.js +73 -0
……@@ -0,0 +1,73 @@
1import { Bridge } from './lib/bridge.js'
2
3const args = parseArgs(typeof Pear !== 'undefined' ? Pear.config.args : process.argv.slice(2))
4
5if (args.help) {
6 console.log(`
7bridge-discord — Discord ↔ Keet bidirectional bridge
8
9Usage:
10 bridge-discord --token <bot-token> --channel <channelId> --keet-room <roomId>
11
12Options:
13 --token <token> Discord bot token (required)
14 --channel <id> Discord channel ID to bridge (required)
15 --guild <id> Discord guild/server ID (optional)
16 --keet-room <roomId> Keet room ID to bridge (required)
17 --keet-port <port> Keet daemon port (default: 3117)
18 --help Show this help
19
20Environment:
21 DISCORD_TOKEN Alternative to --token flag
22
23Setup:
24 1. Create a Discord bot at https://discord.com/developers/applications
25 2. Enable MESSAGE CONTENT intent in Bot settings
26 3. Invite bot to your server with permissions: Send Messages, Read Message History
27 4. Get channel ID (right-click channel → Copy Channel ID with Developer Mode on)
28`)
29 process.exit(0)
30}
31
32const token = args.token || process.env.DISCORD_TOKEN
33if (!token || !args.channel || !args['keet-room']) {
34 console.error('Error: --token (or DISCORD_TOKEN env), --channel, and --keet-room are required')
35 console.error('Run with --help for usage')
36 process.exit(1)
37}
38
39const bridge = new Bridge({
40 discordToken: token,
41 discordChannel: args.channel,
42 discordGuild: args.guild,
43 keetRoom: args['keet-room'],
44 keetPort: parseInt(args['keet-port'] || '3117')
45})
46
47bridge.start().catch((err) => {
48 console.error('Bridge failed to start:', err.message)
49 process.exit(1)
50})
51
52process.on('SIGINT', () => bridge.stop().then(() => process.exit(0)))
53process.on('SIGTERM', () => bridge.stop().then(() => process.exit(0)))
54
55function parseArgs (argv) {
56 const result = {}
57 for (let i = 0; i < argv.length; i++) {
58 const arg = argv[i]
59 if (arg === '--help') {
60 result.help = true
61 } else if (arg.startsWith('--')) {
62 const key = arg.slice(2)
63 const next = argv[i + 1]
64 if (next && !next.startsWith('--')) {
65 result[key] = next
66 i++
67 } else {
68 result[key] = true
69 }
70 }
71 }
72 return result
73}
bridge-discord/lib/adapter.js +99 -0
……@@ -0,0 +1,99 @@
1import { createEnvelope, TYPES, PLATFORMS } from 'bridge-envelope'
2
3function discordToEnvelope (message) {
4 // Skip bot messages
5 if (message.author.bot) return null
6
7 const author = message.author.tag || message.author.username
8 const channel = message.channel.name || message.channelId
9 const content = message.content || ''
10
11 // Detect message type
12 let type = TYPES.TEXT
13 let mediaUrl = ''
14 let mediaType = ''
15 let replyTo = ''
16
17 // Attachments
18 const attachment = message.attachments?.first?.()
19 if (attachment) {
20 type = TYPES.MEDIA
21 mediaUrl = attachment.url
22 mediaType = attachment.contentType || ''
23 }
24
25 // Reply
26 if (message.reference?.messageId) {
27 replyTo = message.reference.messageId
28 if (type === TYPES.TEXT) type = TYPES.REPLY
29 }
30
31 return createEnvelope({
32 type,
33 platform: PLATFORMS.DISCORD,
34 author,
35 content,
36 channel,
37 replyTo,
38 mediaUrl,
39 mediaType
40 })
41}
42
43function keetToDiscord (keetMsg, bridgeIdentity) {
44 if (keetMsg.memberId === bridgeIdentity) return null
45 if (keetMsg.system) return null
46
47 const name = keetMsg.name || 'anonymous'
48 const text = keetMsg.text || ''
49 if (!text.trim()) return null
50
51 // Format for Discord — bold name prefix
52 return {
53 name,
54 text: `**${name}** (Keet): ${text}`
55 }
56}
57
58function envelopeToDiscordContent (envelope) {
59 const platform = platformName(envelope.platform)
60 const prefix = platform !== 'discord' ? `[${platform}] ` : ''
61 const author = envelope.author || 'anonymous'
62
63 switch (envelope.type) {
64 case TYPES.TEXT:
65 case TYPES.REPLY:
66 return `${prefix}**${author}**: ${envelope.content}`
67 case TYPES.MEDIA:
68 return `${prefix}**${author}**: ${envelope.content || ''}\n${envelope.mediaUrl || ''}`
69 case TYPES.REACTION:
70 return `${prefix}*${author} reacted: ${envelope.content}*`
71 case TYPES.SYSTEM:
72 return `*${prefix}${envelope.content}*`
73 default:
74 return `${prefix}**${author}**: ${envelope.content}`
75 }
76}
77
78function discordReactionToEnvelope (reaction, user) {
79 return createEnvelope({
80 type: TYPES.REACTION,
81 platform: PLATFORMS.DISCORD,
82 author: user.tag || user.username,
83 content: reaction.emoji.name || reaction.emoji.toString(),
84 channel: reaction.message.channel.name || reaction.message.channelId,
85 replyTo: reaction.message.id
86 })
87}
88
89function platformName (id) {
90 const names = ['keet', 'irc', 'discord', 'matrix', 'mastodon', 'email', 'android']
91 return names[id] || 'unknown'
92}
93
94export {
95 discordToEnvelope,
96 keetToDiscord,
97 envelopeToDiscordContent,
98 discordReactionToEnvelope
99}
bridge-discord/lib/bridge.js +146 -0
……@@ -0,0 +1,146 @@
1import { DiscordClient } from './discord-client.js'
2import { KeetClient } from './keet-client.js'
3import { Router } from './router.js'
4import { discordToEnvelope, keetToDiscord } from './adapter.js'
5
6class Bridge {
7 constructor (opts) {
8 this.discordToken = opts.discordToken
9 this.discordChannel = opts.discordChannel
10 this.discordGuild = opts.discordGuild || null
11
12 this.keetRoom = opts.keetRoom
13 this.keetHost = opts.keetHost || 'localhost'
14 this.keetPort = opts.keetPort || 3117
15
16 this._discord = null
17 this._keet = null
18 this._router = null
19 this._running = false
20 this._bridgeIdentity = null
21
22 this.onLog = opts.onLog || console.log
23 }
24
25 async start () {
26 this.onLog('[bridge] Starting Discord ↔ Keet bridge')
27 this.onLog(`[bridge] Discord channel: ${this.discordChannel}`)
28 this.onLog(`[bridge] Keet room: ${this.keetRoom}`)
29
30 // Initialize Keet client
31 this._keet = new KeetClient({
32 host: this.keetHost,
33 port: this.keetPort
34 })
35
36 try {
37 const identity = await this._keet.identity()
38 this._bridgeIdentity = identity.memberId || identity.id
39 this.onLog(`[bridge] Keet identity: ${identity.displayName || identity.username || identity.name || 'unknown'}`)
40 } catch (err) {
41 this.onLog(`[bridge] Warning: could not get Keet identity: ${err.message}`)
42 }
43
44 // Initialize router
45 this._router = new Router({
46 onIrcMessage: (name, text) => {
47 // "IrcMessage" is a misnomer inherited from shared router — sends to Discord
48 this._discord.send(`**${name}** (Keet): ${text}`).catch((err) => {
49 this.onLog(`[bridge] Discord send error: ${err.message}`)
50 })
51 },
52 onKeetMessage: (nick, text) => {
53 this._keet.send(this.keetRoom, `[Discord] <${nick}> ${text}`).catch((err) => {
54 this.onLog(`[bridge] Keet send error: ${err.message}`)
55 })
56 }
57 })
58
59 // Initialize Discord client
60 this._discord = new DiscordClient({
61 token: this.discordToken,
62 channelId: this.discordChannel,
63 guildId: this.discordGuild,
64 onMessage: (message) => {
65 const author = message.author.tag || message.author.username
66 const text = message.content
67
68 // Skip bridged messages
69 if (text.startsWith('**') && text.includes('(Keet):')) return
70
71 this.onLog(`[discord→keet] <${author}> ${text}`)
72
73 // Handle attachments
74 let fullText = text
75 for (const [, att] of message.attachments) {
76 fullText += (fullText ? '\n' : '') + att.url
77 }
78
79 this._router.routeToKeet(author, fullText)
80 },
81 onReaction: (reaction, user) => {
82 const emoji = reaction.emoji.name || '?'
83 const author = user.tag || user.username
84 this.onLog(`[discord→keet] ${author} reacted ${emoji}`)
85 },
86 onError: (err) => {
87 this.onLog(`[bridge] Discord error: ${err.message}`)
88 }
89 })
90
91 // Connect to Discord
92 await this._discord.connect()
93 this.onLog(`[bridge] Connected to Discord as ${this._discord.botUser?.tag || 'bot'}`)
94
95 // Verify Keet daemon
96 try {
97 const status = await this._keet.status()
98 this.onLog(`[bridge] Keet daemon online (${status.rooms || '?'} rooms)`)
99 } catch (err) {
100 this.onLog(`[bridge] Warning: Keet daemon unreachable: ${err.message}`)
101 }
102
103 // Start watching Keet room
104 this._startKeetWatch()
105
106 this._running = true
107 this.onLog('[bridge] Bridge active — messages flowing bidirectionally')
108 }
109
110 _startKeetWatch () {
111 this._keet.watch(this.keetRoom, (msg) => {
112 const relayed = keetToDiscord(msg, this._bridgeIdentity)
113 if (!relayed) return
114
115 // Skip bridged messages
116 if (relayed.text.includes('[Discord]')) return
117
118 this.onLog(`[keet→discord] ${relayed.text}`)
119 this._router.routeToIrc(relayed.name, msg.text)
120 })
121 }
122
123 async stop () {
124 this.onLog('[bridge] Shutting down...')
125 this._running = false
126
127 if (this._keet) this._keet.stopWatch()
128 if (this._discord) this._discord.disconnect()
129
130 this.onLog('[bridge] Stopped')
131 }
132
133 get running () {
134 return this._running
135 }
136
137 get stats () {
138 return {
139 running: this._running,
140 discordConnected: this._discord?.connected || false,
141 deduped: this._router?.seenCount || 0
142 }
143 }
144}
145
146export { Bridge }
bridge-discord/lib/discord-client.js +98 -0
……@@ -0,0 +1,98 @@
1import { Client, GatewayIntentBits, Events } from 'discord.js'
2
3class DiscordClient {
4 constructor (opts) {
5 this.token = opts.token
6 this.channelId = opts.channelId
7 this.guildId = opts.guildId || null
8
9 this._client = null
10 this._channel = null
11 this._connected = false
12 this._onMessage = opts.onMessage || null
13 this._onReaction = opts.onReaction || null
14 this._onError = opts.onError || null
15 }
16
17 connect () {
18 return new Promise((resolve, reject) => {
19 this._client = new Client({
20 intents: [
21 GatewayIntentBits.Guilds,
22 GatewayIntentBits.GuildMessages,
23 GatewayIntentBits.MessageContent,
24 GatewayIntentBits.GuildMessageReactions
25 ]
26 })
27
28 this._client.once(Events.ClientReady, async () => {
29 this._connected = true
30
31 // Find channel
32 this._channel = this._client.channels.cache.get(this.channelId)
33 if (!this._channel) {
34 try {
35 this._channel = await this._client.channels.fetch(this.channelId)
36 } catch (err) {
37 return reject(new Error(`Channel ${this.channelId} not found: ${err.message}`))
38 }
39 }
40
41 resolve()
42 })
43
44 this._client.on(Events.MessageCreate, (message) => {
45 if (message.channelId !== this.channelId) return
46 if (message.author.id === this._client.user.id) return // skip self
47 if (this._onMessage) this._onMessage(message)
48 })
49
50 this._client.on(Events.MessageReactionAdd, (reaction, user) => {
51 if (reaction.message.channelId !== this.channelId) return
52 if (user.id === this._client.user.id) return
53 if (this._onReaction) this._onReaction(reaction, user)
54 })
55
56 this._client.on(Events.Error, (err) => {
57 if (this._onError) this._onError(err)
58 })
59
60 this._client.login(this.token).catch(reject)
61 })
62 }
63
64 async send (text) {
65 if (!this._connected || !this._channel) return
66 await this._channel.send(text)
67 }
68
69 async sendEmbed (opts) {
70 if (!this._connected || !this._channel) return
71 await this._channel.send({ embeds: [opts] })
72 }
73
74 async sendFile (url, text) {
75 if (!this._connected || !this._channel) return
76 await this._channel.send({
77 content: text || '',
78 files: [url]
79 })
80 }
81
82 disconnect () {
83 if (this._client) {
84 this._client.destroy()
85 this._connected = false
86 }
87 }
88
89 get connected () {
90 return this._connected
91 }
92
93 get botUser () {
94 return this._client?.user || null
95 }
96}
97
98export { DiscordClient }
bridge-discord/lib/keet-client.js +166 -0
……@@ -0,0 +1,166 @@
1import http from 'http'
2import net from 'net'
3
4class KeetClient {
5 constructor (opts = {}) {
6 this.host = opts.host || 'localhost'
7 this.port = opts.port || 3117
8 this._watching = false
9 this._watchAbort = null
10 }
11
12 // Raw TCP request — bypasses Node's strict HTTP parser
13 // Needed because keet-daemon sends duplicate Content-Length headers
14 _rawRequest (method, path, body) {
15 return new Promise((resolve, reject) => {
16 const socket = net.connect(this.port, this.host)
17 let payload = ''
18
19 if (body) {
20 payload = JSON.stringify(body)
21 }
22
23 socket.on('connect', () => {
24 let req = `${method} ${path} HTTP/1.1\r\n`
25 req += `Host: ${this.host}:${this.port}\r\n`
26 if (payload) {
27 req += `Content-Type: application/json\r\n`
28 req += `Content-Length: ${Buffer.byteLength(payload)}\r\n`
29 }
30 req += `Connection: close\r\n`
31 req += `\r\n`
32 if (payload) req += payload
33 socket.write(req)
34 })
35
36 let data = ''
37 socket.on('data', (chunk) => { data += chunk.toString() })
38
39 socket.on('end', () => {
40 // Split headers from body
41 const idx = data.indexOf('\r\n\r\n')
42 if (idx === -1) return reject(new Error('No HTTP body'))
43 const jsonBody = data.slice(idx + 4)
44
45 try {
46 const parsed = JSON.parse(jsonBody)
47 if (!parsed.ok) reject(new Error(parsed.error || 'RPC error'))
48 else resolve(parsed.data)
49 } catch {
50 reject(new Error('Invalid JSON: ' + jsonBody.slice(0, 200)))
51 }
52 })
53
54 socket.on('error', reject)
55 socket.setTimeout(10000, () => {
56 socket.destroy(new Error('Request timeout'))
57 })
58 })
59 }
60
61 rpc (method, params = {}) {
62 return this._rawRequest('POST', '/rpc', { method, params })
63 }
64
65 async send (roomId, text) {
66 return this.rpc('send', { roomId, text })
67 }
68
69 async messages (roomId, limit = 20) {
70 return this._rawRequest('GET', `/rpc/messages/${roomId}?limit=${limit}`)
71 }
72
73 async rooms () {
74 return this._rawRequest('GET', '/rpc/rooms')
75 }
76
77 async identity () {
78 return this._rawRequest('GET', '/rpc/identity')
79 }
80
81 async status () {
82 return this._rawRequest('GET', '/rpc/status')
83 }
84
85 async members (roomId) {
86 return this._rawRequest('GET', `/rpc/members/${roomId}`)
87 }
88
89 watch (roomId, onMessage) {
90 if (this._watching) return
91
92 this._watching = true
93 this._doWatch(roomId, onMessage)
94 }
95
96 _doWatch (roomId, onMessage) {
97 if (!this._watching) return
98
99 const req = http.request({
100 hostname: this.host,
101 port: this.port,
102 path: `/rpc/watch/${roomId}`,
103 method: 'GET',
104 headers: { Accept: 'text/event-stream' }
105 }, (res) => {
106 let buffer = ''
107
108 res.on('data', (chunk) => {
109 buffer += chunk.toString()
110 const events = buffer.split('\n\n')
111 buffer = events.pop() // keep incomplete tail
112
113 for (const event of events) {
114 if (!event.trim()) continue
115 const lines = event.split('\n')
116 let type = 'message'
117 let data = null
118
119 for (const line of lines) {
120 if (line.startsWith('event: ')) type = line.slice(7)
121 if (line.startsWith('data: ')) {
122 try {
123 data = JSON.parse(line.slice(6))
124 } catch {}
125 }
126 }
127
128 if (type === 'message' && data) {
129 onMessage(data)
130 }
131 }
132 })
133
134 res.on('end', () => {
135 if (this._watching) {
136 setTimeout(() => this._doWatch(roomId, onMessage), 2000)
137 }
138 })
139
140 res.on('error', () => {
141 if (this._watching) {
142 setTimeout(() => this._doWatch(roomId, onMessage), 5000)
143 }
144 })
145 })
146
147 req.on('error', () => {
148 if (this._watching) {
149 setTimeout(() => this._doWatch(roomId, onMessage), 5000)
150 }
151 })
152
153 this._watchAbort = req
154 req.end()
155 }
156
157 stopWatch () {
158 this._watching = false
159 if (this._watchAbort) {
160 this._watchAbort.destroy()
161 this._watchAbort = null
162 }
163 }
164}
165
166export { KeetClient }
bridge-discord/lib/router.js +70 -0
……@@ -0,0 +1,70 @@
1import { createHash } from 'crypto'
2
3class Router {
4 constructor (opts = {}) {
5 this._seen = new Map()
6 this._maxSeen = opts.maxSeen || 10000
7 this._ttl = opts.ttl || 60000 // 1 minute dedup window
8 this._onIrcMessage = opts.onIrcMessage || null
9 this._onKeetMessage = opts.onKeetMessage || null
10 }
11
12 _hash (text, author) {
13 return createHash('sha256')
14 .update(author || '')
15 .update(text || '')
16 .digest('hex')
17 .slice(0, 16)
18 }
19
20 _isDuplicate (hash) {
21 const now = Date.now()
22
23 // Cleanup old entries
24 if (this._seen.size > this._maxSeen) {
25 for (const [k, ts] of this._seen) {
26 if (now - ts > this._ttl) this._seen.delete(k)
27 }
28 }
29
30 if (this._seen.has(hash)) {
31 const ts = this._seen.get(hash)
32 if (now - ts < this._ttl) return true
33 }
34
35 this._seen.set(hash, now)
36 return false
37 }
38
39 // Called when IRC receives a message — relay to Keet
40 routeToKeet (nick, text) {
41 const hash = this._hash(text, nick)
42 if (this._isDuplicate(hash)) return false
43
44 if (this._onKeetMessage) {
45 this._onKeetMessage(nick, text)
46 }
47 return true
48 }
49
50 // Called when Keet receives a message — relay to IRC
51 routeToIrc (name, text) {
52 const hash = this._hash(text, name)
53 if (this._isDuplicate(hash)) return false
54
55 if (this._onIrcMessage) {
56 this._onIrcMessage(name, text)
57 }
58 return true
59 }
60
61 get seenCount () {
62 return this._seen.size
63 }
64
65 clearSeen () {
66 this._seen.clear()
67 }
68}
69
70export { Router }
bridge-discord/package-lock.json +1175 -0
……@@ -0,0 +1,1175 @@
1{
2 "name": "bridge-discord",
3 "version": "1.0.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "bridge-discord",
9 "version": "1.0.0",
10 "license": "MIT",
11 "dependencies": {
12 "b4a": "^1.0.0",
13 "bridge-envelope": "file:../bridge-envelope",
14 "discord.js": "^14.16.3"
15 },
16 "devDependencies": {
17 "brittle": "^3.0.0"
18 }
19 },
20 "../bridge-envelope": {
21 "version": "1.0.0",
22 "license": "MIT",
23 "dependencies": {
24 "b4a": "^1.0.0",
25 "compact-encoding": "^2.0.0"
26 },
27 "devDependencies": {
28 "brittle": "^3.0.0"
29 }
30 },
31 "node_modules/@discordjs/builders": {
32 "version": "1.13.1",
33 "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz",
34 "integrity": "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==",
35 "license": "Apache-2.0",
36 "dependencies": {
37 "@discordjs/formatters": "^0.6.2",
38 "@discordjs/util": "^1.2.0",
39 "@sapphire/shapeshift": "^4.0.0",
40 "discord-api-types": "^0.38.33",
41 "fast-deep-equal": "^3.1.3",
42 "ts-mixer": "^6.0.4",
43 "tslib": "^2.6.3"
44 },
45 "engines": {
46 "node": ">=16.11.0"
47 },
48 "funding": {
49 "url": "https://github.com/discordjs/discord.js?sponsor"
50 }
51 },
52 "node_modules/@discordjs/collection": {
53 "version": "1.5.3",
54 "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
55 "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
56 "license": "Apache-2.0",
57 "engines": {
58 "node": ">=16.11.0"
59 }
60 },
61 "node_modules/@discordjs/formatters": {
62 "version": "0.6.2",
63 "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz",
64 "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==",
65 "license": "Apache-2.0",
66 "dependencies": {
67 "discord-api-types": "^0.38.33"
68 },
69 "engines": {
70 "node": ">=16.11.0"
71 },
72 "funding": {
73 "url": "https://github.com/discordjs/discord.js?sponsor"
74 }
75 },
76 "node_modules/@discordjs/rest": {
77 "version": "2.6.0",
78 "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz",
79 "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==",
80 "license": "Apache-2.0",
81 "dependencies": {
82 "@discordjs/collection": "^2.1.1",
83 "@discordjs/util": "^1.1.1",
84 "@sapphire/async-queue": "^1.5.3",
85 "@sapphire/snowflake": "^3.5.3",
86 "@vladfrangu/async_event_emitter": "^2.4.6",
87 "discord-api-types": "^0.38.16",
88 "magic-bytes.js": "^1.10.0",
89 "tslib": "^2.6.3",
90 "undici": "6.21.3"
91 },
92 "engines": {
93 "node": ">=18"
94 },
95 "funding": {
96 "url": "https://github.com/discordjs/discord.js?sponsor"
97 }
98 },
99 "node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
100 "version": "2.1.1",
101 "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
102 "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
103 "license": "Apache-2.0",
104 "engines": {
105 "node": ">=18"
106 },
107 "funding": {
108 "url": "https://github.com/discordjs/discord.js?sponsor"
109 }
110 },
111 "node_modules/@discordjs/util": {
112 "version": "1.2.0",
113 "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz",
114 "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
115 "license": "Apache-2.0",
116 "dependencies": {
117 "discord-api-types": "^0.38.33"
118 },
119 "engines": {
120 "node": ">=18"
121 },
122 "funding": {
123 "url": "https://github.com/discordjs/discord.js?sponsor"
124 }
125 },
126 "node_modules/@discordjs/ws": {
127 "version": "1.2.3",
128 "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz",
129 "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==",
130 "license": "Apache-2.0",
131 "dependencies": {
132 "@discordjs/collection": "^2.1.0",
133 "@discordjs/rest": "^2.5.1",
134 "@discordjs/util": "^1.1.0",
135 "@sapphire/async-queue": "^1.5.2",
136 "@types/ws": "^8.5.10",
137 "@vladfrangu/async_event_emitter": "^2.2.4",
138 "discord-api-types": "^0.38.1",
139 "tslib": "^2.6.2",
140 "ws": "^8.17.0"
141 },
142 "engines": {
143 "node": ">=16.11.0"
144 },
145 "funding": {
146 "url": "https://github.com/discordjs/discord.js?sponsor"
147 }
148 },
149 "node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
150 "version": "2.1.1",
151 "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
152 "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
153 "license": "Apache-2.0",
154 "engines": {
155 "node": ">=18"
156 },
157 "funding": {
158 "url": "https://github.com/discordjs/discord.js?sponsor"
159 }
160 },
161 "node_modules/@jridgewell/resolve-uri": {
162 "version": "3.1.2",
163 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
164 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
165 "dev": true,
166 "license": "MIT",
167 "engines": {
168 "node": ">=6.0.0"
169 }
170 },
171 "node_modules/@jridgewell/sourcemap-codec": {
172 "version": "1.5.5",
173 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
174 "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
175 "dev": true,
176 "license": "MIT"
177 },
178 "node_modules/@jridgewell/trace-mapping": {
179 "version": "0.3.31",
180 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
181 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
182 "dev": true,
183 "license": "MIT",
184 "dependencies": {
185 "@jridgewell/resolve-uri": "^3.1.0",
186 "@jridgewell/sourcemap-codec": "^1.4.14"
187 }
188 },
189 "node_modules/@sapphire/async-queue": {
190 "version": "1.5.5",
191 "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
192 "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
193 "license": "MIT",
194 "engines": {
195 "node": ">=v14.0.0",
196 "npm": ">=7.0.0"
197 }
198 },
199 "node_modules/@sapphire/shapeshift": {
200 "version": "4.0.0",
201 "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
202 "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
203 "license": "MIT",
204 "dependencies": {
205 "fast-deep-equal": "^3.1.3",
206 "lodash": "^4.17.21"
207 },
208 "engines": {
209 "node": ">=v16"
210 }
211 },
212 "node_modules/@sapphire/snowflake": {
213 "version": "3.5.3",
214 "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz",
215 "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==",
216 "license": "MIT",
217 "engines": {
218 "node": ">=v14.0.0",
219 "npm": ">=7.0.0"
220 }
221 },
222 "node_modules/@types/istanbul-lib-coverage": {
223 "version": "2.0.6",
224 "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
225 "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
226 "dev": true,
227 "license": "MIT"
228 },
229 "node_modules/@types/node": {
230 "version": "25.2.3",
231 "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz",
232 "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==",
233 "license": "MIT",
234 "dependencies": {
235 "undici-types": "~7.16.0"
236 }
237 },
238 "node_modules/@types/ws": {
239 "version": "8.18.1",
240 "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
241 "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
242 "license": "MIT",
243 "dependencies": {
244 "@types/node": "*"
245 }
246 },
247 "node_modules/@vladfrangu/async_event_emitter": {
248 "version": "2.4.7",
249 "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz",
250 "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==",
251 "license": "MIT",
252 "engines": {
253 "node": ">=v14.0.0",
254 "npm": ">=7.0.0"
255 }
256 },
257 "node_modules/b4a": {
258 "version": "1.7.4",
259 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.4.tgz",
260 "integrity": "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug==",
261 "license": "Apache-2.0",
262 "peerDependencies": {
263 "react-native-b4a": "*"
264 },
265 "peerDependenciesMeta": {
266 "react-native-b4a": {
267 "optional": true
268 }
269 }
270 },
271 "node_modules/bare-addon-resolve": {
272 "version": "1.10.0",
273 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
274 "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
275 "dev": true,
276 "license": "Apache-2.0",
277 "dependencies": {
278 "bare-module-resolve": "^1.10.0",
279 "bare-semver": "^1.0.0"
280 },
281 "peerDependencies": {
282 "bare-url": "*"
283 },
284 "peerDependenciesMeta": {
285 "bare-url": {
286 "optional": true
287 }
288 }
289 },
290 "node_modules/bare-ansi-escapes": {
291 "version": "2.2.3",
292 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
293 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
294 "dev": true,
295 "license": "Apache-2.0",
296 "dependencies": {
297 "bare-stream": "^2.6.5"
298 },
299 "peerDependencies": {
300 "bare-buffer": "*"
301 },
302 "peerDependenciesMeta": {
303 "bare-buffer": {
304 "optional": true
305 }
306 }
307 },
308 "node_modules/bare-assert": {
309 "version": "1.2.0",
310 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
311 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
312 "dev": true,
313 "license": "Apache-2.0",
314 "dependencies": {
315 "bare-inspect": "^3.1.2"
316 }
317 },
318 "node_modules/bare-bundle": {
319 "version": "1.10.0",
320 "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
321 "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
322 "dev": true,
323 "license": "Apache-2.0",
324 "peerDependencies": {
325 "bare-buffer": "*",
326 "bare-url": "*"
327 },
328 "peerDependenciesMeta": {
329 "bare-buffer": {
330 "optional": true
331 },
332 "bare-url": {
333 "optional": true
334 }
335 }
336 },
337 "node_modules/bare-cov": {
338 "version": "1.2.1",
339 "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz",
340 "integrity": "sha512-SHNqb/WG9udUEj8m5qGSehoUQ9o0Xzv8nrf4HIdmG+KNU/XdhPLhTP5VPrHnSAMAXhAzEB4Q+2IYZ0x9qPzN1g==",
341 "dev": true,
342 "license": "Apache-2.0",
343 "dependencies": {
344 "bare-fs": "^4.1.2",
345 "bare-inspector": "^4.0.1",
346 "bare-path": "^3.0.0",
347 "bare-process": "^4.2.1",
348 "bare-url": "^2.1.5",
349 "bare-v8-to-istanbul": "^1.0.2",
350 "picomatch": "^4.0.2",
351 "which-runtime": "^1.2.1"
352 }
353 },
354 "node_modules/bare-crypto": {
355 "version": "1.13.0",
356 "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.0.tgz",
357 "integrity": "sha512-RQl13yD+YTACWUZHMMck0C6LNjXgGgRyhVC557ag0xIrkxXf4IQwpnc8WOB58f4k5kSzKhIjy05oW3HUBrFpSQ==",
358 "dev": true,
359 "license": "Apache-2.0",
360 "dependencies": {
361 "bare-stream": "^2.6.3"
362 },
363 "peerDependencies": {
364 "bare-buffer": "*"
365 },
366 "peerDependenciesMeta": {
367 "bare-buffer": {
368 "optional": true
369 }
370 }
371 },
372 "node_modules/bare-debug-log": {
373 "version": "2.0.0",
374 "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
375 "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
376 "dev": true,
377 "license": "Apache-2.0",
378 "dependencies": {
379 "bare-os": "^3.0.1"
380 }
381 },
382 "node_modules/bare-dns": {
383 "version": "2.1.4",
384 "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
385 "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==",
386 "dev": true,
387 "license": "Apache-2.0",
388 "engines": {
389 "bare": ">=1.7.0"
390 }
391 },
392 "node_modules/bare-encoding": {
393 "version": "1.0.3",
394 "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
395 "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
396 "dev": true,
397 "license": "Apache-2.0",
398 "peerDependencies": {
399 "bare-buffer": "*"
400 },
401 "peerDependenciesMeta": {
402 "bare-buffer": {
403 "optional": true
404 }
405 }
406 },
407 "node_modules/bare-env": {
408 "version": "3.0.0",
409 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
410 "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
411 "dev": true,
412 "license": "Apache-2.0",
413 "dependencies": {
414 "bare-os": "^3.0.1"
415 }
416 },
417 "node_modules/bare-events": {
418 "version": "2.8.2",
419 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
420 "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
421 "dev": true,
422 "license": "Apache-2.0",
423 "peerDependencies": {
424 "bare-abort-controller": "*"
425 },
426 "peerDependenciesMeta": {
427 "bare-abort-controller": {
428 "optional": true
429 }
430 }
431 },
432 "node_modules/bare-format": {
433 "version": "1.0.1",
434 "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.1.tgz",
435 "integrity": "sha512-1oS+LZrWK6tnYnvNSHDGljc2MPunRxwhpFriuCgzNF+oklrnwmBKD91tS0yt+jpl2j3UgcSDzBIMiVTvLs9A8w==",
436 "dev": true,
437 "license": "Apache-2.0",
438 "dependencies": {
439 "bare-inspect": "^3.0.0"
440 }
441 },
442 "node_modules/bare-fs": {
443 "version": "4.5.4",
444 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz",
445 "integrity": "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==",
446 "dev": true,
447 "license": "Apache-2.0",
448 "dependencies": {
449 "bare-events": "^2.5.4",
450 "bare-path": "^3.0.0",
451 "bare-stream": "^2.6.4",
452 "bare-url": "^2.2.2",
453 "fast-fifo": "^1.3.2"
454 },
455 "engines": {
456 "bare": ">=1.16.0"
457 },
458 "peerDependencies": {
459 "bare-buffer": "*"
460 },
461 "peerDependenciesMeta": {
462 "bare-buffer": {
463 "optional": true
464 }
465 }
466 },
467 "node_modules/bare-hrtime": {
468 "version": "2.1.1",
469 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
470 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
471 "dev": true,
472 "license": "Apache-2.0"
473 },
474 "node_modules/bare-http-parser": {
475 "version": "1.0.1",
476 "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.0.1.tgz",
477 "integrity": "sha512-A3LTDTcELcmNJ3g5liIaS038v/BQxOhA9cjhBESn7eoV7QCuMoIRBKLDadDe08flxyLbxI2f+1l2MZ/5+HnKPA==",
478 "dev": true,
479 "license": "Apache-2.0"
480 },
481 "node_modules/bare-http1": {
482 "version": "4.2.3",
483 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.2.3.tgz",
484 "integrity": "sha512-zyvHF3xzvBJNpuw0QydE9etTn2AyC/TRVDWTo8Ed3LQEEGaFOkv6KHhaJPlf4eWEeTekk41Fowie+fTsXoNxkA==",
485 "dev": true,
486 "license": "Apache-2.0",
487 "dependencies": {
488 "bare-events": "^2.6.0",
489 "bare-http-parser": "^1.0.0",
490 "bare-stream": "^2.3.0",
491 "bare-tcp": "^2.2.0"
492 },
493 "peerDependencies": {
494 "bare-buffer": "*",
495 "bare-url": "*"
496 },
497 "peerDependenciesMeta": {
498 "bare-buffer": {
499 "optional": true
500 },
501 "bare-url": {
502 "optional": true
503 }
504 }
505 },
506 "node_modules/bare-https": {
507 "version": "2.1.2",
508 "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.2.tgz",
509 "integrity": "sha512-Q+TTydUDsuKQJvh8dX2dvOXCR9fM3xR5TBmKaFrs5p7Lj7XbKX7v4vIUJ36H0SXg2xCOQxXKIbjwrLg5tfJNYg==",
510 "dev": true,
511 "license": "Apache-2.0",
512 "dependencies": {
513 "bare-http1": "^4.0.0",
514 "bare-tcp": "^2.2.0",
515 "bare-tls": "^2.0.0"
516 }
517 },
518 "node_modules/bare-inspect": {
519 "version": "3.1.4",
520 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
521 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
522 "dev": true,
523 "license": "Apache-2.0",
524 "dependencies": {
525 "bare-ansi-escapes": "^2.1.0",
526 "bare-type": "^1.0.0"
527 },
528 "engines": {
529 "bare": ">=1.18.0"
530 }
531 },
532 "node_modules/bare-inspector": {
533 "version": "4.0.3",
534 "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-4.0.3.tgz",
535 "integrity": "sha512-yYk6+7tTVKJKHOja5+QBn989ETv8oVsmwoH0nCM+QYC6w5MC03GC4aQNK+lipAZSz1EaAj42WZ8Wewjpy0L9TA==",
536 "dev": true,
537 "license": "Apache-2.0",
538 "dependencies": {
539 "bare-events": "^2.1.0",
540 "bare-http1": "^4.0.0",
541 "bare-stream": "^2.0.0",
542 "bare-url": "^2.0.0",
543 "bare-ws": "^2.0.0"
544 },
545 "engines": {
546 "bare": ">=1.2.0"
547 },
548 "peerDependencies": {
549 "bare-tcp": "*"
550 },
551 "peerDependenciesMeta": {
552 "bare-tcp": {
553 "optional": true
554 }
555 }
556 },
557 "node_modules/bare-module": {
558 "version": "6.1.3",
559 "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz",
560 "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==",
561 "dev": true,
562 "license": "Apache-2.0",
563 "dependencies": {
564 "bare-bundle": "^1.3.0",
565 "bare-module-lexer": "^1.0.0",
566 "bare-module-resolve": "^1.8.0",
567 "bare-path": "^3.0.0",
568 "bare-url": "^2.0.1"
569 },
570 "engines": {
571 "bare": ">=1.23.0"
572 },
573 "peerDependencies": {
574 "bare-buffer": "*"
575 },
576 "peerDependenciesMeta": {
577 "bare-buffer": {
578 "optional": true
579 }
580 }
581 },
582 "node_modules/bare-module-lexer": {
583 "version": "1.4.7",
584 "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
585 "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
586 "dev": true,
587 "license": "Apache-2.0",
588 "dependencies": {
589 "require-addon": "^1.0.2"
590 },
591 "peerDependencies": {
592 "bare-buffer": "*"
593 },
594 "peerDependenciesMeta": {
595 "bare-buffer": {
596 "optional": true
597 }
598 }
599 },
600 "node_modules/bare-module-resolve": {
601 "version": "1.12.1",
602 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz",
603 "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==",
604 "dev": true,
605 "license": "Apache-2.0",
606 "dependencies": {
607 "bare-semver": "^1.0.0"
608 },
609 "peerDependencies": {
610 "bare-url": "*"
611 },
612 "peerDependenciesMeta": {
613 "bare-url": {
614 "optional": true
615 }
616 }
617 },
618 "node_modules/bare-net": {
619 "version": "2.2.0",
620 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.2.0.tgz",
621 "integrity": "sha512-UF7cAbHsGE+H6uEqWF5IULBow1x58chZz4g3ALgHtv7wZsFcCbRDt0JKWEumf5Oma3QWS1Q6aLi0Rpll8RElMg==",
622 "dev": true,
623 "license": "Apache-2.0",
624 "dependencies": {
625 "bare-events": "^2.2.2",
626 "bare-pipe": "^4.0.0",
627 "bare-stream": "^2.0.0",
628 "bare-tcp": "^2.0.0"
629 }
630 },
631 "node_modules/bare-os": {
632 "version": "3.6.2",
633 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
634 "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
635 "dev": true,
636 "license": "Apache-2.0",
637 "engines": {
638 "bare": ">=1.14.0"
639 }
640 },
641 "node_modules/bare-path": {
642 "version": "3.0.0",
643 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
644 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
645 "dev": true,
646 "license": "Apache-2.0",
647 "dependencies": {
648 "bare-os": "^3.0.1"
649 }
650 },
651 "node_modules/bare-pipe": {
652 "version": "4.1.3",
653 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.3.tgz",
654 "integrity": "sha512-DqQsx93rAzre6yJ9T6l/Vgh+X+bntkVMB1X5ggtXjXtqtMmF2Y2RVlCzxxy/09R6yeR9FSWBEUIaMYJL1/5VDA==",
655 "dev": true,
656 "license": "Apache-2.0",
657 "dependencies": {
658 "bare-events": "^2.0.0",
659 "bare-stream": "^2.0.0"
660 },
661 "engines": {
662 "bare": ">=1.16.0"
663 }
664 },
665 "node_modules/bare-process": {
666 "version": "4.2.2",
667 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.2.2.tgz",
668 "integrity": "sha512-ikzEw+HGLB+2lS/WLVEsqi8BXBwzleG3n7DYI0v6/YNklvNabOIGlLd1dof+7Jw5ob4jsBRPtyflweVahY7rFA==",
669 "dev": true,
670 "license": "Apache-2.0",
671 "dependencies": {
672 "bare-env": "^3.0.0",
673 "bare-events": "^2.3.1",
674 "bare-hrtime": "^2.0.0",
675 "bare-os": "^3.5.0",
676 "bare-pipe": "^4.0.0",
677 "bare-signals": "^4.0.0",
678 "bare-tty": "^5.0.0"
679 }
680 },
681 "node_modules/bare-semver": {
682 "version": "1.0.2",
683 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz",
684 "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==",
685 "dev": true,
686 "license": "Apache-2.0"
687 },
688 "node_modules/bare-signals": {
689 "version": "4.2.0",
690 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz",
691 "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==",
692 "dev": true,
693 "license": "Apache-2.0",
694 "dependencies": {
695 "bare-events": "^2.5.3",
696 "bare-os": "^3.3.1"
697 },
698 "engines": {
699 "bare": ">=1.7.0"
700 }
701 },
702 "node_modules/bare-stream": {
703 "version": "2.7.0",
704 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
705 "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
706 "dev": true,
707 "license": "Apache-2.0",
708 "dependencies": {
709 "streamx": "^2.21.0"
710 },
711 "peerDependencies": {
712 "bare-buffer": "*",
713 "bare-events": "*"
714 },
715 "peerDependenciesMeta": {
716 "bare-buffer": {
717 "optional": true
718 },
719 "bare-events": {
720 "optional": true
721 }
722 }
723 },
724 "node_modules/bare-subprocess": {
725 "version": "5.2.2",
726 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.2.tgz",
727 "integrity": "sha512-L6oXgQ1aWs25RtG5Ky0bDD06p3RAcVVrDDMWb1DfXpHtyEWxamcyIWUbSykxMTWrpLlmSj6ytbb6yoKehGFfmw==",
728 "dev": true,
729 "license": "Apache-2.0",
730 "dependencies": {
731 "bare-env": "^3.0.0",
732 "bare-events": "^2.5.4",
733 "bare-os": "^3.0.1",
734 "bare-pipe": "^4.0.0",
735 "bare-url": "^2.2.2"
736 },
737 "engines": {
738 "bare": ">=1.7.0"
739 },
740 "peerDependencies": {
741 "bare-buffer": "*"
742 },
743 "peerDependenciesMeta": {
744 "bare-buffer": {
745 "optional": true
746 }
747 }
748 },
749 "node_modules/bare-tcp": {
750 "version": "2.2.4",
751 "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.4.tgz",
752 "integrity": "sha512-vGjfMkyhS1lxp+Y4+gXRDR1ijZMK2cKx0RBwtYKUuZbD90ARCoM6bAiTa1HBztumCIC+NXb8eXdMFXyuYwlPyw==",
753 "dev": true,
754 "license": "Apache-2.0",
755 "dependencies": {
756 "bare-dns": "^2.0.4",
757 "bare-events": "^2.5.4",
758 "bare-stream": "^2.6.4"
759 },
760 "engines": {
761 "bare": ">=1.16.0"
762 }
763 },
764 "node_modules/bare-tls": {
765 "version": "2.1.7",
766 "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.1.7.tgz",
767 "integrity": "sha512-h6wcNXQdBeTX7fed9tjPp0/9cA/QfcBTv3ItgjnbUk4rWAU8bEFalZCZnUDdCK/t9zrNfJ+yvcPx4D/1Y6biyA==",
768 "dev": true,
769 "license": "Apache-2.0",
770 "dependencies": {
771 "bare-net": "^2.0.1",
772 "bare-stream": "^2.6.4"
773 },
774 "engines": {
775 "bare": ">=1.7.0"
776 }
777 },
778 "node_modules/bare-tty": {
779 "version": "5.0.3",
780 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.0.3.tgz",
781 "integrity": "sha512-jW24RBWRZOMHuSEWC9mh8wUKO5WUNl0UWUTNPn3yZ8qkOqwa8vaV2dR3a+BvblONPk7V35wuvK9P8508+judAg==",
782 "dev": true,
783 "license": "Apache-2.0",
784 "dependencies": {
785 "bare-events": "^2.2.0",
786 "bare-signals": "^4.0.0",
787 "bare-stream": "^2.0.0"
788 },
789 "engines": {
790 "bare": ">=1.16.0"
791 }
792 },
793 "node_modules/bare-type": {
794 "version": "1.1.0",
795 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
796 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
797 "dev": true,
798 "license": "Apache-2.0",
799 "engines": {
800 "bare": ">=1.2.0"
801 }
802 },
803 "node_modules/bare-url": {
804 "version": "2.3.2",
805 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
806 "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
807 "dev": true,
808 "license": "Apache-2.0",
809 "peer": true,
810 "dependencies": {
811 "bare-path": "^3.0.0"
812 }
813 },
814 "node_modules/bare-utils": {
815 "version": "1.5.1",
816 "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.5.1.tgz",
817 "integrity": "sha512-mxCkFvmDU3mlD/mb+pT64kKXOsx2KMsWLQbngN1LB+NOXfhfnRnyvpy3VZc6m7gzQxe57Bsi+aTCBqA4/S3elQ==",
818 "dev": true,
819 "license": "Apache-2.0",
820 "dependencies": {
821 "bare-debug-log": "^2.0.0",
822 "bare-encoding": "^1.0.0",
823 "bare-format": "^1.0.0",
824 "bare-inspect": "^3.0.0",
825 "bare-type": "^1.0.6"
826 }
827 },
828 "node_modules/bare-v8-to-istanbul": {
829 "version": "1.0.2",
830 "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
831 "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==",
832 "dev": true,
833 "license": "Apache-2.0",
834 "dependencies": {
835 "bare-assert": "^1.0.2",
836 "bare-fs": "^4.1.2",
837 "bare-module": "^6.1.2",
838 "bare-path": "^3.0.0",
839 "bare-process": "^4.2.0",
840 "bare-url": "^2.1.5",
841 "bare-utils": "^1.2.0",
842 "v8-to-istanbul": "^9.3.0",
843 "which-runtime": "^1.2.1"
844 }
845 },
846 "node_modules/bare-ws": {
847 "version": "2.0.4",
848 "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.0.4.tgz",
849 "integrity": "sha512-SQMXzBYna9dRj57Dz1/ag+VWHCRXbfCjMHgyfM2F2lhkVLzMjnVSZP72aVeFWPFqe494Rd70Kzhe2JElGwFlJQ==",
850 "dev": true,
851 "license": "Apache-2.0",
852 "dependencies": {
853 "bare-crypto": "^1.2.0",
854 "bare-events": "^2.3.1",
855 "bare-http1": "^4.0.0",
856 "bare-https": "^2.0.0",
857 "bare-stream": "^2.1.2"
858 },
859 "peerDependencies": {
860 "bare-buffer": "*",
861 "bare-url": "*"
862 },
863 "peerDependenciesMeta": {
864 "bare-buffer": {
865 "optional": true
866 },
867 "bare-url": {
868 "optional": true
869 }
870 }
871 },
872 "node_modules/bridge-envelope": {
873 "resolved": "../bridge-envelope",
874 "link": true
875 },
876 "node_modules/brittle": {
877 "version": "3.19.1",
878 "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz",
879 "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==",
880 "dev": true,
881 "license": "Apache-2.0",
882 "dependencies": {
883 "b4a": "^1.6.0",
884 "bare-assert": "^1.0.2",
885 "bare-cov": "^1.1.0",
886 "bare-fs": "^4.1.6",
887 "bare-os": "^3.6.1",
888 "bare-path": "^3.0.0",
889 "bare-process": "^4.2.1",
890 "bare-subprocess": "^5.0.0",
891 "bare-url": "^2.1.6",
892 "error-stack-parser": "^2.1.4",
893 "globbie": "^1.0.2",
894 "paparam": "^1.6.2",
895 "same-object": "^1.0.2",
896 "test-tmp": "^1.4.0",
897 "tmatch": "^5.0.0"
898 },
899 "bin": {
900 "brittle": "bin/node.js",
901 "brittle-bare": "bin/bare.js",
902 "brittle-node": "bin/node.js",
903 "brittle-pear": "bin/pear.js"
904 }
905 },
906 "node_modules/convert-source-map": {
907 "version": "2.0.0",
908 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
909 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
910 "dev": true,
911 "license": "MIT"
912 },
913 "node_modules/discord-api-types": {
914 "version": "0.38.39",
915 "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.39.tgz",
916 "integrity": "sha512-XRdDQvZvID1XvcFftjSmd4dcmMi/RL/jSy5sduBDAvCGFcNFHThdIQXCEBDZFe52lCNEzuIL0QJoKYAmRmxLUA==",
917 "license": "MIT",
918 "workspaces": [
919 "scripts/actions/documentation"
920 ]
921 },
922 "node_modules/discord.js": {
923 "version": "14.25.1",
924 "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.25.1.tgz",
925 "integrity": "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==",
926 "license": "Apache-2.0",
927 "dependencies": {
928 "@discordjs/builders": "^1.13.0",
929 "@discordjs/collection": "1.5.3",
930 "@discordjs/formatters": "^0.6.2",
931 "@discordjs/rest": "^2.6.0",
932 "@discordjs/util": "^1.2.0",
933 "@discordjs/ws": "^1.2.3",
934 "@sapphire/snowflake": "3.5.3",
935 "discord-api-types": "^0.38.33",
936 "fast-deep-equal": "3.1.3",
937 "lodash.snakecase": "4.1.1",
938 "magic-bytes.js": "^1.10.0",
939 "tslib": "^2.6.3",
940 "undici": "6.21.3"
941 },
942 "engines": {
943 "node": ">=18"
944 },
945 "funding": {
946 "url": "https://github.com/discordjs/discord.js?sponsor"
947 }
948 },
949 "node_modules/error-stack-parser": {
950 "version": "2.1.4",
951 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
952 "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
953 "dev": true,
954 "license": "MIT",
955 "dependencies": {
956 "stackframe": "^1.3.4"
957 }
958 },
959 "node_modules/events-universal": {
960 "version": "1.0.1",
961 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
962 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
963 "dev": true,
964 "license": "Apache-2.0",
965 "dependencies": {
966 "bare-events": "^2.7.0"
967 }
968 },
969 "node_modules/fast-deep-equal": {
970 "version": "3.1.3",
971 "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
972 "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
973 "license": "MIT"
974 },
975 "node_modules/fast-fifo": {
976 "version": "1.3.2",
977 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
978 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
979 "dev": true,
980 "license": "MIT"
981 },
982 "node_modules/globbie": {
983 "version": "1.0.3",
984 "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz",
985 "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==",
986 "dev": true,
987 "license": "Apache-2.0",
988 "dependencies": {
989 "bare-fs": "^4.1.2",
990 "bare-path": "^3.0.0",
991 "bare-process": "^4.2.1",
992 "picomatch": "^4.0.2"
993 }
994 },
995 "node_modules/lodash": {
996 "version": "4.17.23",
997 "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
998 "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
999 "license": "MIT"
1000 },
1001 "node_modules/lodash.snakecase": {
1002 "version": "4.1.1",
1003 "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
1004 "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
1005 "license": "MIT"
1006 },
1007 "node_modules/magic-bytes.js": {
1008 "version": "1.13.0",
1009 "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz",
1010 "integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==",
1011 "license": "MIT"
1012 },
1013 "node_modules/paparam": {
1014 "version": "1.10.0",
1015 "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.0.tgz",
1016 "integrity": "sha512-p36QQrwU3X6fj5d2JN20B4lcZI/O3fxMVUHc1xD7MNe5bcOf3jIgE0CIK5Zv/s5YAUGlobTJ/kKXrh1FJVrs3w==",
1017 "dev": true,
1018 "license": "Apache-2.0"
1019 },
1020 "node_modules/picomatch": {
1021 "version": "4.0.3",
1022 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
1023 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
1024 "dev": true,
1025 "license": "MIT",
1026 "engines": {
1027 "node": ">=12"
1028 },
1029 "funding": {
1030 "url": "https://github.com/sponsors/jonschlinkert"
1031 }
1032 },
1033 "node_modules/require-addon": {
1034 "version": "1.2.0",
1035 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
1036 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
1037 "dev": true,
1038 "license": "Apache-2.0",
1039 "dependencies": {
1040 "bare-addon-resolve": "^1.3.0"
1041 },
1042 "engines": {
1043 "bare": ">=1.10.0"
1044 }
1045 },
1046 "node_modules/same-object": {
1047 "version": "1.0.2",
1048 "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz",
1049 "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==",
1050 "dev": true,
1051 "license": "MIT"
1052 },
1053 "node_modules/stackframe": {
1054 "version": "1.3.4",
1055 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
1056 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
1057 "dev": true,
1058 "license": "MIT"
1059 },
1060 "node_modules/streamx": {
1061 "version": "2.23.0",
1062 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
1063 "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
1064 "dev": true,
1065 "license": "MIT",
1066 "dependencies": {
1067 "events-universal": "^1.0.0",
1068 "fast-fifo": "^1.3.2",
1069 "text-decoder": "^1.1.0"
1070 }
1071 },
1072 "node_modules/test-tmp": {
1073 "version": "1.4.0",
1074 "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz",
1075 "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==",
1076 "dev": true,
1077 "license": "MIT",
1078 "dependencies": {
1079 "bare-fs": "^4.0.1",
1080 "bare-os": "^3.3.0",
1081 "bare-path": "^3.0.0"
1082 }
1083 },
1084 "node_modules/text-decoder": {
1085 "version": "1.2.6",
1086 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.6.tgz",
1087 "integrity": "sha512-27FeW5GQFDfw0FpwMQhMagB7BztOOlmjcSRi97t2oplhKVTZtp0DZbSegSaXS5IIC6mxMvBG4AR1Sgc6BX3CQg==",
1088 "dev": true,
1089 "license": "Apache-2.0",
1090 "dependencies": {
1091 "b4a": "^1.6.4"
1092 }
1093 },
1094 "node_modules/tmatch": {
1095 "version": "5.0.0",
1096 "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz",
1097 "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==",
1098 "dev": true,
1099 "license": "ISC",
1100 "engines": {
1101 "node": ">=8"
1102 }
1103 },
1104 "node_modules/ts-mixer": {
1105 "version": "6.0.4",
1106 "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
1107 "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==",
1108 "license": "MIT"
1109 },
1110 "node_modules/tslib": {
1111 "version": "2.8.1",
1112 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
1113 "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
1114 "license": "0BSD"
1115 },
1116 "node_modules/undici": {
1117 "version": "6.21.3",
1118 "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
1119 "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
1120 "license": "MIT",
1121 "engines": {
1122 "node": ">=18.17"
1123 }
1124 },
1125 "node_modules/undici-types": {
1126 "version": "7.16.0",
1127 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
1128 "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
1129 "license": "MIT"
1130 },
1131 "node_modules/v8-to-istanbul": {
1132 "version": "9.3.0",
1133 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
1134 "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
1135 "dev": true,
1136 "license": "ISC",
1137 "dependencies": {
1138 "@jridgewell/trace-mapping": "^0.3.12",
1139 "@types/istanbul-lib-coverage": "^2.0.1",
1140 "convert-source-map": "^2.0.0"
1141 },
1142 "engines": {
1143 "node": ">=10.12.0"
1144 }
1145 },
1146 "node_modules/which-runtime": {
1147 "version": "1.3.2",
1148 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
1149 "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
1150 "dev": true,
1151 "license": "Apache-2.0"
1152 },
1153 "node_modules/ws": {
1154 "version": "8.19.0",
1155 "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
1156 "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
1157 "license": "MIT",
1158 "engines": {
1159 "node": ">=10.0.0"
1160 },
1161 "peerDependencies": {
1162 "bufferutil": "^4.0.1",
1163 "utf-8-validate": ">=5.0.2"
1164 },
1165 "peerDependenciesMeta": {
1166 "bufferutil": {
1167 "optional": true
1168 },
1169 "utf-8-validate": {
1170 "optional": true
1171 }
1172 }
1173 }
1174 }
1175}
bridge-discord/package.json +23 -0
……@@ -0,0 +1,23 @@
1{
2 "name": "bridge-discord",
3 "version": "1.0.0",
4 "description": "Discord ↔ Keet bidirectional bridge",
5 "type": "module",
6 "main": "index.js",
7 "pear": {
8 "name": "bridge-discord",
9 "type": "terminal"
10 },
11 "scripts": {
12 "test": "brittle test/*.test.js"
13 },
14 "dependencies": {
15 "discord.js": "^14.16.3",
16 "bridge-envelope": "file:../bridge-envelope",
17 "b4a": "^1.0.0"
18 },
19 "devDependencies": {
20 "brittle": "^3.0.0"
21 },
22 "license": "MIT"
23}
bridge-discord/test/adapter.test.js +212 -0
……@@ -0,0 +1,212 @@
1import test from 'brittle'
2import { discordToEnvelope, keetToDiscord, envelopeToDiscordContent, discordReactionToEnvelope } from '../lib/adapter.js'
3import { TYPES, PLATFORMS, createEnvelope } from 'bridge-envelope'
4
5// Mock Discord message objects
6function mockMessage (overrides = {}) {
7 return {
8 author: { tag: 'user#1234', username: 'user', bot: false, id: '123' },
9 channel: { name: 'general', id: '456' },
10 channelId: '456',
11 content: 'Hello from Discord!',
12 attachments: { first: () => null, [Symbol.iterator]: function * () {} },
13 reference: null,
14 ...overrides
15 }
16}
17
18test('discordToEnvelope - basic text message', (t) => {
19 const msg = mockMessage()
20 const env = discordToEnvelope(msg)
21
22 t.is(env.type, TYPES.TEXT)
23 t.is(env.platform, PLATFORMS.DISCORD)
24 t.is(env.author, 'user#1234')
25 t.is(env.content, 'Hello from Discord!')
26 t.is(env.channel, 'general')
27 t.ok(env.id)
28})
29
30test('discordToEnvelope - skips bot messages', (t) => {
31 const msg = mockMessage({ author: { tag: 'bot#0000', bot: true, id: '999' } })
32 const env = discordToEnvelope(msg)
33 t.is(env, null)
34})
35
36test('discordToEnvelope - with attachment', (t) => {
37 const msg = mockMessage({
38 attachments: {
39 first: () => ({ url: 'https://cdn.discord.com/img.png', contentType: 'image/png' }),
40 [Symbol.iterator]: function * () { yield ['1', { url: 'https://cdn.discord.com/img.png' }] }
41 }
42 })
43
44 const env = discordToEnvelope(msg)
45 t.is(env.type, TYPES.MEDIA)
46 t.is(env.mediaUrl, 'https://cdn.discord.com/img.png')
47 t.is(env.mediaType, 'image/png')
48})
49
50test('discordToEnvelope - reply message', (t) => {
51 const msg = mockMessage({
52 reference: { messageId: 'orig-msg-123' }
53 })
54
55 const env = discordToEnvelope(msg)
56 t.is(env.type, TYPES.REPLY)
57 t.is(env.replyTo, 'orig-msg-123')
58})
59
60test('discordToEnvelope - reply with attachment', (t) => {
61 const msg = mockMessage({
62 reference: { messageId: 'orig-456' },
63 attachments: {
64 first: () => ({ url: 'https://cdn.discord.com/file.pdf', contentType: 'application/pdf' }),
65 [Symbol.iterator]: function * () {}
66 }
67 })
68
69 const env = discordToEnvelope(msg)
70 t.is(env.type, TYPES.MEDIA)
71 t.is(env.replyTo, 'orig-456')
72 t.is(env.mediaUrl, 'https://cdn.discord.com/file.pdf')
73})
74
75test('discordToEnvelope - empty content', (t) => {
76 const msg = mockMessage({ content: '' })
77 const env = discordToEnvelope(msg)
78 t.is(env.content, '')
79})
80
81test('discordToEnvelope - unicode content', (t) => {
82 const msg = mockMessage({ content: '🎉 Hello 世界!' })
83 const env = discordToEnvelope(msg)
84 t.is(env.content, '🎉 Hello 世界!')
85})
86
87test('keetToDiscord - basic message', (t) => {
88 const msg = {
89 memberId: 'member-123',
90 name: 'Alice',
91 text: 'Hello from Keet!',
92 timestamp: Date.now()
93 }
94
95 const result = keetToDiscord(msg, 'bridge-identity')
96 t.ok(result)
97 t.is(result.name, 'Alice')
98 t.ok(result.text.includes('Alice'))
99 t.ok(result.text.includes('Keet'))
100 t.ok(result.text.includes('Hello from Keet!'))
101})
102
103test('keetToDiscord - skips own messages', (t) => {
104 const msg = { memberId: 'bridge-id', name: 'Bridge', text: 'echo' }
105 t.is(keetToDiscord(msg, 'bridge-id'), null)
106})
107
108test('keetToDiscord - skips system messages', (t) => {
109 const msg = { memberId: 'other', name: 'Sys', text: 'joined', system: { type: 'join' } }
110 t.is(keetToDiscord(msg, 'bridge-id'), null)
111})
112
113test('keetToDiscord - skips empty text', (t) => {
114 const msg = { memberId: 'other', name: 'User', text: ' ' }
115 t.is(keetToDiscord(msg, 'bridge-id'), null)
116})
117
118test('keetToDiscord - anonymous fallback', (t) => {
119 const msg = { memberId: 'other', text: 'hello' }
120 const result = keetToDiscord(msg, 'bridge-id')
121 t.is(result.name, 'anonymous')
122})
123
124test('envelopeToDiscordContent - keet text', (t) => {
125 const env = createEnvelope({
126 type: TYPES.TEXT,
127 platform: PLATFORMS.KEET,
128 author: 'Alice',
129 content: 'Hello!'
130 })
131 const text = envelopeToDiscordContent(env)
132 t.ok(text.includes('[keet]'))
133 t.ok(text.includes('**Alice**'))
134 t.ok(text.includes('Hello!'))
135})
136
137test('envelopeToDiscordContent - discord message no prefix', (t) => {
138 const env = createEnvelope({
139 type: TYPES.TEXT,
140 platform: PLATFORMS.DISCORD,
141 author: 'bob#1234',
142 content: 'native discord'
143 })
144 const text = envelopeToDiscordContent(env)
145 t.absent(text.includes('[discord]'))
146 t.ok(text.includes('bob#1234'))
147})
148
149test('envelopeToDiscordContent - IRC message', (t) => {
150 const env = createEnvelope({
151 type: TYPES.TEXT,
152 platform: PLATFORMS.IRC,
153 author: 'ircuser',
154 content: 'from IRC'
155 })
156 const text = envelopeToDiscordContent(env)
157 t.ok(text.includes('[irc]'))
158 t.ok(text.includes('**ircuser**'))
159})
160
161test('envelopeToDiscordContent - media', (t) => {
162 const env = createEnvelope({
163 type: TYPES.MEDIA,
164 platform: PLATFORMS.KEET,
165 author: 'Alice',
166 content: 'Check this',
167 mediaUrl: 'https://example.com/img.png'
168 })
169 const text = envelopeToDiscordContent(env)
170 t.ok(text.includes('https://example.com/img.png'))
171})
172
173test('envelopeToDiscordContent - reaction', (t) => {
174 const env = createEnvelope({
175 type: TYPES.REACTION,
176 platform: PLATFORMS.KEET,
177 author: 'Bob',
178 content: '👍'
179 })
180 const text = envelopeToDiscordContent(env)
181 t.ok(text.includes('reacted'))
182 t.ok(text.includes('👍'))
183})
184
185test('envelopeToDiscordContent - system', (t) => {
186 const env = createEnvelope({
187 type: TYPES.SYSTEM,
188 platform: PLATFORMS.KEET,
189 content: 'User joined'
190 })
191 const text = envelopeToDiscordContent(env)
192 t.ok(text.includes('User joined'))
193})
194
195test('discordReactionToEnvelope', (t) => {
196 const reaction = {
197 emoji: { name: '👍', toString: () => '👍' },
198 message: {
199 id: 'msg-789',
200 channel: { name: 'general' },
201 channelId: '456'
202 }
203 }
204 const user = { tag: 'alice#1234', username: 'alice' }
205
206 const env = discordReactionToEnvelope(reaction, user)
207 t.is(env.type, TYPES.REACTION)
208 t.is(env.platform, PLATFORMS.DISCORD)
209 t.is(env.author, 'alice#1234')
210 t.is(env.content, '👍')
211 t.is(env.replyTo, 'msg-789')
212})
bridge-email/index.js +86 -0
……@@ -0,0 +1,86 @@
1import { EmailBridge } from './lib/bridge.js'
2import { ImapClient, parseEmail } from './lib/imap-client.js'
3import { SmtpClient } from './lib/smtp-client.js'
4import { KeetClient } from './lib/keet-client.js'
5import { Router } from './lib/router.js'
6import { emailToEnvelope, keetToEmail, emailToKeetText, keetToEmailText, extractEmail, extractName } from './lib/adapter.js'
7
8export {
9 EmailBridge,
10 ImapClient,
11 SmtpClient,
12 KeetClient,
13 Router,
14 parseEmail,
15 emailToEnvelope,
16 keetToEmail,
17 emailToKeetText,
18 keetToEmailText,
19 extractEmail,
20 extractName
21}
22
23// CLI entry point
24const args = process.argv.slice(2)
25if (args.includes('--help') || args.includes('-h')) {
26 console.log(`bridge-email — Email ↔ Keet bridge
27
28Usage:
29 bridge-email --imap-host <host> --smtp-host <host> --user <email> --pass <password> --keet-room <roomId> [options]
30
31Options:
32 --imap-host <host> IMAP server hostname
33 --imap-port <port> IMAP port (default: 993)
34 --smtp-host <host> SMTP server hostname
35 --smtp-port <port> SMTP port (default: 465)
36 --user <email> Email username/address
37 --pass <password> Email password (or EMAIL_PASS env)
38 --forward-to <email> Address to relay Keet messages to
39 --keet-room <roomId> Keet room ID to bridge
40 --keet-port <port> Keet daemon port (default: 3117)
41 --poll-interval <ms> IMAP poll interval in ms (default: 30000)
42 --help Show this help`)
43 process.exit(0)
44}
45
46function getArg (flag) {
47 const idx = args.indexOf(flag)
48 return idx !== -1 ? args[idx + 1] : null
49}
50
51const imapHost = getArg('--imap-host')
52const smtpHost = getArg('--smtp-host')
53const user = getArg('--user')
54const pass = getArg('--pass') || process.env.EMAIL_PASS
55const roomId = getArg('--keet-room')
56const keetPort = parseInt(getArg('--keet-port') || '3117', 10)
57const forwardTo = getArg('--forward-to')
58const pollInterval = parseInt(getArg('--poll-interval') || '30000', 10)
59const imapPort = parseInt(getArg('--imap-port') || '993', 10)
60const smtpPort = parseInt(getArg('--smtp-port') || '465', 10)
61
62if (imapHost && smtpHost && user && pass && roomId) {
63 const bridge = new EmailBridge({
64 imapHost,
65 imapPort,
66 smtpHost,
67 smtpPort,
68 user,
69 pass,
70 roomId,
71 forwardTo,
72 keetPort,
73 pollInterval,
74 onLog: (msg) => console.log(`[email-bridge] ${msg}`)
75 })
76
77 bridge.start().catch(err => {
78 console.error('Bridge failed to start:', err.message)
79 process.exit(1)
80 })
81
82 process.on('SIGINT', () => {
83 bridge.stop()
84 process.exit(0)
85 })
86}
bridge-email/lib/adapter.js +112 -0
……@@ -0,0 +1,112 @@
1import { createEnvelope, TYPES, PLATFORMS } from 'bridge-envelope'
2
3// Extract email address from "Name <addr>" format
4function extractEmail (from) {
5 const match = from.match(/<([^>]+)>/)
6 return match ? match[1] : from.trim()
7}
8
9// Extract display name from "Name <addr>" format
10function extractName (from) {
11 const match = from.match(/^"?([^"<]+)"?\s*</)
12 return match ? match[1].trim() : extractEmail(from)
13}
14
15// Convert parsed email to bridge envelope
16function emailToEnvelope (email) {
17 if (!email) return null
18
19 const from = email.from || ''
20 const author = extractName(from)
21 const subject = email.subject || ''
22 const body = email.body || ''
23
24 if (!body.trim() && !subject.trim()) return null
25
26 // Skip bridge-generated emails
27 if (email.body?.includes('[email]') || email.body?.includes('[Email]')) return null
28
29 const content = subject ? `[${subject}] ${body}` : body
30
31 const envelope = createEnvelope({
32 type: TYPES.TEXT,
33 platform: PLATFORMS.EMAIL,
34 author,
35 content,
36 channel: extractEmail(from)
37 })
38
39 // Preserve email metadata for threading
40 envelope._messageId = email.messageId || ''
41 envelope._subject = subject
42 envelope._from = from
43 envelope._to = email.to || ''
44 envelope._date = email.date || ''
45
46 return envelope
47}
48
49// Convert Keet message to email-ready format
50function keetToEmail (keetMsg, bridgeIdentity) {
51 if (!keetMsg) return null
52
53 // Skip own messages
54 if (keetMsg.memberId === bridgeIdentity) return null
55
56 // Skip system messages
57 if (keetMsg.type === 'system') return null
58
59 const text = keetMsg.text || ''
60 if (!text.trim()) return null
61
62 // Skip already bridged from email
63 if (text.includes('[email]') || text.includes('[Email]')) return null
64
65 const name = keetMsg.name || keetMsg.username || 'someone'
66
67 return {
68 author: name,
69 text,
70 subject: `[Keet] Message from ${name}`,
71 body: `${name} wrote:\n\n${text}\n\n---\nRelayed via bridge-email from Keet P2P chat`
72 }
73}
74
75// Format email envelope for display in Keet
76function emailToKeetText (envelope) {
77 if (!envelope) return ''
78
79 let text = `[email] ${envelope.author}`
80
81 if (envelope._subject) {
82 text += ` — ${envelope._subject}`
83 }
84
85 // Truncate long emails for chat display
86 const body = envelope.content.replace(`[${envelope._subject}] `, '')
87 const maxLen = 500
88 if (body.length > maxLen) {
89 text += `: ${body.slice(0, maxLen)}...`
90 } else {
91 text += `: ${body}`
92 }
93
94 return text
95}
96
97// Format Keet message as email subject/body
98function keetToEmailText (author, text) {
99 return {
100 subject: `[Keet] Message from ${author}`,
101 body: `${author} wrote:\n\n${text}\n\n---\nRelayed via bridge-email from Keet P2P chat`
102 }
103}
104
105export {
106 emailToEnvelope,
107 keetToEmail,
108 emailToKeetText,
109 keetToEmailText,
110 extractEmail,
111 extractName
112}
bridge-email/lib/bridge.js +142 -0
……@@ -0,0 +1,142 @@
1import { ImapClient } from './imap-client.js'
2import { SmtpClient } from './smtp-client.js'
3import { KeetClient } from './keet-client.js'
4import { Router } from './router.js'
5import { emailToEnvelope, keetToEmail, emailToKeetText } from './adapter.js'
6
7class EmailBridge {
8 constructor (opts = {}) {
9 this.imap = new ImapClient({
10 host: opts.imapHost,
11 port: opts.imapPort,
12 user: opts.user,
13 pass: opts.pass
14 })
15 this.smtp = new SmtpClient({
16 host: opts.smtpHost,
17 port: opts.smtpPort,
18 user: opts.user,
19 pass: opts.pass,
20 from: opts.from || opts.user
21 })
22 this.keet = new KeetClient({
23 host: opts.keetHost,
24 port: opts.keetPort
25 })
26 this.roomId = opts.roomId
27 this.forwardTo = opts.forwardTo // email address to relay Keet messages to
28 this.pollInterval = opts.pollInterval || 30000 // 30s
29 this.bridgeIdentity = opts.bridgeIdentity || null
30 this.onLog = opts.onLog || (() => {})
31
32 this._polling = false
33 this._pollTimer = null
34
35 this.router = new Router({
36 onKeetMessage: (author, text) => {
37 this.keet.send(this.roomId, text).catch(err => {
38 this.onLog(`Keet send error: ${err.message}`)
39 })
40 },
41 onExternalMessage: (author, text) => {
42 if (!this.forwardTo) return
43 this._sendEmail(author, text).catch(err => {
44 this.onLog(`Email send error: ${err.message}`)
45 })
46 }
47 })
48 }
49
50 async _sendEmail (author, text) {
51 const smtp = new SmtpClient({
52 host: this.smtp.host,
53 port: this.smtp.port,
54 user: this.smtp.user,
55 pass: this.smtp.pass,
56 from: this.smtp.from
57 })
58
59 await smtp.connect()
60 await smtp.ehlo()
61 await smtp.auth()
62 await smtp.sendMail(
63 this.forwardTo,
64 `[Keet] Message from ${author}`,
65 `${author} wrote:\n\n${text}\n\n---\nRelayed via bridge-email from Keet P2P chat`
66 )
67 await smtp.quit()
68 }
69
70 async start () {
71 // Get bridge identity
72 if (!this.bridgeIdentity) {
73 try {
74 const identity = await this.keet.identity()
75 this.bridgeIdentity = identity.memberId || identity.id || ''
76 this.onLog(`Bridge identity: ${this.bridgeIdentity}`)
77 } catch (err) {
78 this.onLog(`Warning: Could not get identity: ${err.message}`)
79 }
80 }
81
82 // Connect IMAP
83 await this.imap.connect()
84 await this.imap.login()
85 await this.imap.select('INBOX')
86 this.onLog(`IMAP: connected to ${this.imap.host}`)
87
88 // Start polling IMAP for new messages
89 this._polling = true
90 this._poll()
91
92 // Watch Keet → Email
93 this.keet.watch(this.roomId, (msg) => {
94 const result = keetToEmail(msg, this.bridgeIdentity)
95 if (!result) return
96
97 this.router.routeToExternal(result.author, result.text)
98 })
99
100 this.onLog(`Bridge started: Email (${this.imap.host}) ↔ Keet`)
101 }
102
103 async _poll () {
104 if (!this._polling) return
105
106 try {
107 const unseen = await this.imap.searchUnseen()
108
109 for (const id of unseen) {
110 try {
111 const email = await this.imap.fetch(id)
112 const envelope = emailToEnvelope(email)
113
114 if (envelope) {
115 const text = emailToKeetText(envelope)
116 this.router.routeToKeet(envelope.author, text)
117 await this.imap.markSeen(id)
118 }
119 } catch (err) {
120 this.onLog(`Fetch error for message ${id}: ${err.message}`)
121 }
122 }
123 } catch (err) {
124 this.onLog(`Poll error: ${err.message}`)
125 }
126
127 this._pollTimer = setTimeout(() => this._poll(), this.pollInterval)
128 }
129
130 stop () {
131 this._polling = false
132 if (this._pollTimer) {
133 clearTimeout(this._pollTimer)
134 this._pollTimer = null
135 }
136 this.keet.stopWatch()
137 this.imap.close()
138 this.onLog('Bridge stopped')
139 }
140}
141
142export { EmailBridge }
bridge-email/lib/imap-client.js +223 -0
……@@ -0,0 +1,223 @@
1import tls from 'tls'
2
3class ImapClient {
4 constructor (opts = {}) {
5 this.host = opts.host
6 this.port = opts.port || 993
7 this.user = opts.user
8 this.pass = opts.pass
9 this._socket = null
10 this._tag = 0
11 this._buffer = ''
12 this._handlers = new Map()
13 this._connected = false
14 }
15
16 _nextTag () {
17 return `A${++this._tag}`
18 }
19
20 connect () {
21 return new Promise((resolve, reject) => {
22 this._socket = tls.connect({
23 host: this.host,
24 port: this.port,
25 rejectUnauthorized: true
26 })
27
28 this._socket.setEncoding('utf8')
29
30 let greeting = false
31 this._socket.on('data', (data) => {
32 this._buffer += data
33
34 if (!greeting && this._buffer.includes('\r\n')) {
35 greeting = true
36 this._buffer = ''
37 this._connected = true
38 resolve()
39 }
40
41 this._processBuffer()
42 })
43
44 this._socket.on('error', (err) => {
45 if (!greeting) reject(err)
46 })
47
48 this._socket.setTimeout(15000, () => {
49 if (!greeting) reject(new Error('IMAP connection timeout'))
50 })
51 })
52 }
53
54 _processBuffer () {
55 const lines = this._buffer.split('\r\n')
56 this._buffer = lines.pop() // keep incomplete
57
58 for (const line of lines) {
59 if (!line) continue
60
61 // Check if it's a tagged response
62 for (const [tag, handler] of this._handlers) {
63 if (line.startsWith(tag + ' ')) {
64 const rest = line.slice(tag.length + 1)
65 handler.lines.push(line)
66 if (rest.startsWith('OK')) {
67 handler.resolve(handler.lines)
68 } else {
69 handler.reject(new Error(`IMAP error: ${line}`))
70 }
71 this._handlers.delete(tag)
72 break
73 }
74 }
75
76 // Untagged response — add to current handler
77 if (line.startsWith('* ')) {
78 for (const handler of this._handlers.values()) {
79 handler.lines.push(line)
80 }
81 }
82 }
83 }
84
85 _command (cmd) {
86 return new Promise((resolve, reject) => {
87 const tag = this._nextTag()
88 this._handlers.set(tag, { resolve, reject, lines: [] })
89 this._socket.write(`${tag} ${cmd}\r\n`)
90
91 // Timeout
92 setTimeout(() => {
93 if (this._handlers.has(tag)) {
94 this._handlers.delete(tag)
95 reject(new Error(`IMAP timeout: ${cmd}`))
96 }
97 }, 15000)
98 })
99 }
100
101 async login () {
102 // Escape password with quotes
103 const pass = this.pass.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
104 return this._command(`LOGIN "${this.user}" "${pass}"`)
105 }
106
107 async select (mailbox = 'INBOX') {
108 return this._command(`SELECT "${mailbox}"`)
109 }
110
111 async searchUnseen () {
112 const lines = await this._command('SEARCH UNSEEN')
113 const searchLine = lines.find(l => l.startsWith('* SEARCH'))
114 if (!searchLine) return []
115
116 const ids = searchLine.slice(9).trim().split(/\s+/).filter(Boolean)
117 return ids.map(Number).filter(n => !isNaN(n))
118 }
119
120 async fetch (id) {
121 const lines = await this._command(`FETCH ${id} (RFC822)`)
122 // Extract the raw email body from FETCH response
123 const content = lines
124 .filter(l => !l.startsWith(`A${this._tag}`) && !l.match(/^\* \d+ FETCH/))
125 .join('\r\n')
126
127 return parseEmail(content)
128 }
129
130 async markSeen (id) {
131 return this._command(`STORE ${id} +FLAGS (\\Seen)`)
132 }
133
134 async logout () {
135 try {
136 await this._command('LOGOUT')
137 } catch {}
138 this.close()
139 }
140
141 close () {
142 this._connected = false
143 if (this._socket) {
144 this._socket.destroy()
145 this._socket = null
146 }
147 this._handlers.clear()
148 }
149}
150
151// Minimal email parser — extracts headers and text body
152function parseEmail (raw) {
153 const headerEnd = raw.indexOf('\r\n\r\n')
154 if (headerEnd === -1) {
155 return { from: '', to: '', subject: '', body: raw, date: '', messageId: '' }
156 }
157
158 const headerSection = raw.slice(0, headerEnd)
159 const body = raw.slice(headerEnd + 4)
160
161 function getHeader (name) {
162 const regex = new RegExp(`^${name}:\\s*(.+?)$`, 'mi')
163 const match = headerSection.match(regex)
164 return match ? match[1].trim() : ''
165 }
166
167 // Extract plain text from multipart if needed
168 let textBody = body
169 const contentType = getHeader('Content-Type')
170
171 if (contentType.includes('multipart')) {
172 const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/)
173 if (boundaryMatch) {
174 const boundary = boundaryMatch[1]
175 const parts = body.split('--' + boundary)
176
177 for (const part of parts) {
178 if (part.includes('text/plain')) {
179 const partBody = part.split('\r\n\r\n').slice(1).join('\r\n\r\n')
180 textBody = partBody.replace(/--\s*$/, '').trim()
181 break
182 }
183 }
184 }
185 }
186
187 // Decode quoted-printable if needed
188 if (raw.includes('Content-Transfer-Encoding: quoted-printable')) {
189 textBody = textBody.replace(/=\r?\n/g, '') // soft line breaks
190 // Collect bytes, then decode as UTF-8
191 const bytes = []
192 for (let i = 0; i < textBody.length; i++) {
193 if (textBody[i] === '=' && i + 2 < textBody.length) {
194 const hex = textBody.slice(i + 1, i + 3)
195 if (/^[0-9A-Fa-f]{2}$/.test(hex)) {
196 bytes.push(parseInt(hex, 16))
197 i += 2
198 continue
199 }
200 }
201 bytes.push(textBody.charCodeAt(i))
202 }
203 textBody = Buffer.from(bytes).toString('utf8')
204 }
205
206 // Decode base64 if needed
207 if (raw.includes('Content-Transfer-Encoding: base64')) {
208 try {
209 textBody = Buffer.from(textBody.replace(/\s/g, ''), 'base64').toString('utf8')
210 } catch {}
211 }
212
213 return {
214 from: getHeader('From'),
215 to: getHeader('To'),
216 subject: getHeader('Subject'),
217 body: textBody.trim(),
218 date: getHeader('Date'),
219 messageId: getHeader('Message-ID') || getHeader('Message-Id')
220 }
221}
222
223export { ImapClient, parseEmail }
bridge-email/lib/keet-client.js +156 -0
……@@ -0,0 +1,156 @@
1import http from 'http'
2import net from 'net'
3
4class KeetClient {
5 constructor (opts = {}) {
6 this.host = opts.host || 'localhost'
7 this.port = opts.port || 3117
8 this._watching = false
9 this._watchAbort = null
10 }
11
12 _rawRequest (method, path, body) {
13 return new Promise((resolve, reject) => {
14 const socket = net.connect(this.port, this.host)
15 let payload = ''
16
17 if (body) {
18 payload = JSON.stringify(body)
19 }
20
21 socket.on('connect', () => {
22 let req = `${method} ${path} HTTP/1.1\r\n`
23 req += `Host: ${this.host}:${this.port}\r\n`
24 if (payload) {
25 req += `Content-Type: application/json\r\n`
26 req += `Content-Length: ${Buffer.byteLength(payload)}\r\n`
27 }
28 req += `Connection: close\r\n`
29 req += `\r\n`
30 if (payload) req += payload
31 socket.write(req)
32 })
33
34 let data = ''
35 socket.on('data', (chunk) => { data += chunk.toString() })
36
37 socket.on('end', () => {
38 const idx = data.indexOf('\r\n\r\n')
39 if (idx === -1) return reject(new Error('No HTTP body'))
40 const jsonBody = data.slice(idx + 4)
41
42 try {
43 const parsed = JSON.parse(jsonBody)
44 if (!parsed.ok) reject(new Error(parsed.error || 'RPC error'))
45 else resolve(parsed.data)
46 } catch {
47 reject(new Error('Invalid JSON: ' + jsonBody.slice(0, 200)))
48 }
49 })
50
51 socket.on('error', reject)
52 socket.setTimeout(10000, () => {
53 socket.destroy(new Error('Request timeout'))
54 })
55 })
56 }
57
58 rpc (method, params = {}) {
59 return this._rawRequest('POST', '/rpc', { method, params })
60 }
61
62 async send (roomId, text) {
63 return this.rpc('send', { roomId, text })
64 }
65
66 async messages (roomId, limit = 20) {
67 return this._rawRequest('GET', `/rpc/messages/${roomId}?limit=${limit}`)
68 }
69
70 async rooms () {
71 return this._rawRequest('GET', '/rpc/rooms')
72 }
73
74 async identity () {
75 return this._rawRequest('GET', '/rpc/identity')
76 }
77
78 async status () {
79 return this._rawRequest('GET', '/rpc/status')
80 }
81
82 watch (roomId, onMessage) {
83 if (this._watching) return
84 this._watching = true
85 this._doWatch(roomId, onMessage)
86 }
87
88 _doWatch (roomId, onMessage) {
89 if (!this._watching) return
90
91 const req = http.request({
92 hostname: this.host,
93 port: this.port,
94 path: `/rpc/watch/${roomId}`,
95 method: 'GET',
96 headers: { Accept: 'text/event-stream' }
97 }, (res) => {
98 let buffer = ''
99
100 res.on('data', (chunk) => {
101 buffer += chunk.toString()
102 const events = buffer.split('\n\n')
103 buffer = events.pop()
104
105 for (const event of events) {
106 if (!event.trim()) continue
107 const lines = event.split('\n')
108 let type = 'message'
109 let data = null
110
111 for (const line of lines) {
112 if (line.startsWith('event: ')) type = line.slice(7)
113 if (line.startsWith('data: ')) {
114 try { data = JSON.parse(line.slice(6)) } catch {}
115 }
116 }
117
118 if (type === 'message' && data) {
119 onMessage(data)
120 }
121 }
122 })
123
124 res.on('end', () => {
125 if (this._watching) {
126 setTimeout(() => this._doWatch(roomId, onMessage), 2000)
127 }
128 })
129
130 res.on('error', () => {
131 if (this._watching) {
132 setTimeout(() => this._doWatch(roomId, onMessage), 5000)
133 }
134 })
135 })
136
137 req.on('error', () => {
138 if (this._watching) {
139 setTimeout(() => this._doWatch(roomId, onMessage), 5000)
140 }
141 })
142
143 this._watchAbort = req
144 req.end()
145 }
146
147 stopWatch () {
148 this._watching = false
149 if (this._watchAbort) {
150 this._watchAbort.destroy()
151 this._watchAbort = null
152 }
153 }
154}
155
156export { KeetClient }
bridge-email/lib/router.js +67 -0
……@@ -0,0 +1,67 @@
1import { createHash } from 'crypto'
2
3class Router {
4 constructor (opts = {}) {
5 this._seen = new Map()
6 this._maxSeen = opts.maxSeen || 10000
7 this._ttl = opts.ttl || 60000
8 this._onExternalMessage = opts.onExternalMessage || null
9 this._onKeetMessage = opts.onKeetMessage || null
10 }
11
12 _hash (text, author) {
13 return createHash('sha256')
14 .update(author || '')
15 .update(text || '')
16 .digest('hex')
17 .slice(0, 16)
18 }
19
20 _isDuplicate (hash) {
21 const now = Date.now()
22
23 if (this._seen.size > this._maxSeen) {
24 for (const [k, ts] of this._seen) {
25 if (now - ts > this._ttl) this._seen.delete(k)
26 }
27 }
28
29 if (this._seen.has(hash)) {
30 const ts = this._seen.get(hash)
31 if (now - ts < this._ttl) return true
32 }
33
34 this._seen.set(hash, now)
35 return false
36 }
37
38 routeToKeet (author, text) {
39 const hash = this._hash(text, author)
40 if (this._isDuplicate(hash)) return false
41
42 if (this._onKeetMessage) {
43 this._onKeetMessage(author, text)
44 }
45 return true
46 }
47
48 routeToExternal (name, text) {
49 const hash = this._hash(text, name)
50 if (this._isDuplicate(hash)) return false
51
52 if (this._onExternalMessage) {
53 this._onExternalMessage(name, text)
54 }
55 return true
56 }
57
58 get seenCount () {
59 return this._seen.size
60 }
61
62 clearSeen () {
63 this._seen.clear()
64 }
65}
66
67export { Router }
bridge-email/lib/smtp-client.js +111 -0
……@@ -0,0 +1,111 @@
1import tls from 'tls'
2import net from 'net'
3
4class SmtpClient {
5 constructor (opts = {}) {
6 this.host = opts.host
7 this.port = opts.port || 465
8 this.user = opts.user
9 this.pass = opts.pass
10 this.from = opts.from || opts.user
11 this.secure = opts.secure !== false // TLS by default
12 this._socket = null
13 }
14
15 connect () {
16 return new Promise((resolve, reject) => {
17 const onConnect = () => {
18 this._readLine().then(() => resolve()).catch(reject)
19 }
20
21 if (this.secure) {
22 this._socket = tls.connect({
23 host: this.host,
24 port: this.port,
25 rejectUnauthorized: true
26 }, onConnect)
27 } else {
28 this._socket = net.connect(this.port, this.host, onConnect)
29 }
30
31 this._socket.setEncoding('utf8')
32 this._socket.on('error', reject)
33 this._socket.setTimeout(15000, () => reject(new Error('SMTP connection timeout')))
34 })
35 }
36
37 _readLine () {
38 return new Promise((resolve, reject) => {
39 let buf = ''
40 const onData = (chunk) => {
41 buf += chunk
42 if (buf.includes('\r\n')) {
43 this._socket.removeListener('data', onData)
44 const code = parseInt(buf.slice(0, 3), 10)
45 if (code >= 400) reject(new Error(`SMTP ${code}: ${buf.trim()}`))
46 else resolve(buf.trim())
47 }
48 }
49 this._socket.on('data', onData)
50
51 setTimeout(() => {
52 this._socket.removeListener('data', onData)
53 reject(new Error('SMTP read timeout'))
54 }, 10000)
55 })
56 }
57
58 async _send (cmd) {
59 this._socket.write(cmd + '\r\n')
60 return this._readLine()
61 }
62
63 async ehlo () {
64 return this._send('EHLO bridge-email')
65 }
66
67 async auth () {
68 await this._send('AUTH LOGIN')
69 await this._send(Buffer.from(this.user).toString('base64'))
70 return this._send(Buffer.from(this.pass).toString('base64'))
71 }
72
73 async sendMail (to, subject, body, opts = {}) {
74 await this._send(`MAIL FROM:<${this.from}>`)
75 await this._send(`RCPT TO:<${to}>`)
76 await this._send('DATA')
77
78 const inReplyTo = opts.inReplyTo || ''
79 const references = opts.references || ''
80
81 let headers = `From: ${this.from}\r\n`
82 headers += `To: ${to}\r\n`
83 headers += `Subject: ${subject}\r\n`
84 headers += `Date: ${new Date().toUTCString()}\r\n`
85 headers += `Content-Type: text/plain; charset=utf-8\r\n`
86 headers += `Content-Transfer-Encoding: 8bit\r\n`
87 headers += `X-Bridge: bridge-email\r\n`
88
89 if (inReplyTo) headers += `In-Reply-To: ${inReplyTo}\r\n`
90 if (references) headers += `References: ${references}\r\n`
91
92 const message = headers + '\r\n' + body + '\r\n.'
93 return this._send(message)
94 }
95
96 async quit () {
97 try {
98 await this._send('QUIT')
99 } catch {}
100 this.close()
101 }
102
103 close () {
104 if (this._socket) {
105 this._socket.destroy()
106 this._socket = null
107 }
108 }
109}
110
111export { SmtpClient }
bridge-email/package-lock.json +875 -0
……@@ -0,0 +1,875 @@
1{
2 "name": "bridge-email",
3 "version": "1.0.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "bridge-email",
9 "version": "1.0.0",
10 "license": "MIT",
11 "dependencies": {
12 "b4a": "^1.0.0",
13 "bridge-envelope": "file:../bridge-envelope"
14 },
15 "devDependencies": {
16 "brittle": "^3.0.0"
17 }
18 },
19 "../bridge-envelope": {
20 "version": "1.0.0",
21 "license": "MIT",
22 "dependencies": {
23 "b4a": "^1.0.0",
24 "compact-encoding": "^2.0.0"
25 },
26 "devDependencies": {
27 "brittle": "^3.0.0"
28 }
29 },
30 "node_modules/@jridgewell/resolve-uri": {
31 "version": "3.1.2",
32 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
33 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
34 "dev": true,
35 "license": "MIT",
36 "engines": {
37 "node": ">=6.0.0"
38 }
39 },
40 "node_modules/@jridgewell/sourcemap-codec": {
41 "version": "1.5.5",
42 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
43 "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
44 "dev": true,
45 "license": "MIT"
46 },
47 "node_modules/@jridgewell/trace-mapping": {
48 "version": "0.3.31",
49 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
50 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
51 "dev": true,
52 "license": "MIT",
53 "dependencies": {
54 "@jridgewell/resolve-uri": "^3.1.0",
55 "@jridgewell/sourcemap-codec": "^1.4.14"
56 }
57 },
58 "node_modules/@types/istanbul-lib-coverage": {
59 "version": "2.0.6",
60 "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
61 "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
62 "dev": true,
63 "license": "MIT"
64 },
65 "node_modules/b4a": {
66 "version": "1.7.4",
67 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.4.tgz",
68 "integrity": "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug==",
69 "license": "Apache-2.0",
70 "peerDependencies": {
71 "react-native-b4a": "*"
72 },
73 "peerDependenciesMeta": {
74 "react-native-b4a": {
75 "optional": true
76 }
77 }
78 },
79 "node_modules/bare-addon-resolve": {
80 "version": "1.10.0",
81 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
82 "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
83 "dev": true,
84 "license": "Apache-2.0",
85 "dependencies": {
86 "bare-module-resolve": "^1.10.0",
87 "bare-semver": "^1.0.0"
88 },
89 "peerDependencies": {
90 "bare-url": "*"
91 },
92 "peerDependenciesMeta": {
93 "bare-url": {
94 "optional": true
95 }
96 }
97 },
98 "node_modules/bare-ansi-escapes": {
99 "version": "2.2.3",
100 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
101 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
102 "dev": true,
103 "license": "Apache-2.0",
104 "dependencies": {
105 "bare-stream": "^2.6.5"
106 },
107 "peerDependencies": {
108 "bare-buffer": "*"
109 },
110 "peerDependenciesMeta": {
111 "bare-buffer": {
112 "optional": true
113 }
114 }
115 },
116 "node_modules/bare-assert": {
117 "version": "1.2.0",
118 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
119 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
120 "dev": true,
121 "license": "Apache-2.0",
122 "dependencies": {
123 "bare-inspect": "^3.1.2"
124 }
125 },
126 "node_modules/bare-bundle": {
127 "version": "1.10.0",
128 "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
129 "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
130 "dev": true,
131 "license": "Apache-2.0",
132 "peerDependencies": {
133 "bare-buffer": "*",
134 "bare-url": "*"
135 },
136 "peerDependenciesMeta": {
137 "bare-buffer": {
138 "optional": true
139 },
140 "bare-url": {
141 "optional": true
142 }
143 }
144 },
145 "node_modules/bare-cov": {
146 "version": "1.2.1",
147 "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz",
148 "integrity": "sha512-SHNqb/WG9udUEj8m5qGSehoUQ9o0Xzv8nrf4HIdmG+KNU/XdhPLhTP5VPrHnSAMAXhAzEB4Q+2IYZ0x9qPzN1g==",
149 "dev": true,
150 "license": "Apache-2.0",
151 "dependencies": {
152 "bare-fs": "^4.1.2",
153 "bare-inspector": "^4.0.1",
154 "bare-path": "^3.0.0",
155 "bare-process": "^4.2.1",
156 "bare-url": "^2.1.5",
157 "bare-v8-to-istanbul": "^1.0.2",
158 "picomatch": "^4.0.2",
159 "which-runtime": "^1.2.1"
160 }
161 },
162 "node_modules/bare-crypto": {
163 "version": "1.13.0",
164 "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.0.tgz",
165 "integrity": "sha512-RQl13yD+YTACWUZHMMck0C6LNjXgGgRyhVC557ag0xIrkxXf4IQwpnc8WOB58f4k5kSzKhIjy05oW3HUBrFpSQ==",
166 "dev": true,
167 "license": "Apache-2.0",
168 "dependencies": {
169 "bare-stream": "^2.6.3"
170 },
171 "peerDependencies": {
172 "bare-buffer": "*"
173 },
174 "peerDependenciesMeta": {
175 "bare-buffer": {
176 "optional": true
177 }
178 }
179 },
180 "node_modules/bare-debug-log": {
181 "version": "2.0.0",
182 "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
183 "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
184 "dev": true,
185 "license": "Apache-2.0",
186 "dependencies": {
187 "bare-os": "^3.0.1"
188 }
189 },
190 "node_modules/bare-dns": {
191 "version": "2.1.4",
192 "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
193 "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==",
194 "dev": true,
195 "license": "Apache-2.0",
196 "engines": {
197 "bare": ">=1.7.0"
198 }
199 },
200 "node_modules/bare-encoding": {
201 "version": "1.0.3",
202 "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
203 "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
204 "dev": true,
205 "license": "Apache-2.0",
206 "peerDependencies": {
207 "bare-buffer": "*"
208 },
209 "peerDependenciesMeta": {
210 "bare-buffer": {
211 "optional": true
212 }
213 }
214 },
215 "node_modules/bare-env": {
216 "version": "3.0.0",
217 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
218 "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
219 "dev": true,
220 "license": "Apache-2.0",
221 "dependencies": {
222 "bare-os": "^3.0.1"
223 }
224 },
225 "node_modules/bare-events": {
226 "version": "2.8.2",
227 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
228 "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
229 "dev": true,
230 "license": "Apache-2.0",
231 "peerDependencies": {
232 "bare-abort-controller": "*"
233 },
234 "peerDependenciesMeta": {
235 "bare-abort-controller": {
236 "optional": true
237 }
238 }
239 },
240 "node_modules/bare-format": {
241 "version": "1.0.1",
242 "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.1.tgz",
243 "integrity": "sha512-1oS+LZrWK6tnYnvNSHDGljc2MPunRxwhpFriuCgzNF+oklrnwmBKD91tS0yt+jpl2j3UgcSDzBIMiVTvLs9A8w==",
244 "dev": true,
245 "license": "Apache-2.0",
246 "dependencies": {
247 "bare-inspect": "^3.0.0"
248 }
249 },
250 "node_modules/bare-fs": {
251 "version": "4.5.4",
252 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz",
253 "integrity": "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==",
254 "dev": true,
255 "license": "Apache-2.0",
256 "dependencies": {
257 "bare-events": "^2.5.4",
258 "bare-path": "^3.0.0",
259 "bare-stream": "^2.6.4",
260 "bare-url": "^2.2.2",
261 "fast-fifo": "^1.3.2"
262 },
263 "engines": {
264 "bare": ">=1.16.0"
265 },
266 "peerDependencies": {
267 "bare-buffer": "*"
268 },
269 "peerDependenciesMeta": {
270 "bare-buffer": {
271 "optional": true
272 }
273 }
274 },
275 "node_modules/bare-hrtime": {
276 "version": "2.1.1",
277 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
278 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
279 "dev": true,
280 "license": "Apache-2.0"
281 },
282 "node_modules/bare-http-parser": {
283 "version": "1.0.1",
284 "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.0.1.tgz",
285 "integrity": "sha512-A3LTDTcELcmNJ3g5liIaS038v/BQxOhA9cjhBESn7eoV7QCuMoIRBKLDadDe08flxyLbxI2f+1l2MZ/5+HnKPA==",
286 "dev": true,
287 "license": "Apache-2.0"
288 },
289 "node_modules/bare-http1": {
290 "version": "4.2.3",
291 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.2.3.tgz",
292 "integrity": "sha512-zyvHF3xzvBJNpuw0QydE9etTn2AyC/TRVDWTo8Ed3LQEEGaFOkv6KHhaJPlf4eWEeTekk41Fowie+fTsXoNxkA==",
293 "dev": true,
294 "license": "Apache-2.0",
295 "dependencies": {
296 "bare-events": "^2.6.0",
297 "bare-http-parser": "^1.0.0",
298 "bare-stream": "^2.3.0",
299 "bare-tcp": "^2.2.0"
300 },
301 "peerDependencies": {
302 "bare-buffer": "*",
303 "bare-url": "*"
304 },
305 "peerDependenciesMeta": {
306 "bare-buffer": {
307 "optional": true
308 },
309 "bare-url": {
310 "optional": true
311 }
312 }
313 },
314 "node_modules/bare-https": {
315 "version": "2.1.2",
316 "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.2.tgz",
317 "integrity": "sha512-Q+TTydUDsuKQJvh8dX2dvOXCR9fM3xR5TBmKaFrs5p7Lj7XbKX7v4vIUJ36H0SXg2xCOQxXKIbjwrLg5tfJNYg==",
318 "dev": true,
319 "license": "Apache-2.0",
320 "dependencies": {
321 "bare-http1": "^4.0.0",
322 "bare-tcp": "^2.2.0",
323 "bare-tls": "^2.0.0"
324 }
325 },
326 "node_modules/bare-inspect": {
327 "version": "3.1.4",
328 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
329 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
330 "dev": true,
331 "license": "Apache-2.0",
332 "dependencies": {
333 "bare-ansi-escapes": "^2.1.0",
334 "bare-type": "^1.0.0"
335 },
336 "engines": {
337 "bare": ">=1.18.0"
338 }
339 },
340 "node_modules/bare-inspector": {
341 "version": "4.0.3",
342 "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-4.0.3.tgz",
343 "integrity": "sha512-yYk6+7tTVKJKHOja5+QBn989ETv8oVsmwoH0nCM+QYC6w5MC03GC4aQNK+lipAZSz1EaAj42WZ8Wewjpy0L9TA==",
344 "dev": true,
345 "license": "Apache-2.0",
346 "dependencies": {
347 "bare-events": "^2.1.0",
348 "bare-http1": "^4.0.0",
349 "bare-stream": "^2.0.0",
350 "bare-url": "^2.0.0",
351 "bare-ws": "^2.0.0"
352 },
353 "engines": {
354 "bare": ">=1.2.0"
355 },
356 "peerDependencies": {
357 "bare-tcp": "*"
358 },
359 "peerDependenciesMeta": {
360 "bare-tcp": {
361 "optional": true
362 }
363 }
364 },
365 "node_modules/bare-module": {
366 "version": "6.1.3",
367 "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz",
368 "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==",
369 "dev": true,
370 "license": "Apache-2.0",
371 "dependencies": {
372 "bare-bundle": "^1.3.0",
373 "bare-module-lexer": "^1.0.0",
374 "bare-module-resolve": "^1.8.0",
375 "bare-path": "^3.0.0",
376 "bare-url": "^2.0.1"
377 },
378 "engines": {
379 "bare": ">=1.23.0"
380 },
381 "peerDependencies": {
382 "bare-buffer": "*"
383 },
384 "peerDependenciesMeta": {
385 "bare-buffer": {
386 "optional": true
387 }
388 }
389 },
390 "node_modules/bare-module-lexer": {
391 "version": "1.4.7",
392 "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
393 "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
394 "dev": true,
395 "license": "Apache-2.0",
396 "dependencies": {
397 "require-addon": "^1.0.2"
398 },
399 "peerDependencies": {
400 "bare-buffer": "*"
401 },
402 "peerDependenciesMeta": {
403 "bare-buffer": {
404 "optional": true
405 }
406 }
407 },
408 "node_modules/bare-module-resolve": {
409 "version": "1.12.1",
410 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz",
411 "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==",
412 "dev": true,
413 "license": "Apache-2.0",
414 "dependencies": {
415 "bare-semver": "^1.0.0"
416 },
417 "peerDependencies": {
418 "bare-url": "*"
419 },
420 "peerDependenciesMeta": {
421 "bare-url": {
422 "optional": true
423 }
424 }
425 },
426 "node_modules/bare-net": {
427 "version": "2.2.0",
428 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.2.0.tgz",
429 "integrity": "sha512-UF7cAbHsGE+H6uEqWF5IULBow1x58chZz4g3ALgHtv7wZsFcCbRDt0JKWEumf5Oma3QWS1Q6aLi0Rpll8RElMg==",
430 "dev": true,
431 "license": "Apache-2.0",
432 "dependencies": {
433 "bare-events": "^2.2.2",
434 "bare-pipe": "^4.0.0",
435 "bare-stream": "^2.0.0",
436 "bare-tcp": "^2.0.0"
437 }
438 },
439 "node_modules/bare-os": {
440 "version": "3.6.2",
441 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
442 "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
443 "dev": true,
444 "license": "Apache-2.0",
445 "engines": {
446 "bare": ">=1.14.0"
447 }
448 },
449 "node_modules/bare-path": {
450 "version": "3.0.0",
451 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
452 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
453 "dev": true,
454 "license": "Apache-2.0",
455 "dependencies": {
456 "bare-os": "^3.0.1"
457 }
458 },
459 "node_modules/bare-pipe": {
460 "version": "4.1.3",
461 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.3.tgz",
462 "integrity": "sha512-DqQsx93rAzre6yJ9T6l/Vgh+X+bntkVMB1X5ggtXjXtqtMmF2Y2RVlCzxxy/09R6yeR9FSWBEUIaMYJL1/5VDA==",
463 "dev": true,
464 "license": "Apache-2.0",
465 "dependencies": {
466 "bare-events": "^2.0.0",
467 "bare-stream": "^2.0.0"
468 },
469 "engines": {
470 "bare": ">=1.16.0"
471 }
472 },
473 "node_modules/bare-process": {
474 "version": "4.2.2",
475 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.2.2.tgz",
476 "integrity": "sha512-ikzEw+HGLB+2lS/WLVEsqi8BXBwzleG3n7DYI0v6/YNklvNabOIGlLd1dof+7Jw5ob4jsBRPtyflweVahY7rFA==",
477 "dev": true,
478 "license": "Apache-2.0",
479 "dependencies": {
480 "bare-env": "^3.0.0",
481 "bare-events": "^2.3.1",
482 "bare-hrtime": "^2.0.0",
483 "bare-os": "^3.5.0",
484 "bare-pipe": "^4.0.0",
485 "bare-signals": "^4.0.0",
486 "bare-tty": "^5.0.0"
487 }
488 },
489 "node_modules/bare-semver": {
490 "version": "1.0.2",
491 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz",
492 "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==",
493 "dev": true,
494 "license": "Apache-2.0"
495 },
496 "node_modules/bare-signals": {
497 "version": "4.2.0",
498 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz",
499 "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==",
500 "dev": true,
501 "license": "Apache-2.0",
502 "dependencies": {
503 "bare-events": "^2.5.3",
504 "bare-os": "^3.3.1"
505 },
506 "engines": {
507 "bare": ">=1.7.0"
508 }
509 },
510 "node_modules/bare-stream": {
511 "version": "2.7.0",
512 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
513 "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
514 "dev": true,
515 "license": "Apache-2.0",
516 "dependencies": {
517 "streamx": "^2.21.0"
518 },
519 "peerDependencies": {
520 "bare-buffer": "*",
521 "bare-events": "*"
522 },
523 "peerDependenciesMeta": {
524 "bare-buffer": {
525 "optional": true
526 },
527 "bare-events": {
528 "optional": true
529 }
530 }
531 },
532 "node_modules/bare-subprocess": {
533 "version": "5.2.2",
534 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.2.tgz",
535 "integrity": "sha512-L6oXgQ1aWs25RtG5Ky0bDD06p3RAcVVrDDMWb1DfXpHtyEWxamcyIWUbSykxMTWrpLlmSj6ytbb6yoKehGFfmw==",
536 "dev": true,
537 "license": "Apache-2.0",
538 "dependencies": {
539 "bare-env": "^3.0.0",
540 "bare-events": "^2.5.4",
541 "bare-os": "^3.0.1",
542 "bare-pipe": "^4.0.0",
543 "bare-url": "^2.2.2"
544 },
545 "engines": {
546 "bare": ">=1.7.0"
547 },
548 "peerDependencies": {
549 "bare-buffer": "*"
550 },
551 "peerDependenciesMeta": {
552 "bare-buffer": {
553 "optional": true
554 }
555 }
556 },
557 "node_modules/bare-tcp": {
558 "version": "2.2.4",
559 "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.4.tgz",
560 "integrity": "sha512-vGjfMkyhS1lxp+Y4+gXRDR1ijZMK2cKx0RBwtYKUuZbD90ARCoM6bAiTa1HBztumCIC+NXb8eXdMFXyuYwlPyw==",
561 "dev": true,
562 "license": "Apache-2.0",
563 "dependencies": {
564 "bare-dns": "^2.0.4",
565 "bare-events": "^2.5.4",
566 "bare-stream": "^2.6.4"
567 },
568 "engines": {
569 "bare": ">=1.16.0"
570 }
571 },
572 "node_modules/bare-tls": {
573 "version": "2.1.7",
574 "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.1.7.tgz",
575 "integrity": "sha512-h6wcNXQdBeTX7fed9tjPp0/9cA/QfcBTv3ItgjnbUk4rWAU8bEFalZCZnUDdCK/t9zrNfJ+yvcPx4D/1Y6biyA==",
576 "dev": true,
577 "license": "Apache-2.0",
578 "dependencies": {
579 "bare-net": "^2.0.1",
580 "bare-stream": "^2.6.4"
581 },
582 "engines": {
583 "bare": ">=1.7.0"
584 }
585 },
586 "node_modules/bare-tty": {
587 "version": "5.0.3",
588 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.0.3.tgz",
589 "integrity": "sha512-jW24RBWRZOMHuSEWC9mh8wUKO5WUNl0UWUTNPn3yZ8qkOqwa8vaV2dR3a+BvblONPk7V35wuvK9P8508+judAg==",
590 "dev": true,
591 "license": "Apache-2.0",
592 "dependencies": {
593 "bare-events": "^2.2.0",
594 "bare-signals": "^4.0.0",
595 "bare-stream": "^2.0.0"
596 },
597 "engines": {
598 "bare": ">=1.16.0"
599 }
600 },
601 "node_modules/bare-type": {
602 "version": "1.1.0",
603 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
604 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
605 "dev": true,
606 "license": "Apache-2.0",
607 "engines": {
608 "bare": ">=1.2.0"
609 }
610 },
611 "node_modules/bare-url": {
612 "version": "2.3.2",
613 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
614 "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
615 "dev": true,
616 "license": "Apache-2.0",
617 "peer": true,
618 "dependencies": {
619 "bare-path": "^3.0.0"
620 }
621 },
622 "node_modules/bare-utils": {
623 "version": "1.5.1",
624 "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.5.1.tgz",
625 "integrity": "sha512-mxCkFvmDU3mlD/mb+pT64kKXOsx2KMsWLQbngN1LB+NOXfhfnRnyvpy3VZc6m7gzQxe57Bsi+aTCBqA4/S3elQ==",
626 "dev": true,
627 "license": "Apache-2.0",
628 "dependencies": {
629 "bare-debug-log": "^2.0.0",
630 "bare-encoding": "^1.0.0",
631 "bare-format": "^1.0.0",
632 "bare-inspect": "^3.0.0",
633 "bare-type": "^1.0.6"
634 }
635 },
636 "node_modules/bare-v8-to-istanbul": {
637 "version": "1.0.2",
638 "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
639 "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==",
640 "dev": true,
641 "license": "Apache-2.0",
642 "dependencies": {
643 "bare-assert": "^1.0.2",
644 "bare-fs": "^4.1.2",
645 "bare-module": "^6.1.2",
646 "bare-path": "^3.0.0",
647 "bare-process": "^4.2.0",
648 "bare-url": "^2.1.5",
649 "bare-utils": "^1.2.0",
650 "v8-to-istanbul": "^9.3.0",
651 "which-runtime": "^1.2.1"
652 }
653 },
654 "node_modules/bare-ws": {
655 "version": "2.0.4",
656 "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.0.4.tgz",
657 "integrity": "sha512-SQMXzBYna9dRj57Dz1/ag+VWHCRXbfCjMHgyfM2F2lhkVLzMjnVSZP72aVeFWPFqe494Rd70Kzhe2JElGwFlJQ==",
658 "dev": true,
659 "license": "Apache-2.0",
660 "dependencies": {
661 "bare-crypto": "^1.2.0",
662 "bare-events": "^2.3.1",
663 "bare-http1": "^4.0.0",
664 "bare-https": "^2.0.0",
665 "bare-stream": "^2.1.2"
666 },
667 "peerDependencies": {
668 "bare-buffer": "*",
669 "bare-url": "*"
670 },
671 "peerDependenciesMeta": {
672 "bare-buffer": {
673 "optional": true
674 },
675 "bare-url": {
676 "optional": true
677 }
678 }
679 },
680 "node_modules/bridge-envelope": {
681 "resolved": "../bridge-envelope",
682 "link": true
683 },
684 "node_modules/brittle": {
685 "version": "3.19.1",
686 "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz",
687 "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==",
688 "dev": true,
689 "license": "Apache-2.0",
690 "dependencies": {
691 "b4a": "^1.6.0",
692 "bare-assert": "^1.0.2",
693 "bare-cov": "^1.1.0",
694 "bare-fs": "^4.1.6",
695 "bare-os": "^3.6.1",
696 "bare-path": "^3.0.0",
697 "bare-process": "^4.2.1",
698 "bare-subprocess": "^5.0.0",
699 "bare-url": "^2.1.6",
700 "error-stack-parser": "^2.1.4",
701 "globbie": "^1.0.2",
702 "paparam": "^1.6.2",
703 "same-object": "^1.0.2",
704 "test-tmp": "^1.4.0",
705 "tmatch": "^5.0.0"
706 },
707 "bin": {
708 "brittle": "bin/node.js",
709 "brittle-bare": "bin/bare.js",
710 "brittle-node": "bin/node.js",
711 "brittle-pear": "bin/pear.js"
712 }
713 },
714 "node_modules/convert-source-map": {
715 "version": "2.0.0",
716 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
717 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
718 "dev": true,
719 "license": "MIT"
720 },
721 "node_modules/error-stack-parser": {
722 "version": "2.1.4",
723 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
724 "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
725 "dev": true,
726 "license": "MIT",
727 "dependencies": {
728 "stackframe": "^1.3.4"
729 }
730 },
731 "node_modules/events-universal": {
732 "version": "1.0.1",
733 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
734 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
735 "dev": true,
736 "license": "Apache-2.0",
737 "dependencies": {
738 "bare-events": "^2.7.0"
739 }
740 },
741 "node_modules/fast-fifo": {
742 "version": "1.3.2",
743 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
744 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
745 "dev": true,
746 "license": "MIT"
747 },
748 "node_modules/globbie": {
749 "version": "1.0.3",
750 "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz",
751 "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==",
752 "dev": true,
753 "license": "Apache-2.0",
754 "dependencies": {
755 "bare-fs": "^4.1.2",
756 "bare-path": "^3.0.0",
757 "bare-process": "^4.2.1",
758 "picomatch": "^4.0.2"
759 }
760 },
761 "node_modules/paparam": {
762 "version": "1.10.0",
763 "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.0.tgz",
764 "integrity": "sha512-p36QQrwU3X6fj5d2JN20B4lcZI/O3fxMVUHc1xD7MNe5bcOf3jIgE0CIK5Zv/s5YAUGlobTJ/kKXrh1FJVrs3w==",
765 "dev": true,
766 "license": "Apache-2.0"
767 },
768 "node_modules/picomatch": {
769 "version": "4.0.3",
770 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
771 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
772 "dev": true,
773 "license": "MIT",
774 "engines": {
775 "node": ">=12"
776 },
777 "funding": {
778 "url": "https://github.com/sponsors/jonschlinkert"
779 }
780 },
781 "node_modules/require-addon": {
782 "version": "1.2.0",
783 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
784 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
785 "dev": true,
786 "license": "Apache-2.0",
787 "dependencies": {
788 "bare-addon-resolve": "^1.3.0"
789 },
790 "engines": {
791 "bare": ">=1.10.0"
792 }
793 },
794 "node_modules/same-object": {
795 "version": "1.0.2",
796 "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz",
797 "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==",
798 "dev": true,
799 "license": "MIT"
800 },
801 "node_modules/stackframe": {
802 "version": "1.3.4",
803 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
804 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
805 "dev": true,
806 "license": "MIT"
807 },
808 "node_modules/streamx": {
809 "version": "2.23.0",
810 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
811 "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
812 "dev": true,
813 "license": "MIT",
814 "dependencies": {
815 "events-universal": "^1.0.0",
816 "fast-fifo": "^1.3.2",
817 "text-decoder": "^1.1.0"
818 }
819 },
820 "node_modules/test-tmp": {
821 "version": "1.4.0",
822 "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz",
823 "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==",
824 "dev": true,
825 "license": "MIT",
826 "dependencies": {
827 "bare-fs": "^4.0.1",
828 "bare-os": "^3.3.0",
829 "bare-path": "^3.0.0"
830 }
831 },
832 "node_modules/text-decoder": {
833 "version": "1.2.6",
834 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.6.tgz",
835 "integrity": "sha512-27FeW5GQFDfw0FpwMQhMagB7BztOOlmjcSRi97t2oplhKVTZtp0DZbSegSaXS5IIC6mxMvBG4AR1Sgc6BX3CQg==",
836 "dev": true,
837 "license": "Apache-2.0",
838 "dependencies": {
839 "b4a": "^1.6.4"
840 }
841 },
842 "node_modules/tmatch": {
843 "version": "5.0.0",
844 "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz",
845 "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==",
846 "dev": true,
847 "license": "ISC",
848 "engines": {
849 "node": ">=8"
850 }
851 },
852 "node_modules/v8-to-istanbul": {
853 "version": "9.3.0",
854 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
855 "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
856 "dev": true,
857 "license": "ISC",
858 "dependencies": {
859 "@jridgewell/trace-mapping": "^0.3.12",
860 "@types/istanbul-lib-coverage": "^2.0.1",
861 "convert-source-map": "^2.0.0"
862 },
863 "engines": {
864 "node": ">=10.12.0"
865 }
866 },
867 "node_modules/which-runtime": {
868 "version": "1.3.2",
869 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
870 "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
871 "dev": true,
872 "license": "Apache-2.0"
873 }
874 }
875}
bridge-email/package.json +22 -0
……@@ -0,0 +1,22 @@
1{
2 "name": "bridge-email",
3 "version": "1.0.0",
4 "description": "Email ↔ Keet bidirectional bridge (IMAP/SMTP)",
5 "type": "module",
6 "main": "index.js",
7 "pear": {
8 "name": "bridge-email",
9 "type": "terminal"
10 },
11 "scripts": {
12 "test": "brittle test/*.test.js"
13 },
14 "dependencies": {
15 "bridge-envelope": "file:../bridge-envelope",
16 "b4a": "^1.0.0"
17 },
18 "devDependencies": {
19 "brittle": "^3.0.0"
20 },
21 "license": "MIT"
22}
bridge-email/test/adapter.test.js +139 -0
……@@ -0,0 +1,139 @@
1import test from 'brittle'
2import { emailToEnvelope, keetToEmail, emailToKeetText, keetToEmailText, extractEmail, extractName } from '../lib/adapter.js'
3import { TYPES, PLATFORMS } from 'bridge-envelope'
4
5test('extractEmail - angle bracket format', (t) => {
6 t.is(extractEmail('Alice <alice@example.com>'), 'alice@example.com')
7 t.is(extractEmail('"Bob Smith" <bob@test.org>'), 'bob@test.org')
8})
9
10test('extractEmail - bare address', (t) => {
11 t.is(extractEmail('alice@example.com'), 'alice@example.com')
12})
13
14test('extractName - display name', (t) => {
15 t.is(extractName('Alice <alice@example.com>'), 'Alice')
16 t.is(extractName('"Bob Smith" <bob@test.org>'), 'Bob Smith')
17})
18
19test('extractName - bare address falls back', (t) => {
20 t.is(extractName('alice@example.com'), 'alice@example.com')
21})
22
23test('emailToEnvelope - basic email', (t) => {
24 const email = {
25 from: 'Alice <alice@example.com>',
26 to: 'bridge@example.com',
27 subject: 'Hello',
28 body: 'This is a test message',
29 date: 'Fri, 14 Feb 2026 10:00:00 +0000',
30 messageId: '<abc123@example.com>'
31 }
32
33 const env = emailToEnvelope(email)
34 t.ok(env)
35 t.is(env.type, TYPES.TEXT)
36 t.is(env.platform, PLATFORMS.EMAIL)
37 t.is(env.author, 'Alice')
38 t.ok(env.content.includes('Hello'))
39 t.ok(env.content.includes('test message'))
40 t.is(env._messageId, '<abc123@example.com>')
41 t.is(env._subject, 'Hello')
42})
43
44test('emailToEnvelope - no subject', (t) => {
45 const email = {
46 from: 'bob@test.org',
47 subject: '',
48 body: 'Just a body',
49 messageId: ''
50 }
51
52 const env = emailToEnvelope(email)
53 t.ok(env)
54 t.is(env.content, 'Just a body')
55})
56
57test('emailToEnvelope - empty body and subject', (t) => {
58 const email = { from: 'x@y.z', subject: '', body: '', messageId: '' }
59 t.is(emailToEnvelope(email), null)
60})
61
62test('emailToEnvelope - skip bridged emails', (t) => {
63 const email = {
64 from: 'bridge@test.com',
65 subject: '[Keet] Message',
66 body: '[email] Alice: hello',
67 messageId: ''
68 }
69 t.is(emailToEnvelope(email), null)
70})
71
72test('emailToEnvelope - null input', (t) => {
73 t.is(emailToEnvelope(null), null)
74})
75
76test('keetToEmail - normal message', (t) => {
77 const msg = { name: 'fleeky', text: 'Hello!', memberId: 'abc' }
78 const result = keetToEmail(msg, 'bridge-id')
79 t.ok(result)
80 t.is(result.author, 'fleeky')
81 t.is(result.text, 'Hello!')
82 t.ok(result.subject.includes('fleeky'))
83 t.ok(result.body.includes('Hello!'))
84})
85
86test('keetToEmail - skip bridge identity', (t) => {
87 const msg = { name: 'clode', text: 'Hello', memberId: 'bridge-id' }
88 t.is(keetToEmail(msg, 'bridge-id'), null)
89})
90
91test('keetToEmail - skip system messages', (t) => {
92 const msg = { name: 'system', text: 'joined', type: 'system' }
93 t.is(keetToEmail(msg, 'bridge-id'), null)
94})
95
96test('keetToEmail - skip already bridged', (t) => {
97 const msg = { name: 'x', text: '[email] Alice: hello', memberId: 'y' }
98 t.is(keetToEmail(msg, 'bridge-id'), null)
99})
100
101test('keetToEmail - skip empty', (t) => {
102 t.is(keetToEmail({ name: 'x', text: '', memberId: 'y' }, 'b'), null)
103 t.is(keetToEmail(null, 'b'), null)
104})
105
106test('emailToKeetText - basic', (t) => {
107 const email = {
108 from: 'Alice <alice@example.com>',
109 subject: 'Hello',
110 body: 'Test message',
111 messageId: ''
112 }
113 const env = emailToEnvelope(email)
114 const text = emailToKeetText(env)
115 t.ok(text.includes('[email]'))
116 t.ok(text.includes('Alice'))
117 t.ok(text.includes('Hello'))
118})
119
120test('emailToKeetText - long email truncated', (t) => {
121 const email = {
122 from: 'bob@test.org',
123 subject: 'Long',
124 body: 'x'.repeat(1000),
125 messageId: ''
126 }
127 const env = emailToEnvelope(email)
128 const text = emailToKeetText(env)
129 t.ok(text.includes('...'))
130 t.ok(text.length < 600)
131})
132
133test('keetToEmailText - format', (t) => {
134 const result = keetToEmailText('fleeky', 'Hello world')
135 t.is(result.subject, '[Keet] Message from fleeky')
136 t.ok(result.body.includes('fleeky wrote'))
137 t.ok(result.body.includes('Hello world'))
138 t.ok(result.body.includes('bridge-email'))
139})
bridge-email/test/parser.test.js +96 -0
……@@ -0,0 +1,96 @@
1import test from 'brittle'
2import { parseEmail } from '../lib/imap-client.js'
3
4test('parseEmail - simple email', (t) => {
5 const raw = [
6 'From: Alice <alice@example.com>',
7 'To: bob@example.com',
8 'Subject: Test',
9 'Date: Fri, 14 Feb 2026 10:00:00 +0000',
10 'Message-ID: <abc123@example.com>',
11 'Content-Type: text/plain; charset=utf-8',
12 '',
13 'Hello, this is a test email.'
14 ].join('\r\n')
15
16 const email = parseEmail(raw)
17 t.is(email.from, 'Alice <alice@example.com>')
18 t.is(email.to, 'bob@example.com')
19 t.is(email.subject, 'Test')
20 t.is(email.body, 'Hello, this is a test email.')
21 t.is(email.messageId, '<abc123@example.com>')
22})
23
24test('parseEmail - multiline body', (t) => {
25 const raw = [
26 'From: sender@test.com',
27 'Subject: Multi',
28 '',
29 'Line 1',
30 'Line 2',
31 'Line 3'
32 ].join('\r\n')
33
34 const email = parseEmail(raw)
35 t.ok(email.body.includes('Line 1'))
36 t.ok(email.body.includes('Line 2'))
37 t.ok(email.body.includes('Line 3'))
38})
39
40test('parseEmail - multipart text/plain extraction', (t) => {
41 const raw = [
42 'From: sender@test.com',
43 'Subject: Multipart',
44 'Content-Type: multipart/alternative; boundary="boundary123"',
45 '',
46 '--boundary123',
47 'Content-Type: text/plain; charset=utf-8',
48 '',
49 'Plain text body here',
50 '--boundary123',
51 'Content-Type: text/html; charset=utf-8',
52 '',
53 '<html><body>HTML body</body></html>',
54 '--boundary123--'
55 ].join('\r\n')
56
57 const email = parseEmail(raw)
58 t.ok(email.body.includes('Plain text body'))
59 t.absent(email.body.includes('HTML body'))
60})
61
62test('parseEmail - quoted-printable decoding', (t) => {
63 const raw = [
64 'From: sender@test.com',
65 'Subject: QP',
66 'Content-Transfer-Encoding: quoted-printable',
67 '',
68 'Hello =C3=A9l=C3=A8ve'
69 ].join('\r\n')
70
71 const email = parseEmail(raw)
72 t.ok(email.body.includes('Hello'))
73 // UTF-8 decoded é and è
74 t.ok(email.body.includes('élève'))
75})
76
77test('parseEmail - no headers', (t) => {
78 const raw = 'Just a body with no headers at all'
79 const email = parseEmail(raw)
80 t.is(email.from, '')
81 t.is(email.subject, '')
82 t.ok(email.body.includes('Just a body'))
83})
84
85test('parseEmail - base64 encoding', (t) => {
86 const raw = [
87 'From: sender@test.com',
88 'Subject: B64',
89 'Content-Transfer-Encoding: base64',
90 '',
91 Buffer.from('Decoded base64 content').toString('base64')
92 ].join('\r\n')
93
94 const email = parseEmail(raw)
95 t.is(email.body, 'Decoded base64 content')
96})
bridge-envelope/index.js +11 -0
……@@ -0,0 +1,11 @@
1import { envelopeCodec } from './lib/codec.js'
2import { createEnvelope, TYPES, PLATFORMS } from './lib/envelope.js'
3
4export {
5 envelopeCodec,
6 createEnvelope,
7 TYPES,
8 PLATFORMS
9}
10
11export { encode, decode } from './lib/codec.js'
bridge-envelope/lib/codec.js +66 -0
……@@ -0,0 +1,66 @@
1import cenc from 'compact-encoding'
2
3// Compact-encoding codec for bridge envelopes
4// Fields: type(uint), platform(uint), author(string), content(string),
5// timestamp(uint), channel(string), replyTo(string),
6// mediaUrl(string), mediaType(string), editOf(string),
7// deleteOf(string), id(string)
8
9const envelopeCodec = {
10 preencode (state, envelope) {
11 cenc.uint.preencode(state, envelope.type)
12 cenc.uint.preencode(state, envelope.platform)
13 cenc.string.preencode(state, envelope.author)
14 cenc.string.preencode(state, envelope.content)
15 cenc.uint.preencode(state, envelope.timestamp)
16 cenc.string.preencode(state, envelope.channel)
17 cenc.string.preencode(state, envelope.replyTo || '')
18 cenc.string.preencode(state, envelope.mediaUrl || '')
19 cenc.string.preencode(state, envelope.mediaType || '')
20 cenc.string.preencode(state, envelope.editOf || '')
21 cenc.string.preencode(state, envelope.deleteOf || '')
22 cenc.string.preencode(state, envelope.id)
23 },
24
25 encode (state, envelope) {
26 cenc.uint.encode(state, envelope.type)
27 cenc.uint.encode(state, envelope.platform)
28 cenc.string.encode(state, envelope.author)
29 cenc.string.encode(state, envelope.content)
30 cenc.uint.encode(state, envelope.timestamp)
31 cenc.string.encode(state, envelope.channel)
32 cenc.string.encode(state, envelope.replyTo || '')
33 cenc.string.encode(state, envelope.mediaUrl || '')
34 cenc.string.encode(state, envelope.mediaType || '')
35 cenc.string.encode(state, envelope.editOf || '')
36 cenc.string.encode(state, envelope.deleteOf || '')
37 cenc.string.encode(state, envelope.id)
38 },
39
40 decode (state) {
41 return {
42 type: cenc.uint.decode(state),
43 platform: cenc.uint.decode(state),
44 author: cenc.string.decode(state),
45 content: cenc.string.decode(state),
46 timestamp: cenc.uint.decode(state),
47 channel: cenc.string.decode(state),
48 replyTo: cenc.string.decode(state),
49 mediaUrl: cenc.string.decode(state),
50 mediaType: cenc.string.decode(state),
51 editOf: cenc.string.decode(state),
52 deleteOf: cenc.string.decode(state),
53 id: cenc.string.decode(state)
54 }
55 }
56}
57
58function encode (envelope) {
59 return cenc.encode(envelopeCodec, envelope)
60}
61
62function decode (buf) {
63 return cenc.decode(envelopeCodec, buf)
64}
65
66export { envelopeCodec, encode, decode }
bridge-envelope/lib/envelope.js +79 -0
……@@ -0,0 +1,79 @@
1import b4a from 'b4a'
2import { createHash } from 'crypto'
3
4// Message types
5const TYPES = {
6 TEXT: 0,
7 MEDIA: 1,
8 REACTION: 2,
9 EDIT: 3,
10 DELETE: 4,
11 SYSTEM: 5,
12 REPLY: 6
13}
14
15// Platform identifiers
16const PLATFORMS = {
17 KEET: 0,
18 IRC: 1,
19 DISCORD: 2,
20 MATRIX: 3,
21 MASTODON: 4,
22 EMAIL: 5,
23 ANDROID: 6
24}
25
26// Reverse maps for display
27const TYPE_NAMES = Object.fromEntries(Object.entries(TYPES).map(([k, v]) => [v, k.toLowerCase()]))
28const PLATFORM_NAMES = Object.fromEntries(Object.entries(PLATFORMS).map(([k, v]) => [v, k.toLowerCase()]))
29
30function contentHash (envelope) {
31 const h = createHash('sha256')
32 h.update(String(envelope.platform))
33 h.update(envelope.author)
34 h.update(envelope.content)
35 h.update(String(envelope.timestamp))
36 return h.digest('hex').slice(0, 16)
37}
38
39function createEnvelope (opts) {
40 const type = opts.type ?? TYPES.TEXT
41 const platform = opts.platform ?? PLATFORMS.KEET
42 const author = opts.author || ''
43 const content = opts.content || ''
44 const timestamp = opts.timestamp || Date.now()
45 const channel = opts.channel || ''
46 const replyTo = opts.replyTo || ''
47 const mediaUrl = opts.mediaUrl || ''
48 const mediaType = opts.mediaType || ''
49 const editOf = opts.editOf || ''
50 const deleteOf = opts.deleteOf || ''
51
52 const envelope = {
53 type,
54 platform,
55 author,
56 content,
57 timestamp,
58 channel,
59 replyTo,
60 mediaUrl,
61 mediaType,
62 editOf,
63 deleteOf,
64 id: ''
65 }
66
67 envelope.id = opts.id || contentHash(envelope)
68
69 return envelope
70}
71
72export {
73 TYPES,
74 PLATFORMS,
75 TYPE_NAMES,
76 PLATFORM_NAMES,
77 createEnvelope,
78 contentHash
79}
bridge-envelope/package-lock.json +869 -0
……@@ -0,0 +1,869 @@
1{
2 "name": "bridge-envelope",
3 "version": "1.0.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "bridge-envelope",
9 "version": "1.0.0",
10 "license": "MIT",
11 "dependencies": {
12 "b4a": "^1.0.0",
13 "compact-encoding": "^2.0.0"
14 },
15 "devDependencies": {
16 "brittle": "^3.0.0"
17 }
18 },
19 "node_modules/@jridgewell/resolve-uri": {
20 "version": "3.1.2",
21 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
22 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
23 "dev": true,
24 "license": "MIT",
25 "engines": {
26 "node": ">=6.0.0"
27 }
28 },
29 "node_modules/@jridgewell/sourcemap-codec": {
30 "version": "1.5.5",
31 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
32 "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
33 "dev": true,
34 "license": "MIT"
35 },
36 "node_modules/@jridgewell/trace-mapping": {
37 "version": "0.3.31",
38 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
39 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
40 "dev": true,
41 "license": "MIT",
42 "dependencies": {
43 "@jridgewell/resolve-uri": "^3.1.0",
44 "@jridgewell/sourcemap-codec": "^1.4.14"
45 }
46 },
47 "node_modules/@types/istanbul-lib-coverage": {
48 "version": "2.0.6",
49 "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
50 "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
51 "dev": true,
52 "license": "MIT"
53 },
54 "node_modules/b4a": {
55 "version": "1.7.4",
56 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.4.tgz",
57 "integrity": "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug==",
58 "license": "Apache-2.0",
59 "peerDependencies": {
60 "react-native-b4a": "*"
61 },
62 "peerDependenciesMeta": {
63 "react-native-b4a": {
64 "optional": true
65 }
66 }
67 },
68 "node_modules/bare-addon-resolve": {
69 "version": "1.10.0",
70 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
71 "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
72 "dev": true,
73 "license": "Apache-2.0",
74 "dependencies": {
75 "bare-module-resolve": "^1.10.0",
76 "bare-semver": "^1.0.0"
77 },
78 "peerDependencies": {
79 "bare-url": "*"
80 },
81 "peerDependenciesMeta": {
82 "bare-url": {
83 "optional": true
84 }
85 }
86 },
87 "node_modules/bare-ansi-escapes": {
88 "version": "2.2.3",
89 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
90 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
91 "dev": true,
92 "license": "Apache-2.0",
93 "dependencies": {
94 "bare-stream": "^2.6.5"
95 },
96 "peerDependencies": {
97 "bare-buffer": "*"
98 },
99 "peerDependenciesMeta": {
100 "bare-buffer": {
101 "optional": true
102 }
103 }
104 },
105 "node_modules/bare-assert": {
106 "version": "1.2.0",
107 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
108 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
109 "dev": true,
110 "license": "Apache-2.0",
111 "dependencies": {
112 "bare-inspect": "^3.1.2"
113 }
114 },
115 "node_modules/bare-bundle": {
116 "version": "1.10.0",
117 "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
118 "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
119 "dev": true,
120 "license": "Apache-2.0",
121 "peerDependencies": {
122 "bare-buffer": "*",
123 "bare-url": "*"
124 },
125 "peerDependenciesMeta": {
126 "bare-buffer": {
127 "optional": true
128 },
129 "bare-url": {
130 "optional": true
131 }
132 }
133 },
134 "node_modules/bare-cov": {
135 "version": "1.2.1",
136 "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz",
137 "integrity": "sha512-SHNqb/WG9udUEj8m5qGSehoUQ9o0Xzv8nrf4HIdmG+KNU/XdhPLhTP5VPrHnSAMAXhAzEB4Q+2IYZ0x9qPzN1g==",
138 "dev": true,
139 "license": "Apache-2.0",
140 "dependencies": {
141 "bare-fs": "^4.1.2",
142 "bare-inspector": "^4.0.1",
143 "bare-path": "^3.0.0",
144 "bare-process": "^4.2.1",
145 "bare-url": "^2.1.5",
146 "bare-v8-to-istanbul": "^1.0.2",
147 "picomatch": "^4.0.2",
148 "which-runtime": "^1.2.1"
149 }
150 },
151 "node_modules/bare-crypto": {
152 "version": "1.13.0",
153 "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.0.tgz",
154 "integrity": "sha512-RQl13yD+YTACWUZHMMck0C6LNjXgGgRyhVC557ag0xIrkxXf4IQwpnc8WOB58f4k5kSzKhIjy05oW3HUBrFpSQ==",
155 "dev": true,
156 "license": "Apache-2.0",
157 "dependencies": {
158 "bare-stream": "^2.6.3"
159 },
160 "peerDependencies": {
161 "bare-buffer": "*"
162 },
163 "peerDependenciesMeta": {
164 "bare-buffer": {
165 "optional": true
166 }
167 }
168 },
169 "node_modules/bare-debug-log": {
170 "version": "2.0.0",
171 "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
172 "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
173 "dev": true,
174 "license": "Apache-2.0",
175 "dependencies": {
176 "bare-os": "^3.0.1"
177 }
178 },
179 "node_modules/bare-dns": {
180 "version": "2.1.4",
181 "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
182 "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==",
183 "dev": true,
184 "license": "Apache-2.0",
185 "engines": {
186 "bare": ">=1.7.0"
187 }
188 },
189 "node_modules/bare-encoding": {
190 "version": "1.0.3",
191 "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
192 "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
193 "dev": true,
194 "license": "Apache-2.0",
195 "peerDependencies": {
196 "bare-buffer": "*"
197 },
198 "peerDependenciesMeta": {
199 "bare-buffer": {
200 "optional": true
201 }
202 }
203 },
204 "node_modules/bare-env": {
205 "version": "3.0.0",
206 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
207 "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
208 "dev": true,
209 "license": "Apache-2.0",
210 "dependencies": {
211 "bare-os": "^3.0.1"
212 }
213 },
214 "node_modules/bare-events": {
215 "version": "2.8.2",
216 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
217 "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
218 "dev": true,
219 "license": "Apache-2.0",
220 "peerDependencies": {
221 "bare-abort-controller": "*"
222 },
223 "peerDependenciesMeta": {
224 "bare-abort-controller": {
225 "optional": true
226 }
227 }
228 },
229 "node_modules/bare-format": {
230 "version": "1.0.1",
231 "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.1.tgz",
232 "integrity": "sha512-1oS+LZrWK6tnYnvNSHDGljc2MPunRxwhpFriuCgzNF+oklrnwmBKD91tS0yt+jpl2j3UgcSDzBIMiVTvLs9A8w==",
233 "dev": true,
234 "license": "Apache-2.0",
235 "dependencies": {
236 "bare-inspect": "^3.0.0"
237 }
238 },
239 "node_modules/bare-fs": {
240 "version": "4.5.4",
241 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz",
242 "integrity": "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==",
243 "dev": true,
244 "license": "Apache-2.0",
245 "dependencies": {
246 "bare-events": "^2.5.4",
247 "bare-path": "^3.0.0",
248 "bare-stream": "^2.6.4",
249 "bare-url": "^2.2.2",
250 "fast-fifo": "^1.3.2"
251 },
252 "engines": {
253 "bare": ">=1.16.0"
254 },
255 "peerDependencies": {
256 "bare-buffer": "*"
257 },
258 "peerDependenciesMeta": {
259 "bare-buffer": {
260 "optional": true
261 }
262 }
263 },
264 "node_modules/bare-hrtime": {
265 "version": "2.1.1",
266 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
267 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
268 "dev": true,
269 "license": "Apache-2.0"
270 },
271 "node_modules/bare-http-parser": {
272 "version": "1.0.1",
273 "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.0.1.tgz",
274 "integrity": "sha512-A3LTDTcELcmNJ3g5liIaS038v/BQxOhA9cjhBESn7eoV7QCuMoIRBKLDadDe08flxyLbxI2f+1l2MZ/5+HnKPA==",
275 "dev": true,
276 "license": "Apache-2.0"
277 },
278 "node_modules/bare-http1": {
279 "version": "4.2.3",
280 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.2.3.tgz",
281 "integrity": "sha512-zyvHF3xzvBJNpuw0QydE9etTn2AyC/TRVDWTo8Ed3LQEEGaFOkv6KHhaJPlf4eWEeTekk41Fowie+fTsXoNxkA==",
282 "dev": true,
283 "license": "Apache-2.0",
284 "dependencies": {
285 "bare-events": "^2.6.0",
286 "bare-http-parser": "^1.0.0",
287 "bare-stream": "^2.3.0",
288 "bare-tcp": "^2.2.0"
289 },
290 "peerDependencies": {
291 "bare-buffer": "*",
292 "bare-url": "*"
293 },
294 "peerDependenciesMeta": {
295 "bare-buffer": {
296 "optional": true
297 },
298 "bare-url": {
299 "optional": true
300 }
301 }
302 },
303 "node_modules/bare-https": {
304 "version": "2.1.2",
305 "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.2.tgz",
306 "integrity": "sha512-Q+TTydUDsuKQJvh8dX2dvOXCR9fM3xR5TBmKaFrs5p7Lj7XbKX7v4vIUJ36H0SXg2xCOQxXKIbjwrLg5tfJNYg==",
307 "dev": true,
308 "license": "Apache-2.0",
309 "dependencies": {
310 "bare-http1": "^4.0.0",
311 "bare-tcp": "^2.2.0",
312 "bare-tls": "^2.0.0"
313 }
314 },
315 "node_modules/bare-inspect": {
316 "version": "3.1.4",
317 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
318 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
319 "dev": true,
320 "license": "Apache-2.0",
321 "dependencies": {
322 "bare-ansi-escapes": "^2.1.0",
323 "bare-type": "^1.0.0"
324 },
325 "engines": {
326 "bare": ">=1.18.0"
327 }
328 },
329 "node_modules/bare-inspector": {
330 "version": "4.0.3",
331 "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-4.0.3.tgz",
332 "integrity": "sha512-yYk6+7tTVKJKHOja5+QBn989ETv8oVsmwoH0nCM+QYC6w5MC03GC4aQNK+lipAZSz1EaAj42WZ8Wewjpy0L9TA==",
333 "dev": true,
334 "license": "Apache-2.0",
335 "dependencies": {
336 "bare-events": "^2.1.0",
337 "bare-http1": "^4.0.0",
338 "bare-stream": "^2.0.0",
339 "bare-url": "^2.0.0",
340 "bare-ws": "^2.0.0"
341 },
342 "engines": {
343 "bare": ">=1.2.0"
344 },
345 "peerDependencies": {
346 "bare-tcp": "*"
347 },
348 "peerDependenciesMeta": {
349 "bare-tcp": {
350 "optional": true
351 }
352 }
353 },
354 "node_modules/bare-module": {
355 "version": "6.1.3",
356 "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz",
357 "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==",
358 "dev": true,
359 "license": "Apache-2.0",
360 "dependencies": {
361 "bare-bundle": "^1.3.0",
362 "bare-module-lexer": "^1.0.0",
363 "bare-module-resolve": "^1.8.0",
364 "bare-path": "^3.0.0",
365 "bare-url": "^2.0.1"
366 },
367 "engines": {
368 "bare": ">=1.23.0"
369 },
370 "peerDependencies": {
371 "bare-buffer": "*"
372 },
373 "peerDependenciesMeta": {
374 "bare-buffer": {
375 "optional": true
376 }
377 }
378 },
379 "node_modules/bare-module-lexer": {
380 "version": "1.4.7",
381 "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
382 "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
383 "dev": true,
384 "license": "Apache-2.0",
385 "dependencies": {
386 "require-addon": "^1.0.2"
387 },
388 "peerDependencies": {
389 "bare-buffer": "*"
390 },
391 "peerDependenciesMeta": {
392 "bare-buffer": {
393 "optional": true
394 }
395 }
396 },
397 "node_modules/bare-module-resolve": {
398 "version": "1.12.1",
399 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz",
400 "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==",
401 "dev": true,
402 "license": "Apache-2.0",
403 "dependencies": {
404 "bare-semver": "^1.0.0"
405 },
406 "peerDependencies": {
407 "bare-url": "*"
408 },
409 "peerDependenciesMeta": {
410 "bare-url": {
411 "optional": true
412 }
413 }
414 },
415 "node_modules/bare-net": {
416 "version": "2.2.0",
417 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.2.0.tgz",
418 "integrity": "sha512-UF7cAbHsGE+H6uEqWF5IULBow1x58chZz4g3ALgHtv7wZsFcCbRDt0JKWEumf5Oma3QWS1Q6aLi0Rpll8RElMg==",
419 "dev": true,
420 "license": "Apache-2.0",
421 "dependencies": {
422 "bare-events": "^2.2.2",
423 "bare-pipe": "^4.0.0",
424 "bare-stream": "^2.0.0",
425 "bare-tcp": "^2.0.0"
426 }
427 },
428 "node_modules/bare-os": {
429 "version": "3.6.2",
430 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
431 "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
432 "dev": true,
433 "license": "Apache-2.0",
434 "engines": {
435 "bare": ">=1.14.0"
436 }
437 },
438 "node_modules/bare-path": {
439 "version": "3.0.0",
440 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
441 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
442 "dev": true,
443 "license": "Apache-2.0",
444 "dependencies": {
445 "bare-os": "^3.0.1"
446 }
447 },
448 "node_modules/bare-pipe": {
449 "version": "4.1.3",
450 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.3.tgz",
451 "integrity": "sha512-DqQsx93rAzre6yJ9T6l/Vgh+X+bntkVMB1X5ggtXjXtqtMmF2Y2RVlCzxxy/09R6yeR9FSWBEUIaMYJL1/5VDA==",
452 "dev": true,
453 "license": "Apache-2.0",
454 "dependencies": {
455 "bare-events": "^2.0.0",
456 "bare-stream": "^2.0.0"
457 },
458 "engines": {
459 "bare": ">=1.16.0"
460 }
461 },
462 "node_modules/bare-process": {
463 "version": "4.2.2",
464 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.2.2.tgz",
465 "integrity": "sha512-ikzEw+HGLB+2lS/WLVEsqi8BXBwzleG3n7DYI0v6/YNklvNabOIGlLd1dof+7Jw5ob4jsBRPtyflweVahY7rFA==",
466 "dev": true,
467 "license": "Apache-2.0",
468 "dependencies": {
469 "bare-env": "^3.0.0",
470 "bare-events": "^2.3.1",
471 "bare-hrtime": "^2.0.0",
472 "bare-os": "^3.5.0",
473 "bare-pipe": "^4.0.0",
474 "bare-signals": "^4.0.0",
475 "bare-tty": "^5.0.0"
476 }
477 },
478 "node_modules/bare-semver": {
479 "version": "1.0.2",
480 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz",
481 "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==",
482 "dev": true,
483 "license": "Apache-2.0"
484 },
485 "node_modules/bare-signals": {
486 "version": "4.2.0",
487 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz",
488 "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==",
489 "dev": true,
490 "license": "Apache-2.0",
491 "dependencies": {
492 "bare-events": "^2.5.3",
493 "bare-os": "^3.3.1"
494 },
495 "engines": {
496 "bare": ">=1.7.0"
497 }
498 },
499 "node_modules/bare-stream": {
500 "version": "2.7.0",
501 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
502 "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
503 "dev": true,
504 "license": "Apache-2.0",
505 "dependencies": {
506 "streamx": "^2.21.0"
507 },
508 "peerDependencies": {
509 "bare-buffer": "*",
510 "bare-events": "*"
511 },
512 "peerDependenciesMeta": {
513 "bare-buffer": {
514 "optional": true
515 },
516 "bare-events": {
517 "optional": true
518 }
519 }
520 },
521 "node_modules/bare-subprocess": {
522 "version": "5.2.2",
523 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.2.tgz",
524 "integrity": "sha512-L6oXgQ1aWs25RtG5Ky0bDD06p3RAcVVrDDMWb1DfXpHtyEWxamcyIWUbSykxMTWrpLlmSj6ytbb6yoKehGFfmw==",
525 "dev": true,
526 "license": "Apache-2.0",
527 "dependencies": {
528 "bare-env": "^3.0.0",
529 "bare-events": "^2.5.4",
530 "bare-os": "^3.0.1",
531 "bare-pipe": "^4.0.0",
532 "bare-url": "^2.2.2"
533 },
534 "engines": {
535 "bare": ">=1.7.0"
536 },
537 "peerDependencies": {
538 "bare-buffer": "*"
539 },
540 "peerDependenciesMeta": {
541 "bare-buffer": {
542 "optional": true
543 }
544 }
545 },
546 "node_modules/bare-tcp": {
547 "version": "2.2.4",
548 "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.4.tgz",
549 "integrity": "sha512-vGjfMkyhS1lxp+Y4+gXRDR1ijZMK2cKx0RBwtYKUuZbD90ARCoM6bAiTa1HBztumCIC+NXb8eXdMFXyuYwlPyw==",
550 "dev": true,
551 "license": "Apache-2.0",
552 "dependencies": {
553 "bare-dns": "^2.0.4",
554 "bare-events": "^2.5.4",
555 "bare-stream": "^2.6.4"
556 },
557 "engines": {
558 "bare": ">=1.16.0"
559 }
560 },
561 "node_modules/bare-tls": {
562 "version": "2.1.7",
563 "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.1.7.tgz",
564 "integrity": "sha512-h6wcNXQdBeTX7fed9tjPp0/9cA/QfcBTv3ItgjnbUk4rWAU8bEFalZCZnUDdCK/t9zrNfJ+yvcPx4D/1Y6biyA==",
565 "dev": true,
566 "license": "Apache-2.0",
567 "dependencies": {
568 "bare-net": "^2.0.1",
569 "bare-stream": "^2.6.4"
570 },
571 "engines": {
572 "bare": ">=1.7.0"
573 }
574 },
575 "node_modules/bare-tty": {
576 "version": "5.0.3",
577 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.0.3.tgz",
578 "integrity": "sha512-jW24RBWRZOMHuSEWC9mh8wUKO5WUNl0UWUTNPn3yZ8qkOqwa8vaV2dR3a+BvblONPk7V35wuvK9P8508+judAg==",
579 "dev": true,
580 "license": "Apache-2.0",
581 "dependencies": {
582 "bare-events": "^2.2.0",
583 "bare-signals": "^4.0.0",
584 "bare-stream": "^2.0.0"
585 },
586 "engines": {
587 "bare": ">=1.16.0"
588 }
589 },
590 "node_modules/bare-type": {
591 "version": "1.1.0",
592 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
593 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
594 "dev": true,
595 "license": "Apache-2.0",
596 "engines": {
597 "bare": ">=1.2.0"
598 }
599 },
600 "node_modules/bare-url": {
601 "version": "2.3.2",
602 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
603 "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
604 "dev": true,
605 "license": "Apache-2.0",
606 "peer": true,
607 "dependencies": {
608 "bare-path": "^3.0.0"
609 }
610 },
611 "node_modules/bare-utils": {
612 "version": "1.5.1",
613 "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.5.1.tgz",
614 "integrity": "sha512-mxCkFvmDU3mlD/mb+pT64kKXOsx2KMsWLQbngN1LB+NOXfhfnRnyvpy3VZc6m7gzQxe57Bsi+aTCBqA4/S3elQ==",
615 "dev": true,
616 "license": "Apache-2.0",
617 "dependencies": {
618 "bare-debug-log": "^2.0.0",
619 "bare-encoding": "^1.0.0",
620 "bare-format": "^1.0.0",
621 "bare-inspect": "^3.0.0",
622 "bare-type": "^1.0.6"
623 }
624 },
625 "node_modules/bare-v8-to-istanbul": {
626 "version": "1.0.2",
627 "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
628 "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==",
629 "dev": true,
630 "license": "Apache-2.0",
631 "dependencies": {
632 "bare-assert": "^1.0.2",
633 "bare-fs": "^4.1.2",
634 "bare-module": "^6.1.2",
635 "bare-path": "^3.0.0",
636 "bare-process": "^4.2.0",
637 "bare-url": "^2.1.5",
638 "bare-utils": "^1.2.0",
639 "v8-to-istanbul": "^9.3.0",
640 "which-runtime": "^1.2.1"
641 }
642 },
643 "node_modules/bare-ws": {
644 "version": "2.0.4",
645 "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.0.4.tgz",
646 "integrity": "sha512-SQMXzBYna9dRj57Dz1/ag+VWHCRXbfCjMHgyfM2F2lhkVLzMjnVSZP72aVeFWPFqe494Rd70Kzhe2JElGwFlJQ==",
647 "dev": true,
648 "license": "Apache-2.0",
649 "dependencies": {
650 "bare-crypto": "^1.2.0",
651 "bare-events": "^2.3.1",
652 "bare-http1": "^4.0.0",
653 "bare-https": "^2.0.0",
654 "bare-stream": "^2.1.2"
655 },
656 "peerDependencies": {
657 "bare-buffer": "*",
658 "bare-url": "*"
659 },
660 "peerDependenciesMeta": {
661 "bare-buffer": {
662 "optional": true
663 },
664 "bare-url": {
665 "optional": true
666 }
667 }
668 },
669 "node_modules/brittle": {
670 "version": "3.19.1",
671 "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz",
672 "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==",
673 "dev": true,
674 "license": "Apache-2.0",
675 "dependencies": {
676 "b4a": "^1.6.0",
677 "bare-assert": "^1.0.2",
678 "bare-cov": "^1.1.0",
679 "bare-fs": "^4.1.6",
680 "bare-os": "^3.6.1",
681 "bare-path": "^3.0.0",
682 "bare-process": "^4.2.1",
683 "bare-subprocess": "^5.0.0",
684 "bare-url": "^2.1.6",
685 "error-stack-parser": "^2.1.4",
686 "globbie": "^1.0.2",
687 "paparam": "^1.6.2",
688 "same-object": "^1.0.2",
689 "test-tmp": "^1.4.0",
690 "tmatch": "^5.0.0"
691 },
692 "bin": {
693 "brittle": "bin/node.js",
694 "brittle-bare": "bin/bare.js",
695 "brittle-node": "bin/node.js",
696 "brittle-pear": "bin/pear.js"
697 }
698 },
699 "node_modules/compact-encoding": {
700 "version": "2.18.0",
701 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-2.18.0.tgz",
702 "integrity": "sha512-goACAOlhMI2xo5jGOMUDfOLnGdRE1jGfyZ+zie8N5114nHrbPIqf6GLUtzbLof6DSyrERlYRm3EcBplte5LcQw==",
703 "license": "Apache-2.0",
704 "dependencies": {
705 "b4a": "^1.3.0"
706 }
707 },
708 "node_modules/convert-source-map": {
709 "version": "2.0.0",
710 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
711 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
712 "dev": true,
713 "license": "MIT"
714 },
715 "node_modules/error-stack-parser": {
716 "version": "2.1.4",
717 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
718 "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
719 "dev": true,
720 "license": "MIT",
721 "dependencies": {
722 "stackframe": "^1.3.4"
723 }
724 },
725 "node_modules/events-universal": {
726 "version": "1.0.1",
727 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
728 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
729 "dev": true,
730 "license": "Apache-2.0",
731 "dependencies": {
732 "bare-events": "^2.7.0"
733 }
734 },
735 "node_modules/fast-fifo": {
736 "version": "1.3.2",
737 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
738 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
739 "dev": true,
740 "license": "MIT"
741 },
742 "node_modules/globbie": {
743 "version": "1.0.3",
744 "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz",
745 "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==",
746 "dev": true,
747 "license": "Apache-2.0",
748 "dependencies": {
749 "bare-fs": "^4.1.2",
750 "bare-path": "^3.0.0",
751 "bare-process": "^4.2.1",
752 "picomatch": "^4.0.2"
753 }
754 },
755 "node_modules/paparam": {
756 "version": "1.10.0",
757 "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.0.tgz",
758 "integrity": "sha512-p36QQrwU3X6fj5d2JN20B4lcZI/O3fxMVUHc1xD7MNe5bcOf3jIgE0CIK5Zv/s5YAUGlobTJ/kKXrh1FJVrs3w==",
759 "dev": true,
760 "license": "Apache-2.0"
761 },
762 "node_modules/picomatch": {
763 "version": "4.0.3",
764 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
765 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
766 "dev": true,
767 "license": "MIT",
768 "engines": {
769 "node": ">=12"
770 },
771 "funding": {
772 "url": "https://github.com/sponsors/jonschlinkert"
773 }
774 },
775 "node_modules/require-addon": {
776 "version": "1.2.0",
777 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
778 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
779 "dev": true,
780 "license": "Apache-2.0",
781 "dependencies": {
782 "bare-addon-resolve": "^1.3.0"
783 },
784 "engines": {
785 "bare": ">=1.10.0"
786 }
787 },
788 "node_modules/same-object": {
789 "version": "1.0.2",
790 "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz",
791 "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==",
792 "dev": true,
793 "license": "MIT"
794 },
795 "node_modules/stackframe": {
796 "version": "1.3.4",
797 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
798 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
799 "dev": true,
800 "license": "MIT"
801 },
802 "node_modules/streamx": {
803 "version": "2.23.0",
804 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
805 "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
806 "dev": true,
807 "license": "MIT",
808 "dependencies": {
809 "events-universal": "^1.0.0",
810 "fast-fifo": "^1.3.2",
811 "text-decoder": "^1.1.0"
812 }
813 },
814 "node_modules/test-tmp": {
815 "version": "1.4.0",
816 "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz",
817 "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==",
818 "dev": true,
819 "license": "MIT",
820 "dependencies": {
821 "bare-fs": "^4.0.1",
822 "bare-os": "^3.3.0",
823 "bare-path": "^3.0.0"
824 }
825 },
826 "node_modules/text-decoder": {
827 "version": "1.2.6",
828 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.6.tgz",
829 "integrity": "sha512-27FeW5GQFDfw0FpwMQhMagB7BztOOlmjcSRi97t2oplhKVTZtp0DZbSegSaXS5IIC6mxMvBG4AR1Sgc6BX3CQg==",
830 "dev": true,
831 "license": "Apache-2.0",
832 "dependencies": {
833 "b4a": "^1.6.4"
834 }
835 },
836 "node_modules/tmatch": {
837 "version": "5.0.0",
838 "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz",
839 "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==",
840 "dev": true,
841 "license": "ISC",
842 "engines": {
843 "node": ">=8"
844 }
845 },
846 "node_modules/v8-to-istanbul": {
847 "version": "9.3.0",
848 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
849 "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
850 "dev": true,
851 "license": "ISC",
852 "dependencies": {
853 "@jridgewell/trace-mapping": "^0.3.12",
854 "@types/istanbul-lib-coverage": "^2.0.1",
855 "convert-source-map": "^2.0.0"
856 },
857 "engines": {
858 "node": ">=10.12.0"
859 }
860 },
861 "node_modules/which-runtime": {
862 "version": "1.3.2",
863 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
864 "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
865 "dev": true,
866 "license": "Apache-2.0"
867 }
868 }
869}
bridge-envelope/package.json +26 -0
……@@ -0,0 +1,26 @@
1{
2 "name": "bridge-envelope",
3 "version": "1.0.0",
4 "description": "Universal message schema + compact-encoding codec for P2P bridges",
5 "type": "module",
6 "main": "index.js",
7 "exports": {
8 ".": "./index.js",
9 "./codec": "./lib/codec.js"
10 },
11 "files": [
12 "index.js",
13 "lib/"
14 ],
15 "scripts": {
16 "test": "brittle test/*.test.js"
17 },
18 "dependencies": {
19 "compact-encoding": "^2.0.0",
20 "b4a": "^1.0.0"
21 },
22 "devDependencies": {
23 "brittle": "^3.0.0"
24 },
25 "license": "MIT"
26}
bridge-envelope/test/envelope.test.js +238 -0
……@@ -0,0 +1,238 @@
1import test from 'brittle'
2import { createEnvelope, TYPES, PLATFORMS, encode, decode, envelopeCodec } from '../index.js'
3import cenc from 'compact-encoding'
4
5test('createEnvelope - basic text message', (t) => {
6 const env = createEnvelope({
7 type: TYPES.TEXT,
8 platform: PLATFORMS.IRC,
9 author: 'testuser',
10 content: 'Hello from IRC!',
11 channel: '#test'
12 })
13
14 t.is(env.type, TYPES.TEXT)
15 t.is(env.platform, PLATFORMS.IRC)
16 t.is(env.author, 'testuser')
17 t.is(env.content, 'Hello from IRC!')
18 t.is(env.channel, '#test')
19 t.ok(env.id, 'has content-hash id')
20 t.ok(env.timestamp > 0, 'has timestamp')
21})
22
23test('createEnvelope - defaults', (t) => {
24 const env = createEnvelope({ content: 'test' })
25
26 t.is(env.type, TYPES.TEXT)
27 t.is(env.platform, PLATFORMS.KEET)
28 t.is(env.author, '')
29 t.is(env.replyTo, '')
30 t.is(env.mediaUrl, '')
31 t.is(env.editOf, '')
32 t.is(env.deleteOf, '')
33})
34
35test('createEnvelope - custom id', (t) => {
36 const env = createEnvelope({ content: 'test', id: 'custom-123' })
37 t.is(env.id, 'custom-123')
38})
39
40test('createEnvelope - content hash is deterministic', (t) => {
41 const ts = Date.now()
42 const a = createEnvelope({ platform: PLATFORMS.IRC, author: 'u', content: 'msg', timestamp: ts })
43 const b = createEnvelope({ platform: PLATFORMS.IRC, author: 'u', content: 'msg', timestamp: ts })
44 t.is(a.id, b.id)
45})
46
47test('createEnvelope - different content gives different hash', (t) => {
48 const ts = Date.now()
49 const a = createEnvelope({ author: 'u', content: 'msg1', timestamp: ts })
50 const b = createEnvelope({ author: 'u', content: 'msg2', timestamp: ts })
51 t.not(a.id, b.id)
52})
53
54test('createEnvelope - all message types', (t) => {
55 for (const [name, value] of Object.entries(TYPES)) {
56 const env = createEnvelope({ type: value, content: `${name} message` })
57 t.is(env.type, value, `type ${name} = ${value}`)
58 }
59})
60
61test('createEnvelope - all platforms', (t) => {
62 for (const [name, value] of Object.entries(PLATFORMS)) {
63 const env = createEnvelope({ platform: value, content: `from ${name}` })
64 t.is(env.platform, value, `platform ${name} = ${value}`)
65 }
66})
67
68test('createEnvelope - media message', (t) => {
69 const env = createEnvelope({
70 type: TYPES.MEDIA,
71 platform: PLATFORMS.DISCORD,
72 author: 'user#1234',
73 content: 'Check this out',
74 mediaUrl: 'https://cdn.discord.com/attachments/abc.png',
75 mediaType: 'image/png'
76 })
77
78 t.is(env.type, TYPES.MEDIA)
79 t.is(env.mediaUrl, 'https://cdn.discord.com/attachments/abc.png')
80 t.is(env.mediaType, 'image/png')
81})
82
83test('createEnvelope - reply message', (t) => {
84 const original = createEnvelope({ content: 'original' })
85 const reply = createEnvelope({
86 type: TYPES.REPLY,
87 content: 'reply to that',
88 replyTo: original.id
89 })
90
91 t.is(reply.type, TYPES.REPLY)
92 t.is(reply.replyTo, original.id)
93})
94
95test('createEnvelope - edit message', (t) => {
96 const original = createEnvelope({ content: 'typo' })
97 const edit = createEnvelope({
98 type: TYPES.EDIT,
99 content: 'fixed',
100 editOf: original.id
101 })
102
103 t.is(edit.type, TYPES.EDIT)
104 t.is(edit.editOf, original.id)
105})
106
107test('createEnvelope - delete message', (t) => {
108 const original = createEnvelope({ content: 'oops' })
109 const del = createEnvelope({
110 type: TYPES.DELETE,
111 deleteOf: original.id
112 })
113
114 t.is(del.type, TYPES.DELETE)
115 t.is(del.deleteOf, original.id)
116})
117
118test('codec - encode/decode roundtrip', (t) => {
119 const env = createEnvelope({
120 type: TYPES.TEXT,
121 platform: PLATFORMS.IRC,
122 author: 'testuser',
123 content: 'Hello world!',
124 channel: '#general',
125 timestamp: 1707000000000
126 })
127
128 const buf = encode(env)
129 t.ok(Buffer.isBuffer(buf), 'encoded to buffer')
130 t.ok(buf.length > 0, 'buffer has content')
131
132 const decoded = decode(buf)
133 t.alike(decoded, env, 'roundtrip preserves all fields')
134})
135
136test('codec - roundtrip all message types', (t) => {
137 for (const [name, type] of Object.entries(TYPES)) {
138 const env = createEnvelope({
139 type,
140 platform: PLATFORMS.KEET,
141 author: 'user',
142 content: `${name} content`,
143 channel: 'room1'
144 })
145
146 const decoded = decode(encode(env))
147 t.is(decoded.type, type, `roundtrip for ${name}`)
148 t.is(decoded.content, env.content)
149 }
150})
151
152test('codec - roundtrip with all fields populated', (t) => {
153 const env = createEnvelope({
154 type: TYPES.REPLY,
155 platform: PLATFORMS.DISCORD,
156 author: 'alice#1234',
157 content: 'replying with image',
158 channel: 'discord-channel-123',
159 replyTo: 'abc123',
160 mediaUrl: 'https://example.com/image.png',
161 mediaType: 'image/png',
162 editOf: 'def456',
163 deleteOf: 'ghi789',
164 id: 'custom-id-000',
165 timestamp: 1707123456789
166 })
167
168 const decoded = decode(encode(env))
169 t.alike(decoded, env, 'all fields preserved')
170})
171
172test('codec - empty strings handled correctly', (t) => {
173 const env = createEnvelope({ content: '' })
174 const decoded = decode(encode(env))
175 t.is(decoded.content, '')
176 t.is(decoded.replyTo, '')
177 t.is(decoded.mediaUrl, '')
178})
179
180test('codec - unicode content', (t) => {
181 const env = createEnvelope({
182 author: 'ユーザー',
183 content: '🎉 Hello 世界! مرحبا'
184 })
185
186 const decoded = decode(encode(env))
187 t.is(decoded.author, 'ユーザー')
188 t.is(decoded.content, '🎉 Hello 世界! مرحبا')
189})
190
191test('codec - large content', (t) => {
192 const content = 'x'.repeat(10000)
193 const env = createEnvelope({ content })
194 const decoded = decode(encode(env))
195 t.is(decoded.content, content)
196})
197
198test('codec - compact size is smaller than JSON', (t) => {
199 const env = createEnvelope({
200 type: TYPES.TEXT,
201 platform: PLATFORMS.IRC,
202 author: 'user',
203 content: 'Hello world',
204 channel: '#test'
205 })
206
207 const compactBuf = encode(env)
208 const jsonBuf = Buffer.from(JSON.stringify(env))
209
210 t.ok(compactBuf.length < jsonBuf.length, `compact (${compactBuf.length}) < JSON (${jsonBuf.length})`)
211})
212
213test('codec - works with cenc.encode/decode directly', (t) => {
214 const env = createEnvelope({ content: 'direct codec usage' })
215 const buf = cenc.encode(envelopeCodec, env)
216 const decoded = cenc.decode(envelopeCodec, buf)
217 t.alike(decoded, env)
218})
219
220test('TYPES constants', (t) => {
221 t.is(TYPES.TEXT, 0)
222 t.is(TYPES.MEDIA, 1)
223 t.is(TYPES.REACTION, 2)
224 t.is(TYPES.EDIT, 3)
225 t.is(TYPES.DELETE, 4)
226 t.is(TYPES.SYSTEM, 5)
227 t.is(TYPES.REPLY, 6)
228})
229
230test('PLATFORMS constants', (t) => {
231 t.is(PLATFORMS.KEET, 0)
232 t.is(PLATFORMS.IRC, 1)
233 t.is(PLATFORMS.DISCORD, 2)
234 t.is(PLATFORMS.MATRIX, 3)
235 t.is(PLATFORMS.MASTODON, 4)
236 t.is(PLATFORMS.EMAIL, 5)
237 t.is(PLATFORMS.ANDROID, 6)
238})
bridge-irc/index.js +73 -0
……@@ -0,0 +1,73 @@
1import { Bridge } from './lib/bridge.js'
2
3// CLI entry point
4const args = parseArgs(typeof Pear !== 'undefined' ? Pear.config.args : process.argv.slice(2))
5
6if (args.help) {
7 console.log(`
8bridge-irc — IRC ↔ Keet bidirectional bridge
9
10Usage:
11 bridge-irc --irc-server <host> --channel <#chan> --keet-room <roomId>
12
13Options:
14 --irc-server <host> IRC server hostname (required)
15 --irc-port <port> IRC port (default: 6667, 6697 for TLS)
16 --irc-nick <nick> IRC nickname (default: keet-bridge)
17 --irc-password <pass> IRC server password
18 --channel <#channel> IRC channel to bridge (required)
19 --tls Use TLS for IRC connection
20 --keet-room <roomId> Keet room ID to bridge (required)
21 --keet-port <port> Keet daemon port (default: 3117)
22 --help Show this help
23`)
24 process.exit(0)
25}
26
27if (!args['irc-server'] || !args.channel || !args['keet-room']) {
28 console.error('Error: --irc-server, --channel, and --keet-room are required')
29 console.error('Run with --help for usage')
30 process.exit(1)
31}
32
33const bridge = new Bridge({
34 ircServer: args['irc-server'],
35 ircPort: parseInt(args['irc-port'] || '6667'),
36 ircNick: args['irc-nick'] || 'keet-bridge',
37 ircPassword: args['irc-password'],
38 ircChannel: args.channel,
39 ircTls: args.tls || parseInt(args['irc-port'] || '0') === 6697,
40 keetRoom: args['keet-room'],
41 keetPort: parseInt(args['keet-port'] || '3117')
42})
43
44bridge.start().catch((err) => {
45 console.error('Bridge failed to start:', err.message)
46 process.exit(1)
47})
48
49// Graceful shutdown
50process.on('SIGINT', () => bridge.stop().then(() => process.exit(0)))
51process.on('SIGTERM', () => bridge.stop().then(() => process.exit(0)))
52
53function parseArgs (argv) {
54 const result = {}
55 for (let i = 0; i < argv.length; i++) {
56 const arg = argv[i]
57 if (arg === '--help') {
58 result.help = true
59 } else if (arg === '--tls') {
60 result.tls = true
61 } else if (arg.startsWith('--')) {
62 const key = arg.slice(2)
63 const next = argv[i + 1]
64 if (next && !next.startsWith('--')) {
65 result[key] = next
66 i++
67 } else {
68 result[key] = true
69 }
70 }
71 }
72 return result
73}
bridge-irc/lib/adapter.js +67 -0
……@@ -0,0 +1,67 @@
1import { createEnvelope, TYPES, PLATFORMS } from 'bridge-envelope'
2
3function ircToEnvelope (nick, channel, text) {
4 // Detect ACTION (/me)
5 const actionMatch = text.match(/^\x01ACTION (.+)\x01$/)
6 if (actionMatch) {
7 return createEnvelope({
8 type: TYPES.TEXT,
9 platform: PLATFORMS.IRC,
10 author: nick,
11 content: `* ${nick} ${actionMatch[1]}`,
12 channel
13 })
14 }
15
16 return createEnvelope({
17 type: TYPES.TEXT,
18 platform: PLATFORMS.IRC,
19 author: nick,
20 content: text,
21 channel
22 })
23}
24
25function keetToIrc (keetMsg, bridgeIdentity) {
26 // Skip messages from bridge identity (prevents echo)
27 if (keetMsg.memberId === bridgeIdentity) return null
28 // Skip system messages
29 if (keetMsg.system) return null
30
31 const name = keetMsg.name || 'anonymous'
32 const text = keetMsg.text || ''
33
34 if (!text.trim()) return null
35
36 return {
37 nick: name,
38 text: `<${name}> ${text}`
39 }
40}
41
42function envelopeToIrcText (envelope) {
43 // Format envelope for IRC display
44 const platform = platformName(envelope.platform)
45 const prefix = platform !== 'irc' ? `[${platform}] ` : ''
46 const author = envelope.author || 'anonymous'
47
48 switch (envelope.type) {
49 case TYPES.TEXT:
50 return `${prefix}<${author}> ${envelope.content}`
51 case TYPES.MEDIA:
52 return `${prefix}<${author}> ${envelope.content || ''} ${envelope.mediaUrl || '[media]'}`
53 case TYPES.REACTION:
54 return `${prefix}* ${author} reacted: ${envelope.content}`
55 case TYPES.SYSTEM:
56 return `${prefix}** ${envelope.content}`
57 default:
58 return `${prefix}<${author}> ${envelope.content}`
59 }
60}
61
62function platformName (id) {
63 const names = ['keet', 'irc', 'discord', 'matrix', 'mastodon', 'email', 'android']
64 return names[id] || 'unknown'
65}
66
67export { ircToEnvelope, keetToIrc, envelopeToIrcText }
bridge-irc/lib/bridge.js +181 -0
……@@ -0,0 +1,181 @@
1import { IrcClient } from './irc-client.js'
2import { KeetClient } from './keet-client.js'
3import { Router } from './router.js'
4import { ircToEnvelope, keetToIrc } from './adapter.js'
5
6class Bridge {
7 constructor (opts) {
8 this.ircServer = opts.ircServer
9 this.ircPort = opts.ircPort || 6667
10 this.ircNick = opts.ircNick || 'keet-bridge'
11 this.ircChannel = opts.ircChannel
12 this.ircPassword = opts.ircPassword || null
13 this.ircTls = opts.ircTls
14
15 this.keetRoom = opts.keetRoom
16 this.keetHost = opts.keetHost || 'localhost'
17 this.keetPort = opts.keetPort || 3117
18
19 this._irc = null
20 this._keet = null
21 this._router = null
22 this._running = false
23 this._bridgeIdentity = null
24 this._lastSeq = -1 // highest seq seen at startup — skip history
25 this._sendQueue = []
26 this._sending = false
27
28 this.onLog = opts.onLog || console.log
29 }
30
31 async start () {
32 this.onLog(`[bridge] Starting IRC ↔ Keet bridge`)
33 this.onLog(`[bridge] IRC: ${this.ircServer}:${this.ircPort} ${this.ircChannel}`)
34 this.onLog(`[bridge] Keet room: ${this.keetRoom}`)
35
36 // Initialize Keet client
37 this._keet = new KeetClient({
38 host: this.keetHost,
39 port: this.keetPort
40 })
41
42 // Get bridge identity so we can filter our own messages
43 try {
44 const identity = await this._keet.identity()
45 this._bridgeIdentity = identity.memberId || identity.id
46 this.onLog(`[bridge] Keet identity: ${identity.displayName || identity.username || 'unknown'}`)
47 } catch (err) {
48 this.onLog(`[bridge] Warning: could not get Keet identity: ${err.message}`)
49 }
50
51 // Snapshot the latest message seq to distinguish history from new messages
52 try {
53 const msgs = await this._keet.messages(this.keetRoom, 1)
54 if (msgs && msgs.length > 0) {
55 this._lastSeq = msgs[msgs.length - 1].seq || 0
56 this.onLog(`[bridge] History cutoff: seq ${this._lastSeq}`)
57 }
58 } catch (err) {
59 this.onLog(`[bridge] Warning: could not get message history: ${err.message}`)
60 }
61
62 // Initialize router with dedup
63 this._router = new Router({
64 onIrcMessage: (name, text) => {
65 this._queueSay(`<${name}> ${text}`)
66 },
67 onKeetMessage: (nick, text) => {
68 this._keet.send(this.keetRoom, `[IRC] <${nick}> ${text}`).catch((err) => {
69 this.onLog(`[bridge] Keet send error: ${err.message}`)
70 })
71 }
72 })
73
74 // Initialize IRC client
75 this._irc = new IrcClient({
76 server: this.ircServer,
77 port: this.ircPort,
78 nick: this.ircNick,
79 channel: this.ircChannel,
80 password: this.ircPassword,
81 tls: this.ircTls,
82 onMessage: (nick, text) => {
83 if (nick === this.ircNick) return // ignore self
84 this.onLog(`[irc→keet] <${nick}> ${text}`)
85 this._router.routeToKeet(nick, text)
86 },
87 onJoin: (nick) => {
88 if (nick === this.ircNick) return
89 this.onLog(`[irc] ${nick} joined ${this.ircChannel}`)
90 },
91 onPart: (nick) => {
92 this.onLog(`[irc] ${nick} left ${this.ircChannel}`)
93 }
94 })
95
96 // Connect to IRC
97 await this._irc.connect()
98 this.onLog(`[bridge] Connected to IRC as ${this.ircNick}`)
99
100 // Verify Keet daemon is reachable
101 try {
102 const status = await this._keet.status()
103 this.onLog(`[bridge] Keet daemon online (${status.rooms || '?'} rooms)`)
104 } catch (err) {
105 this.onLog(`[bridge] Warning: Keet daemon unreachable: ${err.message}`)
106 }
107
108 // Start polling Keet room for new messages
109 this._startKeetPoll()
110
111 this._running = true
112 this.onLog(`[bridge] Bridge active — messages flowing bidirectionally`)
113 }
114
115 _startKeetPoll () {
116 this._pollTimer = setInterval(async () => {
117 try {
118 const msgs = await this._keet.messages(this.keetRoom, 10)
119 if (!msgs || msgs.length === 0) return
120
121 for (const msg of msgs) {
122 if (msg.seq === undefined || msg.seq <= this._lastSeq) continue
123
124 this._lastSeq = msg.seq
125
126 const relayed = keetToIrc(msg, this._bridgeIdentity)
127 if (!relayed) continue
128
129 // Skip bridged messages (prevents echo)
130 if (msg.text && msg.text.startsWith('[IRC]')) continue
131
132 this.onLog(`[keet→irc] ${relayed.text}`)
133 this._router.routeToIrc(relayed.nick, msg.text)
134 }
135 } catch (err) {
136 this.onLog(`[bridge] Keet poll error: ${err.message}`)
137 }
138 }, 2000)
139 }
140
141 // Rate-limited IRC send — 1 message per second to avoid flood
142 _queueSay (text) {
143 this._sendQueue.push(text)
144 if (!this._sending) this._drainQueue()
145 }
146
147 _drainQueue () {
148 if (this._sendQueue.length === 0) {
149 this._sending = false
150 return
151 }
152 this._sending = true
153 const text = this._sendQueue.shift()
154 this._irc.say(text)
155 setTimeout(() => this._drainQueue(), 1000)
156 }
157
158 async stop () {
159 this.onLog(`[bridge] Shutting down...`)
160 this._running = false
161
162 if (this._pollTimer) clearInterval(this._pollTimer)
163 if (this._irc) this._irc.disconnect('Bridge stopped')
164
165 this.onLog(`[bridge] Stopped`)
166 }
167
168 get running () {
169 return this._running
170 }
171
172 get stats () {
173 return {
174 running: this._running,
175 ircConnected: this._irc?.connected || false,
176 deduped: this._router?.seenCount || 0
177 }
178 }
179}
180
181export { Bridge }
bridge-irc/lib/irc-client.js +102 -0
……@@ -0,0 +1,102 @@
1import IRC from 'irc-framework'
2
3class IrcClient {
4 constructor (opts) {
5 this.server = opts.server
6 this.port = opts.port || 6667
7 this.tls = opts.tls ?? (opts.port === 6697)
8 this.nick = opts.nick || 'keet-bridge'
9 this.channel = opts.channel
10 this.password = opts.password || null
11
12 this._client = null
13 this._connected = false
14 this._onMessage = opts.onMessage || null
15 this._onJoin = opts.onJoin || null
16 this._onPart = opts.onPart || null
17 this._onError = opts.onError || null
18 }
19
20 connect () {
21 return new Promise((resolve, reject) => {
22 this._client = new IRC.Client()
23
24 this._client.connect({
25 host: this.server,
26 port: this.port,
27 nick: this.nick,
28 tls: this.tls,
29 password: this.password
30 })
31
32 this._client.on('registered', () => {
33 this._connected = true
34 if (this.channel) {
35 this._client.join(this.channel)
36 }
37 resolve()
38 })
39
40 this._client.on('message', (event) => {
41 if (event.target === this.channel && this._onMessage) {
42 this._onMessage(event.nick, event.message)
43 }
44 })
45
46 this._client.on('action', (event) => {
47 if (event.target === this.channel && this._onMessage) {
48 this._onMessage(event.nick, `\x01ACTION ${event.message}\x01`)
49 }
50 })
51
52 this._client.on('join', (event) => {
53 if (event.channel === this.channel && this._onJoin) {
54 this._onJoin(event.nick)
55 }
56 })
57
58 this._client.on('part', (event) => {
59 if (event.channel === this.channel && this._onPart) {
60 this._onPart(event.nick, event.message)
61 }
62 })
63
64 this._client.on('close', () => {
65 this._connected = false
66 })
67
68 this._client.on('socket close', () => {
69 this._connected = false
70 })
71
72 const errorTimeout = setTimeout(() => {
73 if (!this._connected) reject(new Error('Connection timeout'))
74 }, 15000)
75
76 this._client.on('registered', () => clearTimeout(errorTimeout))
77 })
78 }
79
80 say (text) {
81 if (!this._connected || !this._client) return
82 this._client.say(this.channel, text)
83 }
84
85 action (text) {
86 if (!this._connected || !this._client) return
87 this._client.action(this.channel, text)
88 }
89
90 disconnect (reason) {
91 if (this._client) {
92 this._client.quit(reason || 'Bridge shutting down')
93 this._connected = false
94 }
95 }
96
97 get connected () {
98 return this._connected
99 }
100}
101
102export { IrcClient }
bridge-irc/lib/keet-client.js +166 -0
……@@ -0,0 +1,166 @@
1import http from 'http'
2import net from 'net'
3
4class KeetClient {
5 constructor (opts = {}) {
6 this.host = opts.host || 'localhost'
7 this.port = opts.port || 3117
8 this._watching = false
9 this._watchAbort = null
10 }
11
12 // Raw TCP request — bypasses Node's strict HTTP parser
13 // Needed because keet-daemon sends duplicate Content-Length headers
14 _rawRequest (method, path, body) {
15 return new Promise((resolve, reject) => {
16 const socket = net.connect(this.port, this.host)
17 let payload = ''
18
19 if (body) {
20 payload = JSON.stringify(body)
21 }
22
23 socket.on('connect', () => {
24 let req = `${method} ${path} HTTP/1.1\r\n`
25 req += `Host: ${this.host}:${this.port}\r\n`
26 if (payload) {
27 req += `Content-Type: application/json\r\n`
28 req += `Content-Length: ${Buffer.byteLength(payload)}\r\n`
29 }
30 req += `Connection: close\r\n`
31 req += `\r\n`
32 if (payload) req += payload
33 socket.write(req)
34 })
35
36 let data = ''
37 socket.on('data', (chunk) => { data += chunk.toString() })
38
39 socket.on('end', () => {
40 // Split headers from body
41 const idx = data.indexOf('\r\n\r\n')
42 if (idx === -1) return reject(new Error('No HTTP body'))
43 const jsonBody = data.slice(idx + 4)
44
45 try {
46 const parsed = JSON.parse(jsonBody)
47 if (!parsed.ok) reject(new Error(parsed.error || 'RPC error'))
48 else resolve(parsed.data)
49 } catch {
50 reject(new Error('Invalid JSON: ' + jsonBody.slice(0, 200)))
51 }
52 })
53
54 socket.on('error', reject)
55 socket.setTimeout(10000, () => {
56 socket.destroy(new Error('Request timeout'))
57 })
58 })
59 }
60
61 rpc (method, params = {}) {
62 return this._rawRequest('POST', '/rpc', { method, params })
63 }
64
65 async send (roomId, text) {
66 return this.rpc('send', { roomId, text })
67 }
68
69 async messages (roomId, limit = 20) {
70 return this._rawRequest('GET', `/rpc/messages/${roomId}?limit=${limit}`)
71 }
72
73 async rooms () {
74 return this._rawRequest('GET', '/rpc/rooms')
75 }
76
77 async identity () {
78 return this._rawRequest('GET', '/rpc/identity')
79 }
80
81 async status () {
82 return this._rawRequest('GET', '/rpc/status')
83 }
84
85 async members (roomId) {
86 return this._rawRequest('GET', `/rpc/members/${roomId}`)
87 }
88
89 watch (roomId, onMessage) {
90 if (this._watching) return
91
92 this._watching = true
93 this._doWatch(roomId, onMessage)
94 }
95
96 _doWatch (roomId, onMessage) {
97 if (!this._watching) return
98
99 const req = http.request({
100 hostname: this.host,
101 port: this.port,
102 path: `/rpc/watch/${roomId}`,
103 method: 'GET',
104 headers: { Accept: 'text/event-stream' }
105 }, (res) => {
106 let buffer = ''
107
108 res.on('data', (chunk) => {
109 buffer += chunk.toString()
110 const events = buffer.split('\n\n')
111 buffer = events.pop() // keep incomplete tail
112
113 for (const event of events) {
114 if (!event.trim()) continue
115 const lines = event.split('\n')
116 let type = 'message'
117 let data = null
118
119 for (const line of lines) {
120 if (line.startsWith('event: ')) type = line.slice(7)
121 if (line.startsWith('data: ')) {
122 try {
123 data = JSON.parse(line.slice(6))
124 } catch {}
125 }
126 }
127
128 if (type === 'message' && data) {
129 onMessage(data)
130 }
131 }
132 })
133
134 res.on('end', () => {
135 if (this._watching) {
136 setTimeout(() => this._doWatch(roomId, onMessage), 2000)
137 }
138 })
139
140 res.on('error', () => {
141 if (this._watching) {
142 setTimeout(() => this._doWatch(roomId, onMessage), 5000)
143 }
144 })
145 })
146
147 req.on('error', () => {
148 if (this._watching) {
149 setTimeout(() => this._doWatch(roomId, onMessage), 5000)
150 }
151 })
152
153 this._watchAbort = req
154 req.end()
155 }
156
157 stopWatch () {
158 this._watching = false
159 if (this._watchAbort) {
160 this._watchAbort.destroy()
161 this._watchAbort = null
162 }
163 }
164}
165
166export { KeetClient }
bridge-irc/lib/router.js +70 -0
……@@ -0,0 +1,70 @@
1import { createHash } from 'crypto'
2
3class Router {
4 constructor (opts = {}) {
5 this._seen = new Map()
6 this._maxSeen = opts.maxSeen || 10000
7 this._ttl = opts.ttl || 60000 // 1 minute dedup window
8 this._onIrcMessage = opts.onIrcMessage || null
9 this._onKeetMessage = opts.onKeetMessage || null
10 }
11
12 _hash (text, author) {
13 return createHash('sha256')
14 .update(author || '')
15 .update(text || '')
16 .digest('hex')
17 .slice(0, 16)
18 }
19
20 _isDuplicate (hash) {
21 const now = Date.now()
22
23 // Cleanup old entries
24 if (this._seen.size > this._maxSeen) {
25 for (const [k, ts] of this._seen) {
26 if (now - ts > this._ttl) this._seen.delete(k)
27 }
28 }
29
30 if (this._seen.has(hash)) {
31 const ts = this._seen.get(hash)
32 if (now - ts < this._ttl) return true
33 }
34
35 this._seen.set(hash, now)
36 return false
37 }
38
39 // Called when IRC receives a message — relay to Keet
40 routeToKeet (nick, text) {
41 const hash = this._hash(text, nick)
42 if (this._isDuplicate(hash)) return false
43
44 if (this._onKeetMessage) {
45 this._onKeetMessage(nick, text)
46 }
47 return true
48 }
49
50 // Called when Keet receives a message — relay to IRC
51 routeToIrc (name, text) {
52 const hash = this._hash(text, name)
53 if (this._isDuplicate(hash)) return false
54
55 if (this._onIrcMessage) {
56 this._onIrcMessage(name, text)
57 }
58 return true
59 }
60
61 get seenCount () {
62 return this._seen.size
63 }
64
65 clearSeen () {
66 this._seen.clear()
67 }
68}
69
70export { Router }
bridge-irc/package-lock.json +1553 -0
……@@ -0,0 +1,1553 @@
1{
2 "name": "bridge-irc",
3 "version": "1.0.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "bridge-irc",
9 "version": "1.0.0",
10 "license": "MIT",
11 "dependencies": {
12 "b4a": "^1.0.0",
13 "bridge-envelope": "file:../bridge-envelope",
14 "irc-framework": "^4.13.1"
15 },
16 "devDependencies": {
17 "brittle": "^3.0.0"
18 }
19 },
20 "../bridge-envelope": {
21 "version": "1.0.0",
22 "license": "MIT",
23 "dependencies": {
24 "b4a": "^1.0.0",
25 "compact-encoding": "^2.0.0"
26 },
27 "devDependencies": {
28 "brittle": "^3.0.0"
29 }
30 },
31 "node_modules/@jridgewell/resolve-uri": {
32 "version": "3.1.2",
33 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
34 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
35 "dev": true,
36 "license": "MIT",
37 "engines": {
38 "node": ">=6.0.0"
39 }
40 },
41 "node_modules/@jridgewell/sourcemap-codec": {
42 "version": "1.5.5",
43 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
44 "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
45 "dev": true,
46 "license": "MIT"
47 },
48 "node_modules/@jridgewell/trace-mapping": {
49 "version": "0.3.31",
50 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
51 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
52 "dev": true,
53 "license": "MIT",
54 "dependencies": {
55 "@jridgewell/resolve-uri": "^3.1.0",
56 "@jridgewell/sourcemap-codec": "^1.4.14"
57 }
58 },
59 "node_modules/@types/istanbul-lib-coverage": {
60 "version": "2.0.6",
61 "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
62 "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
63 "dev": true,
64 "license": "MIT"
65 },
66 "node_modules/available-typed-arrays": {
67 "version": "1.0.7",
68 "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
69 "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
70 "license": "MIT",
71 "dependencies": {
72 "possible-typed-array-names": "^1.0.0"
73 },
74 "engines": {
75 "node": ">= 0.4"
76 },
77 "funding": {
78 "url": "https://github.com/sponsors/ljharb"
79 }
80 },
81 "node_modules/b4a": {
82 "version": "1.7.4",
83 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.4.tgz",
84 "integrity": "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug==",
85 "license": "Apache-2.0",
86 "peerDependencies": {
87 "react-native-b4a": "*"
88 },
89 "peerDependenciesMeta": {
90 "react-native-b4a": {
91 "optional": true
92 }
93 }
94 },
95 "node_modules/bare-addon-resolve": {
96 "version": "1.10.0",
97 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
98 "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
99 "dev": true,
100 "license": "Apache-2.0",
101 "dependencies": {
102 "bare-module-resolve": "^1.10.0",
103 "bare-semver": "^1.0.0"
104 },
105 "peerDependencies": {
106 "bare-url": "*"
107 },
108 "peerDependenciesMeta": {
109 "bare-url": {
110 "optional": true
111 }
112 }
113 },
114 "node_modules/bare-ansi-escapes": {
115 "version": "2.2.3",
116 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
117 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
118 "dev": true,
119 "license": "Apache-2.0",
120 "dependencies": {
121 "bare-stream": "^2.6.5"
122 },
123 "peerDependencies": {
124 "bare-buffer": "*"
125 },
126 "peerDependenciesMeta": {
127 "bare-buffer": {
128 "optional": true
129 }
130 }
131 },
132 "node_modules/bare-assert": {
133 "version": "1.2.0",
134 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
135 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
136 "dev": true,
137 "license": "Apache-2.0",
138 "dependencies": {
139 "bare-inspect": "^3.1.2"
140 }
141 },
142 "node_modules/bare-bundle": {
143 "version": "1.10.0",
144 "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
145 "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
146 "dev": true,
147 "license": "Apache-2.0",
148 "peerDependencies": {
149 "bare-buffer": "*",
150 "bare-url": "*"
151 },
152 "peerDependenciesMeta": {
153 "bare-buffer": {
154 "optional": true
155 },
156 "bare-url": {
157 "optional": true
158 }
159 }
160 },
161 "node_modules/bare-cov": {
162 "version": "1.2.1",
163 "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz",
164 "integrity": "sha512-SHNqb/WG9udUEj8m5qGSehoUQ9o0Xzv8nrf4HIdmG+KNU/XdhPLhTP5VPrHnSAMAXhAzEB4Q+2IYZ0x9qPzN1g==",
165 "dev": true,
166 "license": "Apache-2.0",
167 "dependencies": {
168 "bare-fs": "^4.1.2",
169 "bare-inspector": "^4.0.1",
170 "bare-path": "^3.0.0",
171 "bare-process": "^4.2.1",
172 "bare-url": "^2.1.5",
173 "bare-v8-to-istanbul": "^1.0.2",
174 "picomatch": "^4.0.2",
175 "which-runtime": "^1.2.1"
176 }
177 },
178 "node_modules/bare-crypto": {
179 "version": "1.13.0",
180 "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.0.tgz",
181 "integrity": "sha512-RQl13yD+YTACWUZHMMck0C6LNjXgGgRyhVC557ag0xIrkxXf4IQwpnc8WOB58f4k5kSzKhIjy05oW3HUBrFpSQ==",
182 "dev": true,
183 "license": "Apache-2.0",
184 "dependencies": {
185 "bare-stream": "^2.6.3"
186 },
187 "peerDependencies": {
188 "bare-buffer": "*"
189 },
190 "peerDependenciesMeta": {
191 "bare-buffer": {
192 "optional": true
193 }
194 }
195 },
196 "node_modules/bare-debug-log": {
197 "version": "2.0.0",
198 "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
199 "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
200 "dev": true,
201 "license": "Apache-2.0",
202 "dependencies": {
203 "bare-os": "^3.0.1"
204 }
205 },
206 "node_modules/bare-dns": {
207 "version": "2.1.4",
208 "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
209 "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==",
210 "dev": true,
211 "license": "Apache-2.0",
212 "engines": {
213 "bare": ">=1.7.0"
214 }
215 },
216 "node_modules/bare-encoding": {
217 "version": "1.0.3",
218 "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
219 "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
220 "dev": true,
221 "license": "Apache-2.0",
222 "peerDependencies": {
223 "bare-buffer": "*"
224 },
225 "peerDependenciesMeta": {
226 "bare-buffer": {
227 "optional": true
228 }
229 }
230 },
231 "node_modules/bare-env": {
232 "version": "3.0.0",
233 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
234 "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
235 "dev": true,
236 "license": "Apache-2.0",
237 "dependencies": {
238 "bare-os": "^3.0.1"
239 }
240 },
241 "node_modules/bare-events": {
242 "version": "2.8.2",
243 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
244 "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
245 "dev": true,
246 "license": "Apache-2.0",
247 "peerDependencies": {
248 "bare-abort-controller": "*"
249 },
250 "peerDependenciesMeta": {
251 "bare-abort-controller": {
252 "optional": true
253 }
254 }
255 },
256 "node_modules/bare-format": {
257 "version": "1.0.1",
258 "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.1.tgz",
259 "integrity": "sha512-1oS+LZrWK6tnYnvNSHDGljc2MPunRxwhpFriuCgzNF+oklrnwmBKD91tS0yt+jpl2j3UgcSDzBIMiVTvLs9A8w==",
260 "dev": true,
261 "license": "Apache-2.0",
262 "dependencies": {
263 "bare-inspect": "^3.0.0"
264 }
265 },
266 "node_modules/bare-fs": {
267 "version": "4.5.4",
268 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz",
269 "integrity": "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==",
270 "dev": true,
271 "license": "Apache-2.0",
272 "dependencies": {
273 "bare-events": "^2.5.4",
274 "bare-path": "^3.0.0",
275 "bare-stream": "^2.6.4",
276 "bare-url": "^2.2.2",
277 "fast-fifo": "^1.3.2"
278 },
279 "engines": {
280 "bare": ">=1.16.0"
281 },
282 "peerDependencies": {
283 "bare-buffer": "*"
284 },
285 "peerDependenciesMeta": {
286 "bare-buffer": {
287 "optional": true
288 }
289 }
290 },
291 "node_modules/bare-hrtime": {
292 "version": "2.1.1",
293 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
294 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
295 "dev": true,
296 "license": "Apache-2.0"
297 },
298 "node_modules/bare-http-parser": {
299 "version": "1.0.1",
300 "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.0.1.tgz",
301 "integrity": "sha512-A3LTDTcELcmNJ3g5liIaS038v/BQxOhA9cjhBESn7eoV7QCuMoIRBKLDadDe08flxyLbxI2f+1l2MZ/5+HnKPA==",
302 "dev": true,
303 "license": "Apache-2.0"
304 },
305 "node_modules/bare-http1": {
306 "version": "4.2.3",
307 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.2.3.tgz",
308 "integrity": "sha512-zyvHF3xzvBJNpuw0QydE9etTn2AyC/TRVDWTo8Ed3LQEEGaFOkv6KHhaJPlf4eWEeTekk41Fowie+fTsXoNxkA==",
309 "dev": true,
310 "license": "Apache-2.0",
311 "dependencies": {
312 "bare-events": "^2.6.0",
313 "bare-http-parser": "^1.0.0",
314 "bare-stream": "^2.3.0",
315 "bare-tcp": "^2.2.0"
316 },
317 "peerDependencies": {
318 "bare-buffer": "*",
319 "bare-url": "*"
320 },
321 "peerDependenciesMeta": {
322 "bare-buffer": {
323 "optional": true
324 },
325 "bare-url": {
326 "optional": true
327 }
328 }
329 },
330 "node_modules/bare-https": {
331 "version": "2.1.2",
332 "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.2.tgz",
333 "integrity": "sha512-Q+TTydUDsuKQJvh8dX2dvOXCR9fM3xR5TBmKaFrs5p7Lj7XbKX7v4vIUJ36H0SXg2xCOQxXKIbjwrLg5tfJNYg==",
334 "dev": true,
335 "license": "Apache-2.0",
336 "dependencies": {
337 "bare-http1": "^4.0.0",
338 "bare-tcp": "^2.2.0",
339 "bare-tls": "^2.0.0"
340 }
341 },
342 "node_modules/bare-inspect": {
343 "version": "3.1.4",
344 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
345 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
346 "dev": true,
347 "license": "Apache-2.0",
348 "dependencies": {
349 "bare-ansi-escapes": "^2.1.0",
350 "bare-type": "^1.0.0"
351 },
352 "engines": {
353 "bare": ">=1.18.0"
354 }
355 },
356 "node_modules/bare-inspector": {
357 "version": "4.0.3",
358 "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-4.0.3.tgz",
359 "integrity": "sha512-yYk6+7tTVKJKHOja5+QBn989ETv8oVsmwoH0nCM+QYC6w5MC03GC4aQNK+lipAZSz1EaAj42WZ8Wewjpy0L9TA==",
360 "dev": true,
361 "license": "Apache-2.0",
362 "dependencies": {
363 "bare-events": "^2.1.0",
364 "bare-http1": "^4.0.0",
365 "bare-stream": "^2.0.0",
366 "bare-url": "^2.0.0",
367 "bare-ws": "^2.0.0"
368 },
369 "engines": {
370 "bare": ">=1.2.0"
371 },
372 "peerDependencies": {
373 "bare-tcp": "*"
374 },
375 "peerDependenciesMeta": {
376 "bare-tcp": {
377 "optional": true
378 }
379 }
380 },
381 "node_modules/bare-module": {
382 "version": "6.1.3",
383 "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz",
384 "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==",
385 "dev": true,
386 "license": "Apache-2.0",
387 "dependencies": {
388 "bare-bundle": "^1.3.0",
389 "bare-module-lexer": "^1.0.0",
390 "bare-module-resolve": "^1.8.0",
391 "bare-path": "^3.0.0",
392 "bare-url": "^2.0.1"
393 },
394 "engines": {
395 "bare": ">=1.23.0"
396 },
397 "peerDependencies": {
398 "bare-buffer": "*"
399 },
400 "peerDependenciesMeta": {
401 "bare-buffer": {
402 "optional": true
403 }
404 }
405 },
406 "node_modules/bare-module-lexer": {
407 "version": "1.4.7",
408 "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
409 "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
410 "dev": true,
411 "license": "Apache-2.0",
412 "dependencies": {
413 "require-addon": "^1.0.2"
414 },
415 "peerDependencies": {
416 "bare-buffer": "*"
417 },
418 "peerDependenciesMeta": {
419 "bare-buffer": {
420 "optional": true
421 }
422 }
423 },
424 "node_modules/bare-module-resolve": {
425 "version": "1.12.1",
426 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz",
427 "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==",
428 "dev": true,
429 "license": "Apache-2.0",
430 "dependencies": {
431 "bare-semver": "^1.0.0"
432 },
433 "peerDependencies": {
434 "bare-url": "*"
435 },
436 "peerDependenciesMeta": {
437 "bare-url": {
438 "optional": true
439 }
440 }
441 },
442 "node_modules/bare-net": {
443 "version": "2.2.0",
444 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.2.0.tgz",
445 "integrity": "sha512-UF7cAbHsGE+H6uEqWF5IULBow1x58chZz4g3ALgHtv7wZsFcCbRDt0JKWEumf5Oma3QWS1Q6aLi0Rpll8RElMg==",
446 "dev": true,
447 "license": "Apache-2.0",
448 "dependencies": {
449 "bare-events": "^2.2.2",
450 "bare-pipe": "^4.0.0",
451 "bare-stream": "^2.0.0",
452 "bare-tcp": "^2.0.0"
453 }
454 },
455 "node_modules/bare-os": {
456 "version": "3.6.2",
457 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
458 "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
459 "dev": true,
460 "license": "Apache-2.0",
461 "engines": {
462 "bare": ">=1.14.0"
463 }
464 },
465 "node_modules/bare-path": {
466 "version": "3.0.0",
467 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
468 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
469 "dev": true,
470 "license": "Apache-2.0",
471 "dependencies": {
472 "bare-os": "^3.0.1"
473 }
474 },
475 "node_modules/bare-pipe": {
476 "version": "4.1.3",
477 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.3.tgz",
478 "integrity": "sha512-DqQsx93rAzre6yJ9T6l/Vgh+X+bntkVMB1X5ggtXjXtqtMmF2Y2RVlCzxxy/09R6yeR9FSWBEUIaMYJL1/5VDA==",
479 "dev": true,
480 "license": "Apache-2.0",
481 "dependencies": {
482 "bare-events": "^2.0.0",
483 "bare-stream": "^2.0.0"
484 },
485 "engines": {
486 "bare": ">=1.16.0"
487 }
488 },
489 "node_modules/bare-process": {
490 "version": "4.2.2",
491 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.2.2.tgz",
492 "integrity": "sha512-ikzEw+HGLB+2lS/WLVEsqi8BXBwzleG3n7DYI0v6/YNklvNabOIGlLd1dof+7Jw5ob4jsBRPtyflweVahY7rFA==",
493 "dev": true,
494 "license": "Apache-2.0",
495 "dependencies": {
496 "bare-env": "^3.0.0",
497 "bare-events": "^2.3.1",
498 "bare-hrtime": "^2.0.0",
499 "bare-os": "^3.5.0",
500 "bare-pipe": "^4.0.0",
501 "bare-signals": "^4.0.0",
502 "bare-tty": "^5.0.0"
503 }
504 },
505 "node_modules/bare-semver": {
506 "version": "1.0.2",
507 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz",
508 "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==",
509 "dev": true,
510 "license": "Apache-2.0"
511 },
512 "node_modules/bare-signals": {
513 "version": "4.2.0",
514 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz",
515 "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==",
516 "dev": true,
517 "license": "Apache-2.0",
518 "dependencies": {
519 "bare-events": "^2.5.3",
520 "bare-os": "^3.3.1"
521 },
522 "engines": {
523 "bare": ">=1.7.0"
524 }
525 },
526 "node_modules/bare-stream": {
527 "version": "2.7.0",
528 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
529 "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
530 "dev": true,
531 "license": "Apache-2.0",
532 "dependencies": {
533 "streamx": "^2.21.0"
534 },
535 "peerDependencies": {
536 "bare-buffer": "*",
537 "bare-events": "*"
538 },
539 "peerDependenciesMeta": {
540 "bare-buffer": {
541 "optional": true
542 },
543 "bare-events": {
544 "optional": true
545 }
546 }
547 },
548 "node_modules/bare-subprocess": {
549 "version": "5.2.2",
550 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.2.tgz",
551 "integrity": "sha512-L6oXgQ1aWs25RtG5Ky0bDD06p3RAcVVrDDMWb1DfXpHtyEWxamcyIWUbSykxMTWrpLlmSj6ytbb6yoKehGFfmw==",
552 "dev": true,
553 "license": "Apache-2.0",
554 "dependencies": {
555 "bare-env": "^3.0.0",
556 "bare-events": "^2.5.4",
557 "bare-os": "^3.0.1",
558 "bare-pipe": "^4.0.0",
559 "bare-url": "^2.2.2"
560 },
561 "engines": {
562 "bare": ">=1.7.0"
563 },
564 "peerDependencies": {
565 "bare-buffer": "*"
566 },
567 "peerDependenciesMeta": {
568 "bare-buffer": {
569 "optional": true
570 }
571 }
572 },
573 "node_modules/bare-tcp": {
574 "version": "2.2.4",
575 "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.4.tgz",
576 "integrity": "sha512-vGjfMkyhS1lxp+Y4+gXRDR1ijZMK2cKx0RBwtYKUuZbD90ARCoM6bAiTa1HBztumCIC+NXb8eXdMFXyuYwlPyw==",
577 "dev": true,
578 "license": "Apache-2.0",
579 "dependencies": {
580 "bare-dns": "^2.0.4",
581 "bare-events": "^2.5.4",
582 "bare-stream": "^2.6.4"
583 },
584 "engines": {
585 "bare": ">=1.16.0"
586 }
587 },
588 "node_modules/bare-tls": {
589 "version": "2.1.7",
590 "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.1.7.tgz",
591 "integrity": "sha512-h6wcNXQdBeTX7fed9tjPp0/9cA/QfcBTv3ItgjnbUk4rWAU8bEFalZCZnUDdCK/t9zrNfJ+yvcPx4D/1Y6biyA==",
592 "dev": true,
593 "license": "Apache-2.0",
594 "dependencies": {
595 "bare-net": "^2.0.1",
596 "bare-stream": "^2.6.4"
597 },
598 "engines": {
599 "bare": ">=1.7.0"
600 }
601 },
602 "node_modules/bare-tty": {
603 "version": "5.0.3",
604 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.0.3.tgz",
605 "integrity": "sha512-jW24RBWRZOMHuSEWC9mh8wUKO5WUNl0UWUTNPn3yZ8qkOqwa8vaV2dR3a+BvblONPk7V35wuvK9P8508+judAg==",
606 "dev": true,
607 "license": "Apache-2.0",
608 "dependencies": {
609 "bare-events": "^2.2.0",
610 "bare-signals": "^4.0.0",
611 "bare-stream": "^2.0.0"
612 },
613 "engines": {
614 "bare": ">=1.16.0"
615 }
616 },
617 "node_modules/bare-type": {
618 "version": "1.1.0",
619 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
620 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
621 "dev": true,
622 "license": "Apache-2.0",
623 "engines": {
624 "bare": ">=1.2.0"
625 }
626 },
627 "node_modules/bare-url": {
628 "version": "2.3.2",
629 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
630 "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
631 "dev": true,
632 "license": "Apache-2.0",
633 "peer": true,
634 "dependencies": {
635 "bare-path": "^3.0.0"
636 }
637 },
638 "node_modules/bare-utils": {
639 "version": "1.5.1",
640 "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.5.1.tgz",
641 "integrity": "sha512-mxCkFvmDU3mlD/mb+pT64kKXOsx2KMsWLQbngN1LB+NOXfhfnRnyvpy3VZc6m7gzQxe57Bsi+aTCBqA4/S3elQ==",
642 "dev": true,
643 "license": "Apache-2.0",
644 "dependencies": {
645 "bare-debug-log": "^2.0.0",
646 "bare-encoding": "^1.0.0",
647 "bare-format": "^1.0.0",
648 "bare-inspect": "^3.0.0",
649 "bare-type": "^1.0.6"
650 }
651 },
652 "node_modules/bare-v8-to-istanbul": {
653 "version": "1.0.2",
654 "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
655 "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==",
656 "dev": true,
657 "license": "Apache-2.0",
658 "dependencies": {
659 "bare-assert": "^1.0.2",
660 "bare-fs": "^4.1.2",
661 "bare-module": "^6.1.2",
662 "bare-path": "^3.0.0",
663 "bare-process": "^4.2.0",
664 "bare-url": "^2.1.5",
665 "bare-utils": "^1.2.0",
666 "v8-to-istanbul": "^9.3.0",
667 "which-runtime": "^1.2.1"
668 }
669 },
670 "node_modules/bare-ws": {
671 "version": "2.0.4",
672 "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.0.4.tgz",
673 "integrity": "sha512-SQMXzBYna9dRj57Dz1/ag+VWHCRXbfCjMHgyfM2F2lhkVLzMjnVSZP72aVeFWPFqe494Rd70Kzhe2JElGwFlJQ==",
674 "dev": true,
675 "license": "Apache-2.0",
676 "dependencies": {
677 "bare-crypto": "^1.2.0",
678 "bare-events": "^2.3.1",
679 "bare-http1": "^4.0.0",
680 "bare-https": "^2.0.0",
681 "bare-stream": "^2.1.2"
682 },
683 "peerDependencies": {
684 "bare-buffer": "*",
685 "bare-url": "*"
686 },
687 "peerDependenciesMeta": {
688 "bare-buffer": {
689 "optional": true
690 },
691 "bare-url": {
692 "optional": true
693 }
694 }
695 },
696 "node_modules/base64-js": {
697 "version": "1.5.1",
698 "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
699 "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
700 "funding": [
701 {
702 "type": "github",
703 "url": "https://github.com/sponsors/feross"
704 },
705 {
706 "type": "patreon",
707 "url": "https://www.patreon.com/feross"
708 },
709 {
710 "type": "consulting",
711 "url": "https://feross.org/support"
712 }
713 ],
714 "license": "MIT"
715 },
716 "node_modules/bridge-envelope": {
717 "resolved": "../bridge-envelope",
718 "link": true
719 },
720 "node_modules/brittle": {
721 "version": "3.19.1",
722 "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz",
723 "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==",
724 "dev": true,
725 "license": "Apache-2.0",
726 "dependencies": {
727 "b4a": "^1.6.0",
728 "bare-assert": "^1.0.2",
729 "bare-cov": "^1.1.0",
730 "bare-fs": "^4.1.6",
731 "bare-os": "^3.6.1",
732 "bare-path": "^3.0.0",
733 "bare-process": "^4.2.1",
734 "bare-subprocess": "^5.0.0",
735 "bare-url": "^2.1.6",
736 "error-stack-parser": "^2.1.4",
737 "globbie": "^1.0.2",
738 "paparam": "^1.6.2",
739 "same-object": "^1.0.2",
740 "test-tmp": "^1.4.0",
741 "tmatch": "^5.0.0"
742 },
743 "bin": {
744 "brittle": "bin/node.js",
745 "brittle-bare": "bin/bare.js",
746 "brittle-node": "bin/node.js",
747 "brittle-pear": "bin/pear.js"
748 }
749 },
750 "node_modules/buffer": {
751 "version": "6.0.3",
752 "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
753 "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
754 "funding": [
755 {
756 "type": "github",
757 "url": "https://github.com/sponsors/feross"
758 },
759 {
760 "type": "patreon",
761 "url": "https://www.patreon.com/feross"
762 },
763 {
764 "type": "consulting",
765 "url": "https://feross.org/support"
766 }
767 ],
768 "license": "MIT",
769 "dependencies": {
770 "base64-js": "^1.3.1",
771 "ieee754": "^1.2.1"
772 }
773 },
774 "node_modules/call-bind": {
775 "version": "1.0.8",
776 "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
777 "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
778 "license": "MIT",
779 "dependencies": {
780 "call-bind-apply-helpers": "^1.0.0",
781 "es-define-property": "^1.0.0",
782 "get-intrinsic": "^1.2.4",
783 "set-function-length": "^1.2.2"
784 },
785 "engines": {
786 "node": ">= 0.4"
787 },
788 "funding": {
789 "url": "https://github.com/sponsors/ljharb"
790 }
791 },
792 "node_modules/call-bind-apply-helpers": {
793 "version": "1.0.2",
794 "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
795 "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
796 "license": "MIT",
797 "dependencies": {
798 "es-errors": "^1.3.0",
799 "function-bind": "^1.1.2"
800 },
801 "engines": {
802 "node": ">= 0.4"
803 }
804 },
805 "node_modules/call-bound": {
806 "version": "1.0.4",
807 "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
808 "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
809 "license": "MIT",
810 "dependencies": {
811 "call-bind-apply-helpers": "^1.0.2",
812 "get-intrinsic": "^1.3.0"
813 },
814 "engines": {
815 "node": ">= 0.4"
816 },
817 "funding": {
818 "url": "https://github.com/sponsors/ljharb"
819 }
820 },
821 "node_modules/convert-source-map": {
822 "version": "2.0.0",
823 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
824 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
825 "dev": true,
826 "license": "MIT"
827 },
828 "node_modules/core-js": {
829 "version": "3.48.0",
830 "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
831 "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
832 "hasInstallScript": true,
833 "license": "MIT",
834 "funding": {
835 "type": "opencollective",
836 "url": "https://opencollective.com/core-js"
837 }
838 },
839 "node_modules/define-data-property": {
840 "version": "1.1.4",
841 "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
842 "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
843 "license": "MIT",
844 "dependencies": {
845 "es-define-property": "^1.0.0",
846 "es-errors": "^1.3.0",
847 "gopd": "^1.0.1"
848 },
849 "engines": {
850 "node": ">= 0.4"
851 },
852 "funding": {
853 "url": "https://github.com/sponsors/ljharb"
854 }
855 },
856 "node_modules/dunder-proto": {
857 "version": "1.0.1",
858 "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
859 "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
860 "license": "MIT",
861 "dependencies": {
862 "call-bind-apply-helpers": "^1.0.1",
863 "es-errors": "^1.3.0",
864 "gopd": "^1.2.0"
865 },
866 "engines": {
867 "node": ">= 0.4"
868 }
869 },
870 "node_modules/error-stack-parser": {
871 "version": "2.1.4",
872 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
873 "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
874 "dev": true,
875 "license": "MIT",
876 "dependencies": {
877 "stackframe": "^1.3.4"
878 }
879 },
880 "node_modules/es-define-property": {
881 "version": "1.0.1",
882 "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
883 "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
884 "license": "MIT",
885 "engines": {
886 "node": ">= 0.4"
887 }
888 },
889 "node_modules/es-errors": {
890 "version": "1.3.0",
891 "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
892 "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
893 "license": "MIT",
894 "engines": {
895 "node": ">= 0.4"
896 }
897 },
898 "node_modules/es-object-atoms": {
899 "version": "1.1.1",
900 "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
901 "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
902 "license": "MIT",
903 "dependencies": {
904 "es-errors": "^1.3.0"
905 },
906 "engines": {
907 "node": ">= 0.4"
908 }
909 },
910 "node_modules/eventemitter3": {
911 "version": "5.0.4",
912 "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
913 "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
914 "license": "MIT"
915 },
916 "node_modules/events-universal": {
917 "version": "1.0.1",
918 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
919 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
920 "dev": true,
921 "license": "Apache-2.0",
922 "dependencies": {
923 "bare-events": "^2.7.0"
924 }
925 },
926 "node_modules/fast-fifo": {
927 "version": "1.3.2",
928 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
929 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
930 "dev": true,
931 "license": "MIT"
932 },
933 "node_modules/fast-text-encoding": {
934 "version": "1.0.6",
935 "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz",
936 "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==",
937 "license": "Apache-2.0"
938 },
939 "node_modules/for-each": {
940 "version": "0.3.5",
941 "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
942 "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
943 "license": "MIT",
944 "dependencies": {
945 "is-callable": "^1.2.7"
946 },
947 "engines": {
948 "node": ">= 0.4"
949 },
950 "funding": {
951 "url": "https://github.com/sponsors/ljharb"
952 }
953 },
954 "node_modules/function-bind": {
955 "version": "1.1.2",
956 "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
957 "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
958 "license": "MIT",
959 "funding": {
960 "url": "https://github.com/sponsors/ljharb"
961 }
962 },
963 "node_modules/generator-function": {
964 "version": "2.0.1",
965 "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
966 "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
967 "license": "MIT",
968 "engines": {
969 "node": ">= 0.4"
970 }
971 },
972 "node_modules/get-intrinsic": {
973 "version": "1.3.0",
974 "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
975 "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
976 "license": "MIT",
977 "dependencies": {
978 "call-bind-apply-helpers": "^1.0.2",
979 "es-define-property": "^1.0.1",
980 "es-errors": "^1.3.0",
981 "es-object-atoms": "^1.1.1",
982 "function-bind": "^1.1.2",
983 "get-proto": "^1.0.1",
984 "gopd": "^1.2.0",
985 "has-symbols": "^1.1.0",
986 "hasown": "^2.0.2",
987 "math-intrinsics": "^1.1.0"
988 },
989 "engines": {
990 "node": ">= 0.4"
991 },
992 "funding": {
993 "url": "https://github.com/sponsors/ljharb"
994 }
995 },
996 "node_modules/get-proto": {
997 "version": "1.0.1",
998 "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
999 "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
1000 "license": "MIT",
1001 "dependencies": {
1002 "dunder-proto": "^1.0.1",
1003 "es-object-atoms": "^1.0.0"
1004 },
1005 "engines": {
1006 "node": ">= 0.4"
1007 }
1008 },
1009 "node_modules/globbie": {
1010 "version": "1.0.3",
1011 "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz",
1012 "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==",
1013 "dev": true,
1014 "license": "Apache-2.0",
1015 "dependencies": {
1016 "bare-fs": "^4.1.2",
1017 "bare-path": "^3.0.0",
1018 "bare-process": "^4.2.1",
1019 "picomatch": "^4.0.2"
1020 }
1021 },
1022 "node_modules/gopd": {
1023 "version": "1.2.0",
1024 "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
1025 "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
1026 "license": "MIT",
1027 "engines": {
1028 "node": ">= 0.4"
1029 },
1030 "funding": {
1031 "url": "https://github.com/sponsors/ljharb"
1032 }
1033 },
1034 "node_modules/grapheme-splitter": {
1035 "version": "1.0.4",
1036 "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
1037 "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
1038 "license": "MIT"
1039 },
1040 "node_modules/has-property-descriptors": {
1041 "version": "1.0.2",
1042 "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
1043 "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
1044 "license": "MIT",
1045 "dependencies": {
1046 "es-define-property": "^1.0.0"
1047 },
1048 "funding": {
1049 "url": "https://github.com/sponsors/ljharb"
1050 }
1051 },
1052 "node_modules/has-symbols": {
1053 "version": "1.1.0",
1054 "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
1055 "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
1056 "license": "MIT",
1057 "engines": {
1058 "node": ">= 0.4"
1059 },
1060 "funding": {
1061 "url": "https://github.com/sponsors/ljharb"
1062 }
1063 },
1064 "node_modules/has-tostringtag": {
1065 "version": "1.0.2",
1066 "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
1067 "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
1068 "license": "MIT",
1069 "dependencies": {
1070 "has-symbols": "^1.0.3"
1071 },
1072 "engines": {
1073 "node": ">= 0.4"
1074 },
1075 "funding": {
1076 "url": "https://github.com/sponsors/ljharb"
1077 }
1078 },
1079 "node_modules/hasown": {
1080 "version": "2.0.2",
1081 "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
1082 "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
1083 "license": "MIT",
1084 "dependencies": {
1085 "function-bind": "^1.1.2"
1086 },
1087 "engines": {
1088 "node": ">= 0.4"
1089 }
1090 },
1091 "node_modules/iconv-lite": {
1092 "version": "0.6.3",
1093 "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
1094 "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
1095 "license": "MIT",
1096 "dependencies": {
1097 "safer-buffer": ">= 2.1.2 < 3.0.0"
1098 },
1099 "engines": {
1100 "node": ">=0.10.0"
1101 }
1102 },
1103 "node_modules/ieee754": {
1104 "version": "1.2.1",
1105 "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
1106 "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
1107 "funding": [
1108 {
1109 "type": "github",
1110 "url": "https://github.com/sponsors/feross"
1111 },
1112 {
1113 "type": "patreon",
1114 "url": "https://www.patreon.com/feross"
1115 },
1116 {
1117 "type": "consulting",
1118 "url": "https://feross.org/support"
1119 }
1120 ],
1121 "license": "BSD-3-Clause"
1122 },
1123 "node_modules/inherits": {
1124 "version": "2.0.4",
1125 "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
1126 "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
1127 "license": "ISC"
1128 },
1129 "node_modules/ip-address": {
1130 "version": "10.1.0",
1131 "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
1132 "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
1133 "license": "MIT",
1134 "engines": {
1135 "node": ">= 12"
1136 }
1137 },
1138 "node_modules/irc-framework": {
1139 "version": "4.14.0",
1140 "resolved": "https://registry.npmjs.org/irc-framework/-/irc-framework-4.14.0.tgz",
1141 "integrity": "sha512-lNujDAxy9kcu89WbU5H7IDWly64aD1B9nN9AV5M6btfx88qyQuyH16j1tjS40nmkQH6ld6vvaihKRn9cjk1JrA==",
1142 "license": "MIT",
1143 "dependencies": {
1144 "buffer": "^6.0.3",
1145 "core-js": "^3.38.1",
1146 "eventemitter3": "^5.0.1",
1147 "grapheme-splitter": "^1.0.4",
1148 "iconv-lite": "^0.6.3",
1149 "isomorphic-textencoder": "^1.0.1",
1150 "lodash": "^4.17.21",
1151 "middleware-handler": "^0.2.0",
1152 "regenerator-runtime": "^0.14.1",
1153 "socks": "^2.8.3",
1154 "stream-browserify": "^3.0.0",
1155 "util": "^0.12.5"
1156 }
1157 },
1158 "node_modules/is-arguments": {
1159 "version": "1.2.0",
1160 "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
1161 "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
1162 "license": "MIT",
1163 "dependencies": {
1164 "call-bound": "^1.0.2",
1165 "has-tostringtag": "^1.0.2"
1166 },
1167 "engines": {
1168 "node": ">= 0.4"
1169 },
1170 "funding": {
1171 "url": "https://github.com/sponsors/ljharb"
1172 }
1173 },
1174 "node_modules/is-callable": {
1175 "version": "1.2.7",
1176 "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
1177 "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
1178 "license": "MIT",
1179 "engines": {
1180 "node": ">= 0.4"
1181 },
1182 "funding": {
1183 "url": "https://github.com/sponsors/ljharb"
1184 }
1185 },
1186 "node_modules/is-generator-function": {
1187 "version": "1.1.2",
1188 "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
1189 "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
1190 "license": "MIT",
1191 "dependencies": {
1192 "call-bound": "^1.0.4",
1193 "generator-function": "^2.0.0",
1194 "get-proto": "^1.0.1",
1195 "has-tostringtag": "^1.0.2",
1196 "safe-regex-test": "^1.1.0"
1197 },
1198 "engines": {
1199 "node": ">= 0.4"
1200 },
1201 "funding": {
1202 "url": "https://github.com/sponsors/ljharb"
1203 }
1204 },
1205 "node_modules/is-regex": {
1206 "version": "1.2.1",
1207 "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
1208 "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
1209 "license": "MIT",
1210 "dependencies": {
1211 "call-bound": "^1.0.2",
1212 "gopd": "^1.2.0",
1213 "has-tostringtag": "^1.0.2",
1214 "hasown": "^2.0.2"
1215 },
1216 "engines": {
1217 "node": ">= 0.4"
1218 },
1219 "funding": {
1220 "url": "https://github.com/sponsors/ljharb"
1221 }
1222 },
1223 "node_modules/is-typed-array": {
1224 "version": "1.1.15",
1225 "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
1226 "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
1227 "license": "MIT",
1228 "dependencies": {
1229 "which-typed-array": "^1.1.16"
1230 },
1231 "engines": {
1232 "node": ">= 0.4"
1233 },
1234 "funding": {
1235 "url": "https://github.com/sponsors/ljharb"
1236 }
1237 },
1238 "node_modules/isomorphic-textencoder": {
1239 "version": "1.0.1",
1240 "resolved": "https://registry.npmjs.org/isomorphic-textencoder/-/isomorphic-textencoder-1.0.1.tgz",
1241 "integrity": "sha512-676hESgHullDdHDsj469hr+7t3i/neBKU9J7q1T4RHaWwLAsaQnywC0D1dIUId0YZ+JtVrShzuBk1soo0+GVcQ==",
1242 "license": "MIT",
1243 "dependencies": {
1244 "fast-text-encoding": "^1.0.0"
1245 }
1246 },
1247 "node_modules/lodash": {
1248 "version": "4.17.23",
1249 "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
1250 "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
1251 "license": "MIT"
1252 },
1253 "node_modules/math-intrinsics": {
1254 "version": "1.1.0",
1255 "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
1256 "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
1257 "license": "MIT",
1258 "engines": {
1259 "node": ">= 0.4"
1260 }
1261 },
1262 "node_modules/middleware-handler": {
1263 "version": "0.2.0",
1264 "resolved": "https://registry.npmjs.org/middleware-handler/-/middleware-handler-0.2.0.tgz",
1265 "integrity": "sha512-Qz4B0yWndSokapr3Kl7fpMRysS0DaBlOuATrExFuZbr+oXZ3rsAPufdLe8mUJXiG5A4aJGW6GfKS4PDfQwu7Mg=="
1266 },
1267 "node_modules/paparam": {
1268 "version": "1.10.0",
1269 "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.0.tgz",
1270 "integrity": "sha512-p36QQrwU3X6fj5d2JN20B4lcZI/O3fxMVUHc1xD7MNe5bcOf3jIgE0CIK5Zv/s5YAUGlobTJ/kKXrh1FJVrs3w==",
1271 "dev": true,
1272 "license": "Apache-2.0"
1273 },
1274 "node_modules/picomatch": {
1275 "version": "4.0.3",
1276 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
1277 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
1278 "dev": true,
1279 "license": "MIT",
1280 "engines": {
1281 "node": ">=12"
1282 },
1283 "funding": {
1284 "url": "https://github.com/sponsors/jonschlinkert"
1285 }
1286 },
1287 "node_modules/possible-typed-array-names": {
1288 "version": "1.1.0",
1289 "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
1290 "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
1291 "license": "MIT",
1292 "engines": {
1293 "node": ">= 0.4"
1294 }
1295 },
1296 "node_modules/readable-stream": {
1297 "version": "3.6.2",
1298 "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
1299 "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
1300 "license": "MIT",
1301 "dependencies": {
1302 "inherits": "^2.0.3",
1303 "string_decoder": "^1.1.1",
1304 "util-deprecate": "^1.0.1"
1305 },
1306 "engines": {
1307 "node": ">= 6"
1308 }
1309 },
1310 "node_modules/regenerator-runtime": {
1311 "version": "0.14.1",
1312 "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
1313 "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
1314 "license": "MIT"
1315 },
1316 "node_modules/require-addon": {
1317 "version": "1.2.0",
1318 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
1319 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
1320 "dev": true,
1321 "license": "Apache-2.0",
1322 "dependencies": {
1323 "bare-addon-resolve": "^1.3.0"
1324 },
1325 "engines": {
1326 "bare": ">=1.10.0"
1327 }
1328 },
1329 "node_modules/safe-buffer": {
1330 "version": "5.2.1",
1331 "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1332 "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1333 "funding": [
1334 {
1335 "type": "github",
1336 "url": "https://github.com/sponsors/feross"
1337 },
1338 {
1339 "type": "patreon",
1340 "url": "https://www.patreon.com/feross"
1341 },
1342 {
1343 "type": "consulting",
1344 "url": "https://feross.org/support"
1345 }
1346 ],
1347 "license": "MIT"
1348 },
1349 "node_modules/safe-regex-test": {
1350 "version": "1.1.0",
1351 "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
1352 "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
1353 "license": "MIT",
1354 "dependencies": {
1355 "call-bound": "^1.0.2",
1356 "es-errors": "^1.3.0",
1357 "is-regex": "^1.2.1"
1358 },
1359 "engines": {
1360 "node": ">= 0.4"
1361 },
1362 "funding": {
1363 "url": "https://github.com/sponsors/ljharb"
1364 }
1365 },
1366 "node_modules/safer-buffer": {
1367 "version": "2.1.2",
1368 "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1369 "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1370 "license": "MIT"
1371 },
1372 "node_modules/same-object": {
1373 "version": "1.0.2",
1374 "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz",
1375 "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==",
1376 "dev": true,
1377 "license": "MIT"
1378 },
1379 "node_modules/set-function-length": {
1380 "version": "1.2.2",
1381 "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
1382 "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
1383 "license": "MIT",
1384 "dependencies": {
1385 "define-data-property": "^1.1.4",
1386 "es-errors": "^1.3.0",
1387 "function-bind": "^1.1.2",
1388 "get-intrinsic": "^1.2.4",
1389 "gopd": "^1.0.1",
1390 "has-property-descriptors": "^1.0.2"
1391 },
1392 "engines": {
1393 "node": ">= 0.4"
1394 }
1395 },
1396 "node_modules/smart-buffer": {
1397 "version": "4.2.0",
1398 "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
1399 "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
1400 "license": "MIT",
1401 "engines": {
1402 "node": ">= 6.0.0",
1403 "npm": ">= 3.0.0"
1404 }
1405 },
1406 "node_modules/socks": {
1407 "version": "2.8.7",
1408 "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
1409 "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
1410 "license": "MIT",
1411 "dependencies": {
1412 "ip-address": "^10.0.1",
1413 "smart-buffer": "^4.2.0"
1414 },
1415 "engines": {
1416 "node": ">= 10.0.0",
1417 "npm": ">= 3.0.0"
1418 }
1419 },
1420 "node_modules/stackframe": {
1421 "version": "1.3.4",
1422 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
1423 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
1424 "dev": true,
1425 "license": "MIT"
1426 },
1427 "node_modules/stream-browserify": {
1428 "version": "3.0.0",
1429 "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz",
1430 "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==",
1431 "license": "MIT",
1432 "dependencies": {
1433 "inherits": "~2.0.4",
1434 "readable-stream": "^3.5.0"
1435 }
1436 },
1437 "node_modules/streamx": {
1438 "version": "2.23.0",
1439 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
1440 "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
1441 "dev": true,
1442 "license": "MIT",
1443 "dependencies": {
1444 "events-universal": "^1.0.0",
1445 "fast-fifo": "^1.3.2",
1446 "text-decoder": "^1.1.0"
1447 }
1448 },
1449 "node_modules/string_decoder": {
1450 "version": "1.3.0",
1451 "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
1452 "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
1453 "license": "MIT",
1454 "dependencies": {
1455 "safe-buffer": "~5.2.0"
1456 }
1457 },
1458 "node_modules/test-tmp": {
1459 "version": "1.4.0",
1460 "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz",
1461 "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==",
1462 "dev": true,
1463 "license": "MIT",
1464 "dependencies": {
1465 "bare-fs": "^4.0.1",
1466 "bare-os": "^3.3.0",
1467 "bare-path": "^3.0.0"
1468 }
1469 },
1470 "node_modules/text-decoder": {
1471 "version": "1.2.6",
1472 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.6.tgz",
1473 "integrity": "sha512-27FeW5GQFDfw0FpwMQhMagB7BztOOlmjcSRi97t2oplhKVTZtp0DZbSegSaXS5IIC6mxMvBG4AR1Sgc6BX3CQg==",
1474 "dev": true,
1475 "license": "Apache-2.0",
1476 "dependencies": {
1477 "b4a": "^1.6.4"
1478 }
1479 },
1480 "node_modules/tmatch": {
1481 "version": "5.0.0",
1482 "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz",
1483 "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==",
1484 "dev": true,
1485 "license": "ISC",
1486 "engines": {
1487 "node": ">=8"
1488 }
1489 },
1490 "node_modules/util": {
1491 "version": "0.12.5",
1492 "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
1493 "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
1494 "license": "MIT",
1495 "dependencies": {
1496 "inherits": "^2.0.3",
1497 "is-arguments": "^1.0.4",
1498 "is-generator-function": "^1.0.7",
1499 "is-typed-array": "^1.1.3",
1500 "which-typed-array": "^1.1.2"
1501 }
1502 },
1503 "node_modules/util-deprecate": {
1504 "version": "1.0.2",
1505 "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
1506 "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
1507 "license": "MIT"
1508 },
1509 "node_modules/v8-to-istanbul": {
1510 "version": "9.3.0",
1511 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
1512 "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
1513 "dev": true,
1514 "license": "ISC",
1515 "dependencies": {
1516 "@jridgewell/trace-mapping": "^0.3.12",
1517 "@types/istanbul-lib-coverage": "^2.0.1",
1518 "convert-source-map": "^2.0.0"
1519 },
1520 "engines": {
1521 "node": ">=10.12.0"
1522 }
1523 },
1524 "node_modules/which-runtime": {
1525 "version": "1.3.2",
1526 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
1527 "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
1528 "dev": true,
1529 "license": "Apache-2.0"
1530 },
1531 "node_modules/which-typed-array": {
1532 "version": "1.1.20",
1533 "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
1534 "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
1535 "license": "MIT",
1536 "dependencies": {
1537 "available-typed-arrays": "^1.0.7",
1538 "call-bind": "^1.0.8",
1539 "call-bound": "^1.0.4",
1540 "for-each": "^0.3.5",
1541 "get-proto": "^1.0.1",
1542 "gopd": "^1.2.0",
1543 "has-tostringtag": "^1.0.2"
1544 },
1545 "engines": {
1546 "node": ">= 0.4"
1547 },
1548 "funding": {
1549 "url": "https://github.com/sponsors/ljharb"
1550 }
1551 }
1552 }
1553}
bridge-irc/package.json +23 -0
……@@ -0,0 +1,23 @@
1{
2 "name": "bridge-irc",
3 "version": "1.0.0",
4 "description": "IRC ↔ Keet bidirectional bridge",
5 "type": "module",
6 "main": "index.js",
7 "pear": {
8 "name": "bridge-irc",
9 "type": "terminal"
10 },
11 "scripts": {
12 "test": "brittle test/*.test.js"
13 },
14 "dependencies": {
15 "irc-framework": "^4.13.1",
16 "bridge-envelope": "file:../bridge-envelope",
17 "b4a": "^1.0.0"
18 },
19 "devDependencies": {
20 "brittle": "^3.0.0"
21 },
22 "license": "MIT"
23}
bridge-irc/test/adapter.test.js +171 -0
……@@ -0,0 +1,171 @@
1import test from 'brittle'
2import { ircToEnvelope, keetToIrc, envelopeToIrcText } from '../lib/adapter.js'
3import { TYPES, PLATFORMS, createEnvelope } from 'bridge-envelope'
4
5test('ircToEnvelope - basic text message', (t) => {
6 const env = ircToEnvelope('alice', '#test', 'Hello world!')
7
8 t.is(env.type, TYPES.TEXT)
9 t.is(env.platform, PLATFORMS.IRC)
10 t.is(env.author, 'alice')
11 t.is(env.content, 'Hello world!')
12 t.is(env.channel, '#test')
13 t.ok(env.id, 'has id')
14})
15
16test('ircToEnvelope - ACTION message', (t) => {
17 const env = ircToEnvelope('bob', '#test', '\x01ACTION waves hello\x01')
18
19 t.is(env.type, TYPES.TEXT)
20 t.is(env.content, '* bob waves hello')
21})
22
23test('ircToEnvelope - empty message', (t) => {
24 const env = ircToEnvelope('user', '#test', '')
25 t.is(env.content, '')
26})
27
28test('ircToEnvelope - unicode message', (t) => {
29 const env = ircToEnvelope('ユーザー', '#日本語', '🎉 こんにちは')
30 t.is(env.author, 'ユーザー')
31 t.is(env.channel, '#日本語')
32 t.is(env.content, '🎉 こんにちは')
33})
34
35test('keetToIrc - basic message', (t) => {
36 const msg = {
37 memberId: 'member-123',
38 name: 'Alice',
39 text: 'Hello from Keet!',
40 timestamp: Date.now()
41 }
42
43 const result = keetToIrc(msg, 'bridge-identity')
44 t.ok(result)
45 t.is(result.nick, 'Alice')
46 t.ok(result.text.includes('Alice'))
47 t.ok(result.text.includes('Hello from Keet!'))
48})
49
50test('keetToIrc - skips own messages', (t) => {
51 const msg = {
52 memberId: 'bridge-identity',
53 name: 'Bridge',
54 text: 'echoed message'
55 }
56
57 const result = keetToIrc(msg, 'bridge-identity')
58 t.is(result, null, 'skips bridge identity messages')
59})
60
61test('keetToIrc - skips system messages', (t) => {
62 const msg = {
63 memberId: 'member-456',
64 name: 'System',
65 text: 'User joined',
66 system: { type: 'join' }
67 }
68
69 const result = keetToIrc(msg, 'bridge-identity')
70 t.is(result, null, 'skips system messages')
71})
72
73test('keetToIrc - skips empty text', (t) => {
74 const msg = {
75 memberId: 'member-789',
76 name: 'User',
77 text: ' '
78 }
79
80 const result = keetToIrc(msg, 'bridge-identity')
81 t.is(result, null)
82})
83
84test('keetToIrc - anonymous if no name', (t) => {
85 const msg = {
86 memberId: 'member-abc',
87 text: 'hello'
88 }
89
90 const result = keetToIrc(msg, 'bridge-identity')
91 t.is(result.nick, 'anonymous')
92})
93
94test('envelopeToIrcText - keet text message', (t) => {
95 const env = createEnvelope({
96 type: TYPES.TEXT,
97 platform: PLATFORMS.KEET,
98 author: 'Alice',
99 content: 'Hello!'
100 })
101
102 const text = envelopeToIrcText(env)
103 t.ok(text.includes('[keet]'))
104 t.ok(text.includes('Alice'))
105 t.ok(text.includes('Hello!'))
106})
107
108test('envelopeToIrcText - irc message has no prefix', (t) => {
109 const env = createEnvelope({
110 type: TYPES.TEXT,
111 platform: PLATFORMS.IRC,
112 author: 'bob',
113 content: 'Hi there'
114 })
115
116 const text = envelopeToIrcText(env)
117 t.absent(text.includes('[irc]'), 'no platform prefix for IRC messages')
118 t.ok(text.includes('bob'))
119 t.ok(text.includes('Hi there'))
120})
121
122test('envelopeToIrcText - discord message', (t) => {
123 const env = createEnvelope({
124 type: TYPES.TEXT,
125 platform: PLATFORMS.DISCORD,
126 author: 'user#1234',
127 content: 'From Discord'
128 })
129
130 const text = envelopeToIrcText(env)
131 t.ok(text.includes('[discord]'))
132 t.ok(text.includes('user#1234'))
133})
134
135test('envelopeToIrcText - media message', (t) => {
136 const env = createEnvelope({
137 type: TYPES.MEDIA,
138 platform: PLATFORMS.KEET,
139 author: 'Alice',
140 content: 'Check this',
141 mediaUrl: 'https://example.com/img.png'
142 })
143
144 const text = envelopeToIrcText(env)
145 t.ok(text.includes('https://example.com/img.png'))
146})
147
148test('envelopeToIrcText - reaction', (t) => {
149 const env = createEnvelope({
150 type: TYPES.REACTION,
151 platform: PLATFORMS.KEET,
152 author: 'Alice',
153 content: '👍'
154 })
155
156 const text = envelopeToIrcText(env)
157 t.ok(text.includes('reacted'))
158 t.ok(text.includes('👍'))
159})
160
161test('envelopeToIrcText - system message', (t) => {
162 const env = createEnvelope({
163 type: TYPES.SYSTEM,
164 platform: PLATFORMS.KEET,
165 content: 'User joined the room'
166 })
167
168 const text = envelopeToIrcText(env)
169 t.ok(text.includes('**'))
170 t.ok(text.includes('User joined the room'))
171})
bridge-irc/test/router.test.js +143 -0
……@@ -0,0 +1,143 @@
1import test from 'brittle'
2import { Router } from '../lib/router.js'
3
4test('Router - routes to Keet', (t) => {
5 let received = null
6
7 const router = new Router({
8 onKeetMessage: (nick, text) => {
9 received = { nick, text }
10 }
11 })
12
13 const ok = router.routeToKeet('alice', 'hello')
14 t.ok(ok)
15 t.is(received.nick, 'alice')
16 t.is(received.text, 'hello')
17})
18
19test('Router - routes to IRC', (t) => {
20 let received = null
21
22 const router = new Router({
23 onIrcMessage: (name, text) => {
24 received = { name, text }
25 }
26 })
27
28 const ok = router.routeToIrc('Bob', 'hello from keet')
29 t.ok(ok)
30 t.is(received.name, 'Bob')
31 t.is(received.text, 'hello from keet')
32})
33
34test('Router - dedup prevents echo', (t) => {
35 let keetCount = 0
36 let ircCount = 0
37
38 const router = new Router({
39 onKeetMessage: () => keetCount++,
40 onIrcMessage: () => ircCount++
41 })
42
43 // Message from IRC → Keet
44 t.ok(router.routeToKeet('alice', 'hello'))
45 t.is(keetCount, 1)
46
47 // Same message from Keet → IRC should be deduped
48 t.absent(router.routeToIrc('alice', 'hello'))
49 t.is(ircCount, 0, 'deduped — same content')
50})
51
52test('Router - different messages not deduped', (t) => {
53 let count = 0
54 const router = new Router({
55 onKeetMessage: () => count++
56 })
57
58 router.routeToKeet('alice', 'message 1')
59 router.routeToKeet('alice', 'message 2')
60 t.is(count, 2)
61})
62
63test('Router - same text different authors not deduped', (t) => {
64 let count = 0
65 const router = new Router({
66 onKeetMessage: () => count++
67 })
68
69 router.routeToKeet('alice', 'hello')
70 router.routeToKeet('bob', 'hello')
71 t.is(count, 2)
72})
73
74test('Router - seenCount tracks entries', (t) => {
75 const router = new Router()
76
77 t.is(router.seenCount, 0)
78 router.routeToKeet('a', 'msg1')
79 router.routeToKeet('b', 'msg2')
80 t.is(router.seenCount, 2)
81})
82
83test('Router - clearSeen resets dedup', (t) => {
84 let count = 0
85 const router = new Router({
86 onKeetMessage: () => count++
87 })
88
89 router.routeToKeet('alice', 'hello')
90 t.is(count, 1)
91
92 router.clearSeen()
93
94 router.routeToKeet('alice', 'hello')
95 t.is(count, 2, 'after clear, same message routes again')
96})
97
98test('Router - TTL expiry allows re-routing', async (t) => {
99 let count = 0
100 const router = new Router({
101 ttl: 50, // 50ms TTL for testing
102 onKeetMessage: () => count++
103 })
104
105 router.routeToKeet('alice', 'hello')
106 t.is(count, 1)
107
108 // Immediately should be deduped
109 router.routeToKeet('alice', 'hello')
110 t.is(count, 1)
111
112 // After TTL, should route again
113 await new Promise(r => setTimeout(r, 60))
114 router.routeToKeet('alice', 'hello')
115 t.is(count, 2, 'after TTL, same message routes again')
116})
117
118test('Router - no callback does not throw', (t) => {
119 const router = new Router()
120
121 const ok = router.routeToKeet('alice', 'hello')
122 t.ok(ok, 'returns true even without callback')
123})
124
125test('Router - handles empty strings', (t) => {
126 let count = 0
127 const router = new Router({
128 onKeetMessage: () => count++
129 })
130
131 router.routeToKeet('', '')
132 t.is(count, 1)
133})
134
135test('Router - handles unicode', (t) => {
136 let received = null
137 const router = new Router({
138 onKeetMessage: (nick, text) => { received = text }
139 })
140
141 router.routeToKeet('ユーザー', '🎉 こんにちは')
142 t.is(received, '🎉 こんにちは')
143})
bridge-mastodon/index.js +70 -0
……@@ -0,0 +1,70 @@
1import { MastodonBridge } from './lib/bridge.js'
2import { MastodonClient } from './lib/mastodon-client.js'
3import { KeetClient } from './lib/keet-client.js'
4import { Router } from './lib/router.js'
5import { statusToEnvelope, keetToMastodon, envelopeToStatusText, statusToKeetText, stripHtml } from './lib/adapter.js'
6
7export {
8 MastodonBridge,
9 MastodonClient,
10 KeetClient,
11 Router,
12 statusToEnvelope,
13 keetToMastodon,
14 envelopeToStatusText,
15 statusToKeetText,
16 stripHtml
17}
18
19// CLI entry point
20const args = process.argv.slice(2)
21if (args.includes('--help') || args.includes('-h')) {
22 console.log(`bridge-mastodon — Mastodon ↔ Keet bridge
23
24Usage:
25 bridge-mastodon --instance <host> --token <token> --keet-room <roomId> [options]
26
27Options:
28 --instance <host> Mastodon instance (e.g. mastodon.social)
29 --token <token> Mastodon access token (or MASTODON_TOKEN env)
30 --keet-room <roomId> Keet room ID to bridge
31 --keet-port <port> Keet daemon port (default: 3117)
32 --visibility <vis> Post visibility: public, unlisted, private, direct (default: unlisted)
33 --tag <hashtag> Filter: only bridge posts with this hashtag
34 --help Show this help`)
35 process.exit(0)
36}
37
38function getArg (flag) {
39 const idx = args.indexOf(flag)
40 return idx !== -1 ? args[idx + 1] : null
41}
42
43const instance = getArg('--instance')
44const token = getArg('--token') || process.env.MASTODON_TOKEN
45const roomId = getArg('--keet-room')
46const keetPort = parseInt(getArg('--keet-port') || '3117', 10)
47const visibility = getArg('--visibility') || 'unlisted'
48const tag = getArg('--tag')
49
50if (instance && token && roomId) {
51 const bridge = new MastodonBridge({
52 instance,
53 token,
54 roomId,
55 keetPort,
56 visibility,
57 tag,
58 onLog: (msg) => console.log(`[mastodon-bridge] ${msg}`)
59 })
60
61 bridge.start().catch(err => {
62 console.error('Bridge failed to start:', err.message)
63 process.exit(1)
64 })
65
66 process.on('SIGINT', () => {
67 bridge.stop()
68 process.exit(0)
69 })
70}
bridge-mastodon/lib/adapter.js +141 -0
……@@ -0,0 +1,141 @@
1import { createEnvelope, TYPES, PLATFORMS } from 'bridge-envelope'
2
3// Strip HTML tags from Mastodon status content
4function stripHtml (html) {
5 return html
6 .replace(/<br\s*\/?>/gi, '\n')
7 .replace(/<\/p>\s*<p>/gi, '\n\n')
8 .replace(/<[^>]+>/g, '')
9 .replace(/&amp;/g, '&')
10 .replace(/&lt;/g, '<')
11 .replace(/&gt;/g, '>')
12 .replace(/&quot;/g, '"')
13 .replace(/&#39;/g, "'")
14 .trim()
15}
16
17// Convert Mastodon status to bridge envelope
18function statusToEnvelope (status) {
19 if (!status) return null
20
21 // Skip boosts — handle separately if needed
22 const actual = status.reblog || status
23 const author = actual.account?.acct || actual.account?.username || 'unknown'
24 const content = stripHtml(actual.content || '')
25
26 if (!content && !actual.media_attachments?.length) return null
27
28 let type = TYPES.TEXT
29 let mediaUrl = ''
30 let mediaType = ''
31 let replyTo = ''
32
33 // Media attachments
34 const attachment = actual.media_attachments?.[0]
35 if (attachment) {
36 type = TYPES.MEDIA
37 mediaUrl = attachment.url || attachment.preview_url || ''
38 mediaType = attachment.type || '' // image, video, gifv, audio
39 }
40
41 // Reply
42 if (actual.in_reply_to_id) {
43 replyTo = actual.in_reply_to_id
44 if (type === TYPES.TEXT) type = TYPES.REPLY
45 }
46
47 const envelope = createEnvelope({
48 type,
49 platform: PLATFORMS.MASTODON,
50 author,
51 content,
52 channel: status.reblog ? 'boost' : (actual.visibility || 'public'),
53 replyTo,
54 mediaUrl,
55 mediaType
56 })
57
58 // Preserve Mastodon status ID for dedup and threading
59 envelope._statusId = actual.id
60 envelope._spoilerText = actual.spoiler_text || ''
61 envelope._tags = (actual.tags || []).map(t => t.name)
62 envelope._isBoost = !!status.reblog
63 envelope._boostBy = status.reblog ? (status.account?.acct || '') : ''
64
65 return envelope
66}
67
68// Convert Keet message to Mastodon post text
69function keetToMastodon (keetMsg, bridgeIdentity) {
70 if (!keetMsg) return null
71
72 // Skip own messages (bridge identity)
73 if (keetMsg.memberId === bridgeIdentity) return null
74
75 // Skip system messages
76 if (keetMsg.type === 'system') return null
77
78 const text = keetMsg.text || ''
79 if (!text.trim()) return null
80
81 // Skip messages already bridged from Mastodon
82 if (text.includes('[mastodon]') || text.includes('[Mastodon]')) return null
83
84 const name = keetMsg.name || keetMsg.username || 'someone'
85 return { author: name, text }
86}
87
88// Format envelope for Mastodon posting (from Keet to Mastodon)
89function envelopeToStatusText (envelope) {
90 if (!envelope) return ''
91
92 const platformName = {
93 [PLATFORMS.KEET]: 'Keet',
94 [PLATFORMS.IRC]: 'IRC',
95 [PLATFORMS.DISCORD]: 'Discord',
96 [PLATFORMS.MATRIX]: 'Matrix',
97 [PLATFORMS.EMAIL]: 'Email'
98 }
99
100 const source = platformName[envelope.platform] || 'P2P'
101 return `[${source}] ${envelope.author}: ${envelope.content}`
102}
103
104// Format a Mastodon status for display in Keet
105function statusToKeetText (envelope) {
106 if (!envelope) return ''
107
108 let text = ''
109 if (envelope._isBoost) {
110 text += `[mastodon] ${envelope._boostBy} boosted @${envelope.author}: `
111 } else {
112 text += `[mastodon] @${envelope.author}: `
113 }
114
115 if (envelope._spoilerText) {
116 text += `[CW: ${envelope._spoilerText}] `
117 }
118
119 text += envelope.content
120
121 if (envelope._tags?.length) {
122 const tags = envelope._tags.map(t => `#${t}`).join(' ')
123 if (!envelope.content.includes('#')) {
124 text += ` ${tags}`
125 }
126 }
127
128 if (envelope.mediaUrl) {
129 text += ` [${envelope.mediaType || 'media'}: ${envelope.mediaUrl}]`
130 }
131
132 return text
133}
134
135export {
136 statusToEnvelope,
137 keetToMastodon,
138 envelopeToStatusText,
139 statusToKeetText,
140 stripHtml
141}
bridge-mastodon/lib/bridge.js +86 -0
……@@ -0,0 +1,86 @@
1import { MastodonClient } from './mastodon-client.js'
2import { KeetClient } from './keet-client.js'
3import { Router } from './router.js'
4import { statusToEnvelope, keetToMastodon, statusToKeetText, envelopeToStatusText } from './adapter.js'
5
6class MastodonBridge {
7 constructor (opts = {}) {
8 this.mastodon = new MastodonClient({
9 instance: opts.instance,
10 token: opts.token
11 })
12 this.keet = new KeetClient({
13 host: opts.keetHost,
14 port: opts.keetPort
15 })
16 this.roomId = opts.roomId
17 this.visibility = opts.visibility || 'unlisted' // default post visibility
18 this.bridgeIdentity = opts.bridgeIdentity || null
19 this.tag = opts.tag || null // optional hashtag filter
20 this.onLog = opts.onLog || (() => {})
21
22 this.router = new Router({
23 onKeetMessage: (author, text) => {
24 this.keet.send(this.roomId, text).catch(err => {
25 this.onLog(`Keet send error: ${err.message}`)
26 })
27 },
28 onExternalMessage: (author, text) => {
29 this.mastodon.postStatus(text, { visibility: this.visibility }).catch(err => {
30 this.onLog(`Mastodon post error: ${err.message}`)
31 })
32 }
33 })
34 }
35
36 async start () {
37 // Get bridge identity from keet-daemon
38 if (!this.bridgeIdentity) {
39 try {
40 const identity = await this.keet.identity()
41 this.bridgeIdentity = identity.memberId || identity.id || ''
42 this.onLog(`Bridge identity: ${this.bridgeIdentity}`)
43 } catch (err) {
44 this.onLog(`Warning: Could not get identity: ${err.message}`)
45 }
46 }
47
48 // Verify Mastodon credentials
49 try {
50 const account = await this.mastodon.verifyCredentials()
51 this.onLog(`Mastodon: logged in as @${account.acct || account.username}`)
52 } catch (err) {
53 this.onLog(`Mastodon auth failed: ${err.message}`)
54 throw err
55 }
56
57 // Stream Mastodon → Keet
58 const streamType = this.tag ? `hashtag&tag=${this.tag}` : 'user'
59 this.mastodon.stream(streamType, (status) => {
60 const envelope = statusToEnvelope(status)
61 if (!envelope) return
62
63 const text = statusToKeetText(envelope)
64 this.router.routeToKeet(envelope.author, text)
65 })
66
67 // Watch Keet → Mastodon
68 this.keet.watch(this.roomId, (msg) => {
69 const result = keetToMastodon(msg, this.bridgeIdentity)
70 if (!result) return
71
72 const text = `${result.author}: ${result.text}`
73 this.router.routeToExternal(result.author, text)
74 })
75
76 this.onLog(`Bridge started: Mastodon (${this.mastodon.instance}) ↔ Keet`)
77 }
78
79 stop () {
80 this.mastodon.stopStream()
81 this.keet.stopWatch()
82 this.onLog('Bridge stopped')
83 }
84}
85
86export { MastodonBridge }
bridge-mastodon/lib/keet-client.js +162 -0
……@@ -0,0 +1,162 @@
1import http from 'http'
2import net from 'net'
3
4class KeetClient {
5 constructor (opts = {}) {
6 this.host = opts.host || 'localhost'
7 this.port = opts.port || 3117
8 this._watching = false
9 this._watchAbort = null
10 }
11
12 // Raw TCP request — bypasses Node's strict HTTP parser
13 // Needed because keet-daemon sends duplicate Content-Length headers
14 _rawRequest (method, path, body) {
15 return new Promise((resolve, reject) => {
16 const socket = net.connect(this.port, this.host)
17 let payload = ''
18
19 if (body) {
20 payload = JSON.stringify(body)
21 }
22
23 socket.on('connect', () => {
24 let req = `${method} ${path} HTTP/1.1\r\n`
25 req += `Host: ${this.host}:${this.port}\r\n`
26 if (payload) {
27 req += `Content-Type: application/json\r\n`
28 req += `Content-Length: ${Buffer.byteLength(payload)}\r\n`
29 }
30 req += `Connection: close\r\n`
31 req += `\r\n`
32 if (payload) req += payload
33 socket.write(req)
34 })
35
36 let data = ''
37 socket.on('data', (chunk) => { data += chunk.toString() })
38
39 socket.on('end', () => {
40 const idx = data.indexOf('\r\n\r\n')
41 if (idx === -1) return reject(new Error('No HTTP body'))
42 const jsonBody = data.slice(idx + 4)
43
44 try {
45 const parsed = JSON.parse(jsonBody)
46 if (!parsed.ok) reject(new Error(parsed.error || 'RPC error'))
47 else resolve(parsed.data)
48 } catch {
49 reject(new Error('Invalid JSON: ' + jsonBody.slice(0, 200)))
50 }
51 })
52
53 socket.on('error', reject)
54 socket.setTimeout(10000, () => {
55 socket.destroy(new Error('Request timeout'))
56 })
57 })
58 }
59
60 rpc (method, params = {}) {
61 return this._rawRequest('POST', '/rpc', { method, params })
62 }
63
64 async send (roomId, text) {
65 return this.rpc('send', { roomId, text })
66 }
67
68 async messages (roomId, limit = 20) {
69 return this._rawRequest('GET', `/rpc/messages/${roomId}?limit=${limit}`)
70 }
71
72 async rooms () {
73 return this._rawRequest('GET', '/rpc/rooms')
74 }
75
76 async identity () {
77 return this._rawRequest('GET', '/rpc/identity')
78 }
79
80 async status () {
81 return this._rawRequest('GET', '/rpc/status')
82 }
83
84 async members (roomId) {
85 return this._rawRequest('GET', `/rpc/members/${roomId}`)
86 }
87
88 watch (roomId, onMessage) {
89 if (this._watching) return
90 this._watching = true
91 this._doWatch(roomId, onMessage)
92 }
93
94 _doWatch (roomId, onMessage) {
95 if (!this._watching) return
96
97 const req = http.request({
98 hostname: this.host,
99 port: this.port,
100 path: `/rpc/watch/${roomId}`,
101 method: 'GET',
102 headers: { Accept: 'text/event-stream' }
103 }, (res) => {
104 let buffer = ''
105
106 res.on('data', (chunk) => {
107 buffer += chunk.toString()
108 const events = buffer.split('\n\n')
109 buffer = events.pop()
110
111 for (const event of events) {
112 if (!event.trim()) continue
113 const lines = event.split('\n')
114 let type = 'message'
115 let data = null
116
117 for (const line of lines) {
118 if (line.startsWith('event: ')) type = line.slice(7)
119 if (line.startsWith('data: ')) {
120 try { data = JSON.parse(line.slice(6)) } catch {}
121 }
122 }
123
124 if (type === 'message' && data) {
125 onMessage(data)
126 }
127 }
128 })
129
130 res.on('end', () => {
131 if (this._watching) {
132 setTimeout(() => this._doWatch(roomId, onMessage), 2000)
133 }
134 })
135
136 res.on('error', () => {
137 if (this._watching) {
138 setTimeout(() => this._doWatch(roomId, onMessage), 5000)
139 }
140 })
141 })
142
143 req.on('error', () => {
144 if (this._watching) {
145 setTimeout(() => this._doWatch(roomId, onMessage), 5000)
146 }
147 })
148
149 this._watchAbort = req
150 req.end()
151 }
152
153 stopWatch () {
154 this._watching = false
155 if (this._watchAbort) {
156 this._watchAbort.destroy()
157 this._watchAbort = null
158 }
159 }
160}
161
162export { KeetClient }
bridge-mastodon/lib/mastodon-client.js +182 -0
……@@ -0,0 +1,182 @@
1import https from 'https'
2import { URL } from 'url'
3
4class MastodonClient {
5 constructor (opts = {}) {
6 this.instance = opts.instance // e.g. 'mastodon.social'
7 this.token = opts.token
8 this._streaming = false
9 this._streamReq = null
10 }
11
12 _request (method, path, body) {
13 return new Promise((resolve, reject) => {
14 const url = new URL(`https://${this.instance}${path}`)
15
16 const opts = {
17 hostname: url.hostname,
18 port: 443,
19 path: url.pathname + url.search,
20 method,
21 headers: {
22 Authorization: `Bearer ${this.token}`,
23 Accept: 'application/json'
24 }
25 }
26
27 let payload = ''
28 if (body) {
29 payload = JSON.stringify(body)
30 opts.headers['Content-Type'] = 'application/json'
31 opts.headers['Content-Length'] = Buffer.byteLength(payload)
32 }
33
34 const req = https.request(opts, (res) => {
35 let data = ''
36 res.on('data', (chunk) => { data += chunk.toString() })
37 res.on('end', () => {
38 if (res.statusCode >= 400) {
39 return reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`))
40 }
41 try {
42 resolve(JSON.parse(data))
43 } catch {
44 reject(new Error('Invalid JSON: ' + data.slice(0, 200)))
45 }
46 })
47 })
48
49 req.on('error', reject)
50 req.setTimeout(15000, () => req.destroy(new Error('Request timeout')))
51
52 if (payload) req.write(payload)
53 req.end()
54 })
55 }
56
57 // Post a status (toot)
58 async postStatus (text, opts = {}) {
59 const body = { status: text }
60 if (opts.visibility) body.visibility = opts.visibility // public, unlisted, private, direct
61 if (opts.spoilerText) body.spoiler_text = opts.spoilerText
62 if (opts.inReplyTo) body.in_reply_to_id = opts.inReplyTo
63 if (opts.mediaIds) body.media_ids = opts.mediaIds
64 return this._request('POST', '/api/v1/statuses', body)
65 }
66
67 // Get home timeline
68 async homeTimeline (limit = 20, sinceId) {
69 let path = `/api/v1/timelines/home?limit=${limit}`
70 if (sinceId) path += `&since_id=${sinceId}`
71 return this._request('GET', path)
72 }
73
74 // Get hashtag timeline
75 async hashtagTimeline (tag, limit = 20, sinceId) {
76 let path = `/api/v1/timelines/tag/${encodeURIComponent(tag)}?limit=${limit}`
77 if (sinceId) path += `&since_id=${sinceId}`
78 return this._request('GET', path)
79 }
80
81 // Get notifications
82 async notifications (limit = 15, sinceId) {
83 let path = `/api/v1/notifications?limit=${limit}`
84 if (sinceId) path += `&since_id=${sinceId}`
85 return this._request('GET', path)
86 }
87
88 // Boost (reblog) a status
89 async boost (statusId) {
90 return this._request('POST', `/api/v1/statuses/${statusId}/reblog`)
91 }
92
93 // Favourite a status
94 async favourite (statusId) {
95 return this._request('POST', `/api/v1/statuses/${statusId}/favourite`)
96 }
97
98 // Verify credentials
99 async verifyCredentials () {
100 return this._request('GET', '/api/v1/accounts/verify_credentials')
101 }
102
103 // SSE streaming for user timeline
104 stream (streamType, onStatus) {
105 if (this._streaming) return
106 this._streaming = true
107 this._doStream(streamType, onStatus)
108 }
109
110 _doStream (streamType, onStatus) {
111 if (!this._streaming) return
112
113 // streamType: 'user', 'public', 'public:local', 'hashtag&tag=foo'
114 const req = https.request({
115 hostname: this.instance,
116 port: 443,
117 path: `/api/v1/streaming/${streamType}`,
118 method: 'GET',
119 headers: {
120 Authorization: `Bearer ${this.token}`,
121 Accept: 'text/event-stream'
122 }
123 }, (res) => {
124 let buffer = ''
125
126 res.on('data', (chunk) => {
127 buffer += chunk.toString()
128 const events = buffer.split('\n\n')
129 buffer = events.pop()
130
131 for (const event of events) {
132 if (!event.trim()) continue
133 const lines = event.split('\n')
134 let type = ''
135 let data = null
136
137 for (const line of lines) {
138 if (line.startsWith('event: ')) type = line.slice(7).trim()
139 if (line.startsWith('data: ')) {
140 try { data = JSON.parse(line.slice(6)) } catch {}
141 }
142 }
143
144 if (type === 'update' && data) {
145 onStatus(data)
146 }
147 }
148 })
149
150 res.on('end', () => {
151 if (this._streaming) {
152 setTimeout(() => this._doStream(streamType, onStatus), 3000)
153 }
154 })
155
156 res.on('error', () => {
157 if (this._streaming) {
158 setTimeout(() => this._doStream(streamType, onStatus), 5000)
159 }
160 })
161 })
162
163 req.on('error', () => {
164 if (this._streaming) {
165 setTimeout(() => this._doStream(streamType, onStatus), 5000)
166 }
167 })
168
169 this._streamReq = req
170 req.end()
171 }
172
173 stopStream () {
174 this._streaming = false
175 if (this._streamReq) {
176 this._streamReq.destroy()
177 this._streamReq = null
178 }
179 }
180}
181
182export { MastodonClient }
bridge-mastodon/lib/router.js +69 -0
……@@ -0,0 +1,69 @@
1import { createHash } from 'crypto'
2
3class Router {
4 constructor (opts = {}) {
5 this._seen = new Map()
6 this._maxSeen = opts.maxSeen || 10000
7 this._ttl = opts.ttl || 60000
8 this._onExternalMessage = opts.onExternalMessage || null
9 this._onKeetMessage = opts.onKeetMessage || null
10 }
11
12 _hash (text, author) {
13 return createHash('sha256')
14 .update(author || '')
15 .update(text || '')
16 .digest('hex')
17 .slice(0, 16)
18 }
19
20 _isDuplicate (hash) {
21 const now = Date.now()
22
23 if (this._seen.size > this._maxSeen) {
24 for (const [k, ts] of this._seen) {
25 if (now - ts > this._ttl) this._seen.delete(k)
26 }
27 }
28
29 if (this._seen.has(hash)) {
30 const ts = this._seen.get(hash)
31 if (now - ts < this._ttl) return true
32 }
33
34 this._seen.set(hash, now)
35 return false
36 }
37
38 // External platform message → relay to Keet
39 routeToKeet (author, text) {
40 const hash = this._hash(text, author)
41 if (this._isDuplicate(hash)) return false
42
43 if (this._onKeetMessage) {
44 this._onKeetMessage(author, text)
45 }
46 return true
47 }
48
49 // Keet message → relay to external platform
50 routeToExternal (name, text) {
51 const hash = this._hash(text, name)
52 if (this._isDuplicate(hash)) return false
53
54 if (this._onExternalMessage) {
55 this._onExternalMessage(name, text)
56 }
57 return true
58 }
59
60 get seenCount () {
61 return this._seen.size
62 }
63
64 clearSeen () {
65 this._seen.clear()
66 }
67}
68
69export { Router }
bridge-mastodon/package-lock.json +875 -0
……@@ -0,0 +1,875 @@
1{
2 "name": "bridge-mastodon",
3 "version": "1.0.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "bridge-mastodon",
9 "version": "1.0.0",
10 "license": "MIT",
11 "dependencies": {
12 "b4a": "^1.0.0",
13 "bridge-envelope": "file:../bridge-envelope"
14 },
15 "devDependencies": {
16 "brittle": "^3.0.0"
17 }
18 },
19 "../bridge-envelope": {
20 "version": "1.0.0",
21 "license": "MIT",
22 "dependencies": {
23 "b4a": "^1.0.0",
24 "compact-encoding": "^2.0.0"
25 },
26 "devDependencies": {
27 "brittle": "^3.0.0"
28 }
29 },
30 "node_modules/@jridgewell/resolve-uri": {
31 "version": "3.1.2",
32 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
33 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
34 "dev": true,
35 "license": "MIT",
36 "engines": {
37 "node": ">=6.0.0"
38 }
39 },
40 "node_modules/@jridgewell/sourcemap-codec": {
41 "version": "1.5.5",
42 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
43 "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
44 "dev": true,
45 "license": "MIT"
46 },
47 "node_modules/@jridgewell/trace-mapping": {
48 "version": "0.3.31",
49 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
50 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
51 "dev": true,
52 "license": "MIT",
53 "dependencies": {
54 "@jridgewell/resolve-uri": "^3.1.0",
55 "@jridgewell/sourcemap-codec": "^1.4.14"
56 }
57 },
58 "node_modules/@types/istanbul-lib-coverage": {
59 "version": "2.0.6",
60 "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
61 "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
62 "dev": true,
63 "license": "MIT"
64 },
65 "node_modules/b4a": {
66 "version": "1.7.4",
67 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.4.tgz",
68 "integrity": "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug==",
69 "license": "Apache-2.0",
70 "peerDependencies": {
71 "react-native-b4a": "*"
72 },
73 "peerDependenciesMeta": {
74 "react-native-b4a": {
75 "optional": true
76 }
77 }
78 },
79 "node_modules/bare-addon-resolve": {
80 "version": "1.10.0",
81 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
82 "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
83 "dev": true,
84 "license": "Apache-2.0",
85 "dependencies": {
86 "bare-module-resolve": "^1.10.0",
87 "bare-semver": "^1.0.0"
88 },
89 "peerDependencies": {
90 "bare-url": "*"
91 },
92 "peerDependenciesMeta": {
93 "bare-url": {
94 "optional": true
95 }
96 }
97 },
98 "node_modules/bare-ansi-escapes": {
99 "version": "2.2.3",
100 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
101 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
102 "dev": true,
103 "license": "Apache-2.0",
104 "dependencies": {
105 "bare-stream": "^2.6.5"
106 },
107 "peerDependencies": {
108 "bare-buffer": "*"
109 },
110 "peerDependenciesMeta": {
111 "bare-buffer": {
112 "optional": true
113 }
114 }
115 },
116 "node_modules/bare-assert": {
117 "version": "1.2.0",
118 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
119 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
120 "dev": true,
121 "license": "Apache-2.0",
122 "dependencies": {
123 "bare-inspect": "^3.1.2"
124 }
125 },
126 "node_modules/bare-bundle": {
127 "version": "1.10.0",
128 "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
129 "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
130 "dev": true,
131 "license": "Apache-2.0",
132 "peerDependencies": {
133 "bare-buffer": "*",
134 "bare-url": "*"
135 },
136 "peerDependenciesMeta": {
137 "bare-buffer": {
138 "optional": true
139 },
140 "bare-url": {
141 "optional": true
142 }
143 }
144 },
145 "node_modules/bare-cov": {
146 "version": "1.2.1",
147 "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz",
148 "integrity": "sha512-SHNqb/WG9udUEj8m5qGSehoUQ9o0Xzv8nrf4HIdmG+KNU/XdhPLhTP5VPrHnSAMAXhAzEB4Q+2IYZ0x9qPzN1g==",
149 "dev": true,
150 "license": "Apache-2.0",
151 "dependencies": {
152 "bare-fs": "^4.1.2",
153 "bare-inspector": "^4.0.1",
154 "bare-path": "^3.0.0",
155 "bare-process": "^4.2.1",
156 "bare-url": "^2.1.5",
157 "bare-v8-to-istanbul": "^1.0.2",
158 "picomatch": "^4.0.2",
159 "which-runtime": "^1.2.1"
160 }
161 },
162 "node_modules/bare-crypto": {
163 "version": "1.13.0",
164 "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.0.tgz",
165 "integrity": "sha512-RQl13yD+YTACWUZHMMck0C6LNjXgGgRyhVC557ag0xIrkxXf4IQwpnc8WOB58f4k5kSzKhIjy05oW3HUBrFpSQ==",
166 "dev": true,
167 "license": "Apache-2.0",
168 "dependencies": {
169 "bare-stream": "^2.6.3"
170 },
171 "peerDependencies": {
172 "bare-buffer": "*"
173 },
174 "peerDependenciesMeta": {
175 "bare-buffer": {
176 "optional": true
177 }
178 }
179 },
180 "node_modules/bare-debug-log": {
181 "version": "2.0.0",
182 "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
183 "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
184 "dev": true,
185 "license": "Apache-2.0",
186 "dependencies": {
187 "bare-os": "^3.0.1"
188 }
189 },
190 "node_modules/bare-dns": {
191 "version": "2.1.4",
192 "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
193 "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==",
194 "dev": true,
195 "license": "Apache-2.0",
196 "engines": {
197 "bare": ">=1.7.0"
198 }
199 },
200 "node_modules/bare-encoding": {
201 "version": "1.0.3",
202 "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
203 "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
204 "dev": true,
205 "license": "Apache-2.0",
206 "peerDependencies": {
207 "bare-buffer": "*"
208 },
209 "peerDependenciesMeta": {
210 "bare-buffer": {
211 "optional": true
212 }
213 }
214 },
215 "node_modules/bare-env": {
216 "version": "3.0.0",
217 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
218 "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
219 "dev": true,
220 "license": "Apache-2.0",
221 "dependencies": {
222 "bare-os": "^3.0.1"
223 }
224 },
225 "node_modules/bare-events": {
226 "version": "2.8.2",
227 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
228 "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
229 "dev": true,
230 "license": "Apache-2.0",
231 "peerDependencies": {
232 "bare-abort-controller": "*"
233 },
234 "peerDependenciesMeta": {
235 "bare-abort-controller": {
236 "optional": true
237 }
238 }
239 },
240 "node_modules/bare-format": {
241 "version": "1.0.1",
242 "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.1.tgz",
243 "integrity": "sha512-1oS+LZrWK6tnYnvNSHDGljc2MPunRxwhpFriuCgzNF+oklrnwmBKD91tS0yt+jpl2j3UgcSDzBIMiVTvLs9A8w==",
244 "dev": true,
245 "license": "Apache-2.0",
246 "dependencies": {
247 "bare-inspect": "^3.0.0"
248 }
249 },
250 "node_modules/bare-fs": {
251 "version": "4.5.4",
252 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz",
253 "integrity": "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==",
254 "dev": true,
255 "license": "Apache-2.0",
256 "dependencies": {
257 "bare-events": "^2.5.4",
258 "bare-path": "^3.0.0",
259 "bare-stream": "^2.6.4",
260 "bare-url": "^2.2.2",
261 "fast-fifo": "^1.3.2"
262 },
263 "engines": {
264 "bare": ">=1.16.0"
265 },
266 "peerDependencies": {
267 "bare-buffer": "*"
268 },
269 "peerDependenciesMeta": {
270 "bare-buffer": {
271 "optional": true
272 }
273 }
274 },
275 "node_modules/bare-hrtime": {
276 "version": "2.1.1",
277 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
278 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
279 "dev": true,
280 "license": "Apache-2.0"
281 },
282 "node_modules/bare-http-parser": {
283 "version": "1.0.1",
284 "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.0.1.tgz",
285 "integrity": "sha512-A3LTDTcELcmNJ3g5liIaS038v/BQxOhA9cjhBESn7eoV7QCuMoIRBKLDadDe08flxyLbxI2f+1l2MZ/5+HnKPA==",
286 "dev": true,
287 "license": "Apache-2.0"
288 },
289 "node_modules/bare-http1": {
290 "version": "4.2.3",
291 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.2.3.tgz",
292 "integrity": "sha512-zyvHF3xzvBJNpuw0QydE9etTn2AyC/TRVDWTo8Ed3LQEEGaFOkv6KHhaJPlf4eWEeTekk41Fowie+fTsXoNxkA==",
293 "dev": true,
294 "license": "Apache-2.0",
295 "dependencies": {
296 "bare-events": "^2.6.0",
297 "bare-http-parser": "^1.0.0",
298 "bare-stream": "^2.3.0",
299 "bare-tcp": "^2.2.0"
300 },
301 "peerDependencies": {
302 "bare-buffer": "*",
303 "bare-url": "*"
304 },
305 "peerDependenciesMeta": {
306 "bare-buffer": {
307 "optional": true
308 },
309 "bare-url": {
310 "optional": true
311 }
312 }
313 },
314 "node_modules/bare-https": {
315 "version": "2.1.2",
316 "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.2.tgz",
317 "integrity": "sha512-Q+TTydUDsuKQJvh8dX2dvOXCR9fM3xR5TBmKaFrs5p7Lj7XbKX7v4vIUJ36H0SXg2xCOQxXKIbjwrLg5tfJNYg==",
318 "dev": true,
319 "license": "Apache-2.0",
320 "dependencies": {
321 "bare-http1": "^4.0.0",
322 "bare-tcp": "^2.2.0",
323 "bare-tls": "^2.0.0"
324 }
325 },
326 "node_modules/bare-inspect": {
327 "version": "3.1.4",
328 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
329 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
330 "dev": true,
331 "license": "Apache-2.0",
332 "dependencies": {
333 "bare-ansi-escapes": "^2.1.0",
334 "bare-type": "^1.0.0"
335 },
336 "engines": {
337 "bare": ">=1.18.0"
338 }
339 },
340 "node_modules/bare-inspector": {
341 "version": "4.0.3",
342 "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-4.0.3.tgz",
343 "integrity": "sha512-yYk6+7tTVKJKHOja5+QBn989ETv8oVsmwoH0nCM+QYC6w5MC03GC4aQNK+lipAZSz1EaAj42WZ8Wewjpy0L9TA==",
344 "dev": true,
345 "license": "Apache-2.0",
346 "dependencies": {
347 "bare-events": "^2.1.0",
348 "bare-http1": "^4.0.0",
349 "bare-stream": "^2.0.0",
350 "bare-url": "^2.0.0",
351 "bare-ws": "^2.0.0"
352 },
353 "engines": {
354 "bare": ">=1.2.0"
355 },
356 "peerDependencies": {
357 "bare-tcp": "*"
358 },
359 "peerDependenciesMeta": {
360 "bare-tcp": {
361 "optional": true
362 }
363 }
364 },
365 "node_modules/bare-module": {
366 "version": "6.1.3",
367 "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz",
368 "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==",
369 "dev": true,
370 "license": "Apache-2.0",
371 "dependencies": {
372 "bare-bundle": "^1.3.0",
373 "bare-module-lexer": "^1.0.0",
374 "bare-module-resolve": "^1.8.0",
375 "bare-path": "^3.0.0",
376 "bare-url": "^2.0.1"
377 },
378 "engines": {
379 "bare": ">=1.23.0"
380 },
381 "peerDependencies": {
382 "bare-buffer": "*"
383 },
384 "peerDependenciesMeta": {
385 "bare-buffer": {
386 "optional": true
387 }
388 }
389 },
390 "node_modules/bare-module-lexer": {
391 "version": "1.4.7",
392 "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
393 "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
394 "dev": true,
395 "license": "Apache-2.0",
396 "dependencies": {
397 "require-addon": "^1.0.2"
398 },
399 "peerDependencies": {
400 "bare-buffer": "*"
401 },
402 "peerDependenciesMeta": {
403 "bare-buffer": {
404 "optional": true
405 }
406 }
407 },
408 "node_modules/bare-module-resolve": {
409 "version": "1.12.1",
410 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz",
411 "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==",
412 "dev": true,
413 "license": "Apache-2.0",
414 "dependencies": {
415 "bare-semver": "^1.0.0"
416 },
417 "peerDependencies": {
418 "bare-url": "*"
419 },
420 "peerDependenciesMeta": {
421 "bare-url": {
422 "optional": true
423 }
424 }
425 },
426 "node_modules/bare-net": {
427 "version": "2.2.0",
428 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.2.0.tgz",
429 "integrity": "sha512-UF7cAbHsGE+H6uEqWF5IULBow1x58chZz4g3ALgHtv7wZsFcCbRDt0JKWEumf5Oma3QWS1Q6aLi0Rpll8RElMg==",
430 "dev": true,
431 "license": "Apache-2.0",
432 "dependencies": {
433 "bare-events": "^2.2.2",
434 "bare-pipe": "^4.0.0",
435 "bare-stream": "^2.0.0",
436 "bare-tcp": "^2.0.0"
437 }
438 },
439 "node_modules/bare-os": {
440 "version": "3.6.2",
441 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
442 "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
443 "dev": true,
444 "license": "Apache-2.0",
445 "engines": {
446 "bare": ">=1.14.0"
447 }
448 },
449 "node_modules/bare-path": {
450 "version": "3.0.0",
451 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
452 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
453 "dev": true,
454 "license": "Apache-2.0",
455 "dependencies": {
456 "bare-os": "^3.0.1"
457 }
458 },
459 "node_modules/bare-pipe": {
460 "version": "4.1.3",
461 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.3.tgz",
462 "integrity": "sha512-DqQsx93rAzre6yJ9T6l/Vgh+X+bntkVMB1X5ggtXjXtqtMmF2Y2RVlCzxxy/09R6yeR9FSWBEUIaMYJL1/5VDA==",
463 "dev": true,
464 "license": "Apache-2.0",
465 "dependencies": {
466 "bare-events": "^2.0.0",
467 "bare-stream": "^2.0.0"
468 },
469 "engines": {
470 "bare": ">=1.16.0"
471 }
472 },
473 "node_modules/bare-process": {
474 "version": "4.2.2",
475 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.2.2.tgz",
476 "integrity": "sha512-ikzEw+HGLB+2lS/WLVEsqi8BXBwzleG3n7DYI0v6/YNklvNabOIGlLd1dof+7Jw5ob4jsBRPtyflweVahY7rFA==",
477 "dev": true,
478 "license": "Apache-2.0",
479 "dependencies": {
480 "bare-env": "^3.0.0",
481 "bare-events": "^2.3.1",
482 "bare-hrtime": "^2.0.0",
483 "bare-os": "^3.5.0",
484 "bare-pipe": "^4.0.0",
485 "bare-signals": "^4.0.0",
486 "bare-tty": "^5.0.0"
487 }
488 },
489 "node_modules/bare-semver": {
490 "version": "1.0.2",
491 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz",
492 "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==",
493 "dev": true,
494 "license": "Apache-2.0"
495 },
496 "node_modules/bare-signals": {
497 "version": "4.2.0",
498 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz",
499 "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==",
500 "dev": true,
501 "license": "Apache-2.0",
502 "dependencies": {
503 "bare-events": "^2.5.3",
504 "bare-os": "^3.3.1"
505 },
506 "engines": {
507 "bare": ">=1.7.0"
508 }
509 },
510 "node_modules/bare-stream": {
511 "version": "2.7.0",
512 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
513 "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
514 "dev": true,
515 "license": "Apache-2.0",
516 "dependencies": {
517 "streamx": "^2.21.0"
518 },
519 "peerDependencies": {
520 "bare-buffer": "*",
521 "bare-events": "*"
522 },
523 "peerDependenciesMeta": {
524 "bare-buffer": {
525 "optional": true
526 },
527 "bare-events": {
528 "optional": true
529 }
530 }
531 },
532 "node_modules/bare-subprocess": {
533 "version": "5.2.2",
534 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.2.tgz",
535 "integrity": "sha512-L6oXgQ1aWs25RtG5Ky0bDD06p3RAcVVrDDMWb1DfXpHtyEWxamcyIWUbSykxMTWrpLlmSj6ytbb6yoKehGFfmw==",
536 "dev": true,
537 "license": "Apache-2.0",
538 "dependencies": {
539 "bare-env": "^3.0.0",
540 "bare-events": "^2.5.4",
541 "bare-os": "^3.0.1",
542 "bare-pipe": "^4.0.0",
543 "bare-url": "^2.2.2"
544 },
545 "engines": {
546 "bare": ">=1.7.0"
547 },
548 "peerDependencies": {
549 "bare-buffer": "*"
550 },
551 "peerDependenciesMeta": {
552 "bare-buffer": {
553 "optional": true
554 }
555 }
556 },
557 "node_modules/bare-tcp": {
558 "version": "2.2.4",
559 "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.4.tgz",
560 "integrity": "sha512-vGjfMkyhS1lxp+Y4+gXRDR1ijZMK2cKx0RBwtYKUuZbD90ARCoM6bAiTa1HBztumCIC+NXb8eXdMFXyuYwlPyw==",
561 "dev": true,
562 "license": "Apache-2.0",
563 "dependencies": {
564 "bare-dns": "^2.0.4",
565 "bare-events": "^2.5.4",
566 "bare-stream": "^2.6.4"
567 },
568 "engines": {
569 "bare": ">=1.16.0"
570 }
571 },
572 "node_modules/bare-tls": {
573 "version": "2.1.7",
574 "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.1.7.tgz",
575 "integrity": "sha512-h6wcNXQdBeTX7fed9tjPp0/9cA/QfcBTv3ItgjnbUk4rWAU8bEFalZCZnUDdCK/t9zrNfJ+yvcPx4D/1Y6biyA==",
576 "dev": true,
577 "license": "Apache-2.0",
578 "dependencies": {
579 "bare-net": "^2.0.1",
580 "bare-stream": "^2.6.4"
581 },
582 "engines": {
583 "bare": ">=1.7.0"
584 }
585 },
586 "node_modules/bare-tty": {
587 "version": "5.0.3",
588 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.0.3.tgz",
589 "integrity": "sha512-jW24RBWRZOMHuSEWC9mh8wUKO5WUNl0UWUTNPn3yZ8qkOqwa8vaV2dR3a+BvblONPk7V35wuvK9P8508+judAg==",
590 "dev": true,
591 "license": "Apache-2.0",
592 "dependencies": {
593 "bare-events": "^2.2.0",
594 "bare-signals": "^4.0.0",
595 "bare-stream": "^2.0.0"
596 },
597 "engines": {
598 "bare": ">=1.16.0"
599 }
600 },
601 "node_modules/bare-type": {
602 "version": "1.1.0",
603 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
604 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
605 "dev": true,
606 "license": "Apache-2.0",
607 "engines": {
608 "bare": ">=1.2.0"
609 }
610 },
611 "node_modules/bare-url": {
612 "version": "2.3.2",
613 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
614 "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
615 "dev": true,
616 "license": "Apache-2.0",
617 "peer": true,
618 "dependencies": {
619 "bare-path": "^3.0.0"
620 }
621 },
622 "node_modules/bare-utils": {
623 "version": "1.5.1",
624 "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.5.1.tgz",
625 "integrity": "sha512-mxCkFvmDU3mlD/mb+pT64kKXOsx2KMsWLQbngN1LB+NOXfhfnRnyvpy3VZc6m7gzQxe57Bsi+aTCBqA4/S3elQ==",
626 "dev": true,
627 "license": "Apache-2.0",
628 "dependencies": {
629 "bare-debug-log": "^2.0.0",
630 "bare-encoding": "^1.0.0",
631 "bare-format": "^1.0.0",
632 "bare-inspect": "^3.0.0",
633 "bare-type": "^1.0.6"
634 }
635 },
636 "node_modules/bare-v8-to-istanbul": {
637 "version": "1.0.2",
638 "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
639 "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==",
640 "dev": true,
641 "license": "Apache-2.0",
642 "dependencies": {
643 "bare-assert": "^1.0.2",
644 "bare-fs": "^4.1.2",
645 "bare-module": "^6.1.2",
646 "bare-path": "^3.0.0",
647 "bare-process": "^4.2.0",
648 "bare-url": "^2.1.5",
649 "bare-utils": "^1.2.0",
650 "v8-to-istanbul": "^9.3.0",
651 "which-runtime": "^1.2.1"
652 }
653 },
654 "node_modules/bare-ws": {
655 "version": "2.0.4",
656 "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.0.4.tgz",
657 "integrity": "sha512-SQMXzBYna9dRj57Dz1/ag+VWHCRXbfCjMHgyfM2F2lhkVLzMjnVSZP72aVeFWPFqe494Rd70Kzhe2JElGwFlJQ==",
658 "dev": true,
659 "license": "Apache-2.0",
660 "dependencies": {
661 "bare-crypto": "^1.2.0",
662 "bare-events": "^2.3.1",
663 "bare-http1": "^4.0.0",
664 "bare-https": "^2.0.0",
665 "bare-stream": "^2.1.2"
666 },
667 "peerDependencies": {
668 "bare-buffer": "*",
669 "bare-url": "*"
670 },
671 "peerDependenciesMeta": {
672 "bare-buffer": {
673 "optional": true
674 },
675 "bare-url": {
676 "optional": true
677 }
678 }
679 },
680 "node_modules/bridge-envelope": {
681 "resolved": "../bridge-envelope",
682 "link": true
683 },
684 "node_modules/brittle": {
685 "version": "3.19.1",
686 "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz",
687 "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==",
688 "dev": true,
689 "license": "Apache-2.0",
690 "dependencies": {
691 "b4a": "^1.6.0",
692 "bare-assert": "^1.0.2",
693 "bare-cov": "^1.1.0",
694 "bare-fs": "^4.1.6",
695 "bare-os": "^3.6.1",
696 "bare-path": "^3.0.0",
697 "bare-process": "^4.2.1",
698 "bare-subprocess": "^5.0.0",
699 "bare-url": "^2.1.6",
700 "error-stack-parser": "^2.1.4",
701 "globbie": "^1.0.2",
702 "paparam": "^1.6.2",
703 "same-object": "^1.0.2",
704 "test-tmp": "^1.4.0",
705 "tmatch": "^5.0.0"
706 },
707 "bin": {
708 "brittle": "bin/node.js",
709 "brittle-bare": "bin/bare.js",
710 "brittle-node": "bin/node.js",
711 "brittle-pear": "bin/pear.js"
712 }
713 },
714 "node_modules/convert-source-map": {
715 "version": "2.0.0",
716 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
717 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
718 "dev": true,
719 "license": "MIT"
720 },
721 "node_modules/error-stack-parser": {
722 "version": "2.1.4",
723 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
724 "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
725 "dev": true,
726 "license": "MIT",
727 "dependencies": {
728 "stackframe": "^1.3.4"
729 }
730 },
731 "node_modules/events-universal": {
732 "version": "1.0.1",
733 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
734 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
735 "dev": true,
736 "license": "Apache-2.0",
737 "dependencies": {
738 "bare-events": "^2.7.0"
739 }
740 },
741 "node_modules/fast-fifo": {
742 "version": "1.3.2",
743 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
744 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
745 "dev": true,
746 "license": "MIT"
747 },
748 "node_modules/globbie": {
749 "version": "1.0.3",
750 "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz",
751 "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==",
752 "dev": true,
753 "license": "Apache-2.0",
754 "dependencies": {
755 "bare-fs": "^4.1.2",
756 "bare-path": "^3.0.0",
757 "bare-process": "^4.2.1",
758 "picomatch": "^4.0.2"
759 }
760 },
761 "node_modules/paparam": {
762 "version": "1.10.0",
763 "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.0.tgz",
764 "integrity": "sha512-p36QQrwU3X6fj5d2JN20B4lcZI/O3fxMVUHc1xD7MNe5bcOf3jIgE0CIK5Zv/s5YAUGlobTJ/kKXrh1FJVrs3w==",
765 "dev": true,
766 "license": "Apache-2.0"
767 },
768 "node_modules/picomatch": {
769 "version": "4.0.3",
770 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
771 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
772 "dev": true,
773 "license": "MIT",
774 "engines": {
775 "node": ">=12"
776 },
777 "funding": {
778 "url": "https://github.com/sponsors/jonschlinkert"
779 }
780 },
781 "node_modules/require-addon": {
782 "version": "1.2.0",
783 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
784 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
785 "dev": true,
786 "license": "Apache-2.0",
787 "dependencies": {
788 "bare-addon-resolve": "^1.3.0"
789 },
790 "engines": {
791 "bare": ">=1.10.0"
792 }
793 },
794 "node_modules/same-object": {
795 "version": "1.0.2",
796 "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz",
797 "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==",
798 "dev": true,
799 "license": "MIT"
800 },
801 "node_modules/stackframe": {
802 "version": "1.3.4",
803 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
804 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
805 "dev": true,
806 "license": "MIT"
807 },
808 "node_modules/streamx": {
809 "version": "2.23.0",
810 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
811 "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
812 "dev": true,
813 "license": "MIT",
814 "dependencies": {
815 "events-universal": "^1.0.0",
816 "fast-fifo": "^1.3.2",
817 "text-decoder": "^1.1.0"
818 }
819 },
820 "node_modules/test-tmp": {
821 "version": "1.4.0",
822 "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz",
823 "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==",
824 "dev": true,
825 "license": "MIT",
826 "dependencies": {
827 "bare-fs": "^4.0.1",
828 "bare-os": "^3.3.0",
829 "bare-path": "^3.0.0"
830 }
831 },
832 "node_modules/text-decoder": {
833 "version": "1.2.6",
834 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.6.tgz",
835 "integrity": "sha512-27FeW5GQFDfw0FpwMQhMagB7BztOOlmjcSRi97t2oplhKVTZtp0DZbSegSaXS5IIC6mxMvBG4AR1Sgc6BX3CQg==",
836 "dev": true,
837 "license": "Apache-2.0",
838 "dependencies": {
839 "b4a": "^1.6.4"
840 }
841 },
842 "node_modules/tmatch": {
843 "version": "5.0.0",
844 "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz",
845 "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==",
846 "dev": true,
847 "license": "ISC",
848 "engines": {
849 "node": ">=8"
850 }
851 },
852 "node_modules/v8-to-istanbul": {
853 "version": "9.3.0",
854 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
855 "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
856 "dev": true,
857 "license": "ISC",
858 "dependencies": {
859 "@jridgewell/trace-mapping": "^0.3.12",
860 "@types/istanbul-lib-coverage": "^2.0.1",
861 "convert-source-map": "^2.0.0"
862 },
863 "engines": {
864 "node": ">=10.12.0"
865 }
866 },
867 "node_modules/which-runtime": {
868 "version": "1.3.2",
869 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
870 "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
871 "dev": true,
872 "license": "Apache-2.0"
873 }
874 }
875}
bridge-mastodon/package.json +22 -0
……@@ -0,0 +1,22 @@
1{
2 "name": "bridge-mastodon",
3 "version": "1.0.0",
4 "description": "Mastodon ↔ Keet bidirectional bridge (POSSE syndication)",
5 "type": "module",
6 "main": "index.js",
7 "pear": {
8 "name": "bridge-mastodon",
9 "type": "terminal"
10 },
11 "scripts": {
12 "test": "brittle test/*.test.js"
13 },
14 "dependencies": {
15 "bridge-envelope": "file:../bridge-envelope",
16 "b4a": "^1.0.0"
17 },
18 "devDependencies": {
19 "brittle": "^3.0.0"
20 },
21 "license": "MIT"
22}
bridge-mastodon/test/adapter.test.js +245 -0
……@@ -0,0 +1,245 @@
1import test from 'brittle'
2import { statusToEnvelope, keetToMastodon, envelopeToStatusText, statusToKeetText, stripHtml } from '../lib/adapter.js'
3import { TYPES, PLATFORMS } from 'bridge-envelope'
4
5test('stripHtml - basic tags', (t) => {
6 t.is(stripHtml('<p>Hello</p>'), 'Hello')
7 t.is(stripHtml('<p>Line 1</p><p>Line 2</p>'), 'Line 1\n\nLine 2')
8 t.is(stripHtml('Hello<br/>World'), 'Hello\nWorld')
9 t.is(stripHtml('Hello<br>World'), 'Hello\nWorld')
10})
11
12test('stripHtml - entities', (t) => {
13 t.is(stripHtml('&amp; &lt; &gt; &quot; &#39;'), '& < > " \'')
14})
15
16test('stripHtml - nested tags', (t) => {
17 t.is(stripHtml('<p>Hello <a href="https://example.com">link</a></p>'), 'Hello link')
18 t.is(stripHtml('<p><span class="mention">@user</span> hi</p>'), '@user hi')
19})
20
21test('statusToEnvelope - basic status', (t) => {
22 const status = {
23 id: '12345',
24 account: { acct: 'alice@mastodon.social', username: 'alice' },
25 content: '<p>Hello world!</p>',
26 visibility: 'public',
27 media_attachments: [],
28 tags: [],
29 created_at: '2026-02-14T10:00:00.000Z'
30 }
31
32 const env = statusToEnvelope(status)
33 t.ok(env)
34 t.is(env.type, TYPES.TEXT)
35 t.is(env.platform, PLATFORMS.MASTODON)
36 t.is(env.author, 'alice@mastodon.social')
37 t.is(env.content, 'Hello world!')
38 t.is(env.channel, 'public')
39 t.ok(env.id)
40})
41
42test('statusToEnvelope - status with media', (t) => {
43 const status = {
44 id: '12346',
45 account: { acct: 'bob' },
46 content: '<p>Check this out</p>',
47 visibility: 'unlisted',
48 media_attachments: [{
49 url: 'https://files.mastodon.social/image.jpg',
50 type: 'image',
51 preview_url: 'https://files.mastodon.social/thumb.jpg'
52 }],
53 tags: []
54 }
55
56 const env = statusToEnvelope(status)
57 t.is(env.type, TYPES.MEDIA)
58 t.is(env.mediaUrl, 'https://files.mastodon.social/image.jpg')
59 t.is(env.mediaType, 'image')
60})
61
62test('statusToEnvelope - reply', (t) => {
63 const status = {
64 id: '12347',
65 account: { acct: 'carol' },
66 content: '<p>@alice I agree!</p>',
67 in_reply_to_id: '12345',
68 visibility: 'public',
69 media_attachments: [],
70 tags: []
71 }
72
73 const env = statusToEnvelope(status)
74 t.is(env.type, TYPES.REPLY)
75 t.is(env.replyTo, '12345')
76})
77
78test('statusToEnvelope - boost (reblog)', (t) => {
79 const status = {
80 id: '99999',
81 account: { acct: 'dave' },
82 reblog: {
83 id: '12345',
84 account: { acct: 'alice@mastodon.social' },
85 content: '<p>Original post</p>',
86 visibility: 'public',
87 media_attachments: [],
88 tags: []
89 }
90 }
91
92 const env = statusToEnvelope(status)
93 t.ok(env)
94 t.is(env.author, 'alice@mastodon.social')
95 t.is(env.content, 'Original post')
96 t.ok(env._isBoost)
97 t.is(env._boostBy, 'dave')
98 t.is(env.channel, 'boost')
99})
100
101test('statusToEnvelope - with tags', (t) => {
102 const status = {
103 id: '12348',
104 account: { acct: 'eve' },
105 content: '<p>Great discussion #p2p #hypercore</p>',
106 visibility: 'public',
107 media_attachments: [],
108 tags: [{ name: 'p2p' }, { name: 'hypercore' }]
109 }
110
111 const env = statusToEnvelope(status)
112 t.alike(env._tags, ['p2p', 'hypercore'])
113})
114
115test('statusToEnvelope - with spoiler text', (t) => {
116 const status = {
117 id: '12349',
118 account: { acct: 'frank' },
119 content: '<p>Spoiler content here</p>',
120 spoiler_text: 'Movie spoiler',
121 visibility: 'public',
122 media_attachments: [],
123 tags: []
124 }
125
126 const env = statusToEnvelope(status)
127 t.is(env._spoilerText, 'Movie spoiler')
128})
129
130test('statusToEnvelope - null/empty', (t) => {
131 t.is(statusToEnvelope(null), null)
132 t.is(statusToEnvelope({ id: '1', account: { acct: 'x' }, content: '', media_attachments: [] }), null)
133})
134
135test('keetToMastodon - normal message', (t) => {
136 const msg = { name: 'fleeky', text: 'Hello from Keet!', memberId: 'abc123' }
137 const result = keetToMastodon(msg, 'bridge-id')
138 t.ok(result)
139 t.is(result.author, 'fleeky')
140 t.is(result.text, 'Hello from Keet!')
141})
142
143test('keetToMastodon - skip bridge identity', (t) => {
144 const msg = { name: 'clode', text: 'Hello', memberId: 'bridge-id' }
145 const result = keetToMastodon(msg, 'bridge-id')
146 t.is(result, null)
147})
148
149test('keetToMastodon - skip system messages', (t) => {
150 const msg = { name: 'system', text: 'joined', type: 'system' }
151 t.is(keetToMastodon(msg, 'bridge-id'), null)
152})
153
154test('keetToMastodon - skip already bridged', (t) => {
155 const msg = { name: 'someone', text: '[mastodon] @alice: hello', memberId: 'x' }
156 t.is(keetToMastodon(msg, 'bridge-id'), null)
157})
158
159test('keetToMastodon - skip empty', (t) => {
160 t.is(keetToMastodon({ name: 'x', text: '', memberId: 'y' }, 'b'), null)
161 t.is(keetToMastodon({ name: 'x', text: ' ', memberId: 'y' }, 'b'), null)
162 t.is(keetToMastodon(null, 'b'), null)
163})
164
165test('envelopeToStatusText - from Keet', (t) => {
166 const env = {
167 platform: PLATFORMS.KEET,
168 author: 'fleeky',
169 content: 'Hello world'
170 }
171 t.is(envelopeToStatusText(env), '[Keet] fleeky: Hello world')
172})
173
174test('envelopeToStatusText - from IRC', (t) => {
175 const env = {
176 platform: PLATFORMS.IRC,
177 author: 'nick',
178 content: 'IRC message'
179 }
180 t.is(envelopeToStatusText(env), '[IRC] nick: IRC message')
181})
182
183test('statusToKeetText - basic', (t) => {
184 const env = statusToEnvelope({
185 id: '1',
186 account: { acct: 'alice' },
187 content: '<p>Hello!</p>',
188 visibility: 'public',
189 media_attachments: [],
190 tags: []
191 })
192 const text = statusToKeetText(env)
193 t.ok(text.includes('[mastodon]'))
194 t.ok(text.includes('@alice'))
195 t.ok(text.includes('Hello!'))
196})
197
198test('statusToKeetText - boost', (t) => {
199 const env = statusToEnvelope({
200 id: '2',
201 account: { acct: 'bob' },
202 reblog: {
203 id: '1',
204 account: { acct: 'alice' },
205 content: '<p>Original</p>',
206 visibility: 'public',
207 media_attachments: [],
208 tags: []
209 }
210 })
211 const text = statusToKeetText(env)
212 t.ok(text.includes('bob boosted'))
213 t.ok(text.includes('@alice'))
214})
215
216test('statusToKeetText - content warning', (t) => {
217 const env = statusToEnvelope({
218 id: '3',
219 account: { acct: 'carol' },
220 content: '<p>Secret info</p>',
221 spoiler_text: 'Spoiler ahead',
222 visibility: 'public',
223 media_attachments: [],
224 tags: []
225 })
226 const text = statusToKeetText(env)
227 t.ok(text.includes('[CW: Spoiler ahead]'))
228})
229
230test('statusToKeetText - with media', (t) => {
231 const env = statusToEnvelope({
232 id: '4',
233 account: { acct: 'dave' },
234 content: '<p>Look!</p>',
235 visibility: 'public',
236 media_attachments: [{
237 url: 'https://example.com/image.jpg',
238 type: 'image'
239 }],
240 tags: []
241 })
242 const text = statusToKeetText(env)
243 t.ok(text.includes('[image:'))
244 t.ok(text.includes('image.jpg'))
245})
bridge-mastodon/test/router.test.js +77 -0
……@@ -0,0 +1,77 @@
1import test from 'brittle'
2import { Router } from '../lib/router.js'
3
4test('Router - route to Keet', (t) => {
5 let received = null
6 const router = new Router({
7 onKeetMessage: (author, text) => { received = { author, text } }
8 })
9
10 const ok = router.routeToKeet('alice', 'hello')
11 t.ok(ok)
12 t.is(received.author, 'alice')
13 t.is(received.text, 'hello')
14})
15
16test('Router - route to external', (t) => {
17 let received = null
18 const router = new Router({
19 onExternalMessage: (author, text) => { received = { author, text } }
20 })
21
22 const ok = router.routeToExternal('bob', 'world')
23 t.ok(ok)
24 t.is(received.author, 'bob')
25 t.is(received.text, 'world')
26})
27
28test('Router - dedup identical messages', (t) => {
29 let count = 0
30 const router = new Router({
31 onKeetMessage: () => { count++ }
32 })
33
34 router.routeToKeet('alice', 'hello')
35 router.routeToKeet('alice', 'hello')
36 router.routeToKeet('alice', 'hello')
37
38 t.is(count, 1)
39})
40
41test('Router - different messages not deduped', (t) => {
42 let count = 0
43 const router = new Router({
44 onKeetMessage: () => { count++ }
45 })
46
47 router.routeToKeet('alice', 'msg 1')
48 router.routeToKeet('alice', 'msg 2')
49 router.routeToKeet('bob', 'msg 1')
50
51 t.is(count, 3)
52})
53
54test('Router - cross-direction dedup', (t) => {
55 let keetCount = 0
56 let extCount = 0
57 const router = new Router({
58 onKeetMessage: () => { keetCount++ },
59 onExternalMessage: () => { extCount++ }
60 })
61
62 router.routeToKeet('alice', 'echo test')
63 router.routeToExternal('alice', 'echo test')
64
65 t.is(keetCount, 1)
66 t.is(extCount, 0) // blocked by dedup
67})
68
69test('Router - seenCount and clearSeen', (t) => {
70 const router = new Router()
71 router.routeToKeet('a', '1')
72 router.routeToKeet('b', '2')
73 t.is(router.seenCount, 2)
74
75 router.clearSeen()
76 t.is(router.seenCount, 0)
77})
bridge-matrix/lib/adapter.js +81 -0
……@@ -0,0 +1,81 @@
1import { createEnvelope, TYPES, PLATFORMS } from 'bridge-envelope'
2
3// Strip HTML tags (for Matrix formatted_body)
4function stripHtml (html) {
5 return html
6 .replace(/<br\s*\/?>/gi, '\n')
7 .replace(/<\/p>\s*<p>/gi, '\n\n')
8 .replace(/<[^>]+>/g, '')
9 .replace(/&amp;/g, '&')
10 .replace(/&lt;/g, '<')
11 .replace(/&gt;/g, '>')
12 .replace(/&quot;/g, '"')
13 .replace(/&#39;/g, "'")
14 .trim()
15}
16
17// Convert Matrix message to bridge envelope
18function matrixToEnvelope (sender, roomId, text) {
19 return createEnvelope({
20 type: TYPES.TEXT,
21 platform: PLATFORMS.MATRIX,
22 author: sender,
23 content: text,
24 channel: roomId
25 })
26}
27
28// Format a Matrix message for sending to Keet
29function matrixToKeetText (sender, text) {
30 return `[Matrix] <${sender}> ${text}`
31}
32
33// Convert a Keet message to text for Matrix, filtering out bridge echo
34function keetToMatrixText (keetMsg, bridgeUserId) {
35 if (!keetMsg) return null
36
37 // Skip messages from bridge identity (prevents echo)
38 if (keetMsg.memberId === bridgeUserId) return null
39
40 // Skip system messages
41 if (keetMsg.system) return null
42
43 const text = keetMsg.text || ''
44 if (!text.trim()) return null
45
46 // Skip messages already bridged from Matrix
47 if (text.startsWith('[Matrix]')) return null
48
49 const name = keetMsg.name || keetMsg.username || 'anonymous'
50 return { sender: name, text }
51}
52
53// Format a Keet message for display in Matrix
54function keetToMatrixDisplayText (sender, text) {
55 return `<${sender}> ${text}`
56}
57
58// Format envelope for Matrix posting
59function envelopeToMatrixText (envelope) {
60 if (!envelope) return ''
61
62 const platformName = {
63 [PLATFORMS.KEET]: 'Keet',
64 [PLATFORMS.IRC]: 'IRC',
65 [PLATFORMS.DISCORD]: 'Discord',
66 [PLATFORMS.MASTODON]: 'Mastodon',
67 [PLATFORMS.EMAIL]: 'Email'
68 }
69
70 const source = platformName[envelope.platform] || 'P2P'
71 return `[${source}] <${envelope.author}> ${envelope.content}`
72}
73
74export {
75 matrixToEnvelope,
76 matrixToKeetText,
77 keetToMatrixText,
78 keetToMatrixDisplayText,
79 envelopeToMatrixText,
80 stripHtml
81}
bridge-matrix/lib/bridge.js +168 -0
……@@ -0,0 +1,168 @@
1import { MatrixClient } from './matrix-client.js'
2import { KeetClient } from './keet-client.js'
3import { Router } from './router.js'
4import { matrixToKeetText, keetToMatrixText } from './adapter.js'
5
6class Bridge {
7 constructor (opts) {
8 this.matrixHomeserver = opts.matrixHomeserver || 'matrix.org'
9 this.matrixUser = opts.matrixUser
10 this.matrixPassword = opts.matrixPassword
11 this.matrixRoom = opts.matrixRoom
12
13 this.keetRoom = opts.keetRoom
14 this.keetHost = opts.keetHost || 'localhost'
15 this.keetPort = opts.keetPort || 3117
16
17 this._matrix = null
18 this._keet = null
19 this._router = null
20 this._running = false
21 this._bridgeIdentity = null
22 this._lastSeq = -1
23 this._pollTimer = null
24
25 this.onLog = opts.onLog || console.log
26 }
27
28 async start () {
29 this.onLog('[bridge] Starting Matrix <-> Keet bridge')
30 this.onLog(`[bridge] Matrix: ${this.matrixHomeserver} room: ${this.matrixRoom}`)
31 this.onLog(`[bridge] Keet room: ${this.keetRoom}`)
32
33 // Initialize Keet client
34 this._keet = new KeetClient({
35 host: this.keetHost,
36 port: this.keetPort
37 })
38
39 // Get bridge identity so we can filter our own messages
40 try {
41 const identity = await this._keet.identity()
42 this._bridgeIdentity = identity.memberId || identity.id
43 this.onLog(`[bridge] Keet identity: ${identity.displayName || identity.username || 'unknown'}`)
44 } catch (err) {
45 this.onLog(`[bridge] Warning: could not get Keet identity: ${err.message}`)
46 }
47
48 // Snapshot the latest message seq to distinguish history from new
49 try {
50 const msgs = await this._keet.messages(this.keetRoom, 1)
51 if (msgs && msgs.length > 0) {
52 this._lastSeq = msgs[msgs.length - 1].seq || 0
53 this.onLog(`[bridge] History cutoff: seq ${this._lastSeq}`)
54 }
55 } catch (err) {
56 this.onLog(`[bridge] Warning: could not get message history: ${err.message}`)
57 }
58
59 // Initialize Matrix client
60 this._matrix = new MatrixClient({
61 homeserver: this.matrixHomeserver,
62 onMessage: (roomId, sender, text, eventId) => {
63 // Only relay messages from the configured room
64 if (roomId !== this.matrixRoom) return
65
66 this.onLog(`[matrix->keet] <${sender}> ${text}`)
67 const keetText = matrixToKeetText(sender, text)
68 this._router.routeToKeet(sender, keetText)
69 }
70 })
71
72 // Login to Matrix
73 this.onLog('[bridge] Logging in to Matrix...')
74 const loginRes = await this._matrix.login(this.matrixHomeserver, this.matrixUser, this.matrixPassword)
75 this.onLog(`[bridge] Matrix logged in as ${loginRes.user_id}`)
76
77 // Join the Matrix room
78 try {
79 await this._matrix.joinRoom(this.matrixRoom)
80 const roomName = await this._matrix.getRoomName(this.matrixRoom)
81 this.onLog(`[bridge] Joined Matrix room: ${roomName || this.matrixRoom}`)
82 } catch (err) {
83 this.onLog(`[bridge] Warning: join room error: ${err.message}`)
84 }
85
86 // Initialize router with dedup
87 this._router = new Router({
88 onKeetMessage: (author, text) => {
89 this._keet.send(this.keetRoom, text).catch((err) => {
90 this.onLog(`[bridge] Keet send error: ${err.message}`)
91 })
92 },
93 onExternalMessage: (name, text) => {
94 this._matrix.sendMessage(this.matrixRoom, text).catch((err) => {
95 this.onLog(`[bridge] Matrix send error: ${err.message}`)
96 })
97 }
98 })
99
100 // Verify Keet daemon is reachable
101 try {
102 const status = await this._keet.status()
103 this.onLog(`[bridge] Keet daemon online (${status.rooms || '?'} rooms)`)
104 } catch (err) {
105 this.onLog(`[bridge] Warning: Keet daemon unreachable: ${err.message}`)
106 }
107
108 // Start Matrix sync loop
109 this._matrix._startSync()
110
111 // Start polling Keet room for new messages
112 this._startKeetPoll()
113
114 this._running = true
115 this.onLog('[bridge] Bridge active -- messages flowing bidirectionally')
116 }
117
118 _startKeetPoll () {
119 this._pollTimer = setInterval(async () => {
120 try {
121 const msgs = await this._keet.messages(this.keetRoom, 10)
122 if (!msgs || msgs.length === 0) return
123
124 for (const msg of msgs) {
125 if (msg.seq === undefined || msg.seq <= this._lastSeq) continue
126
127 this._lastSeq = msg.seq
128
129 const relayed = keetToMatrixText(msg, this._bridgeIdentity)
130 if (!relayed) continue
131
132 // Skip messages bridged from Matrix (prevents echo)
133 if (msg.text && msg.text.startsWith('[Matrix]')) continue
134
135 const displayText = `<${relayed.sender}> ${relayed.text}`
136 this.onLog(`[keet->matrix] ${displayText}`)
137 this._router.routeToExternal(relayed.sender, displayText)
138 }
139 } catch (err) {
140 this.onLog(`[bridge] Keet poll error: ${err.message}`)
141 }
142 }, 2000)
143 }
144
145 async stop () {
146 this.onLog('[bridge] Shutting down...')
147 this._running = false
148
149 if (this._pollTimer) clearInterval(this._pollTimer)
150 if (this._matrix) this._matrix.stopSync()
151
152 this.onLog('[bridge] Stopped')
153 }
154
155 get running () {
156 return this._running
157 }
158
159 get stats () {
160 return {
161 running: this._running,
162 matrixUser: this._matrix?.userId || null,
163 deduped: this._router?.seenCount || 0
164 }
165 }
166}
167
168export { Bridge }
bridge-matrix/lib/keet-client.js +166 -0
……@@ -0,0 +1,166 @@
1import http from 'http'
2import net from 'net'
3
4class KeetClient {
5 constructor (opts = {}) {
6 this.host = opts.host || 'localhost'
7 this.port = opts.port || 3117
8 this._watching = false
9 this._watchAbort = null
10 }
11
12 // Raw TCP request — bypasses Node's strict HTTP parser
13 // Needed because keet-daemon sends duplicate Content-Length headers
14 _rawRequest (method, path, body) {
15 return new Promise((resolve, reject) => {
16 const socket = net.connect(this.port, this.host)
17 let payload = ''
18
19 if (body) {
20 payload = JSON.stringify(body)
21 }
22
23 socket.on('connect', () => {
24 let req = `${method} ${path} HTTP/1.1\r\n`
25 req += `Host: ${this.host}:${this.port}\r\n`
26 if (payload) {
27 req += `Content-Type: application/json\r\n`
28 req += `Content-Length: ${Buffer.byteLength(payload)}\r\n`
29 }
30 req += `Connection: close\r\n`
31 req += `\r\n`
32 if (payload) req += payload
33 socket.write(req)
34 })
35
36 let data = ''
37 socket.on('data', (chunk) => { data += chunk.toString() })
38
39 socket.on('end', () => {
40 // Split headers from body
41 const idx = data.indexOf('\r\n\r\n')
42 if (idx === -1) return reject(new Error('No HTTP body'))
43 const jsonBody = data.slice(idx + 4)
44
45 try {
46 const parsed = JSON.parse(jsonBody)
47 if (!parsed.ok) reject(new Error(parsed.error || 'RPC error'))
48 else resolve(parsed.data)
49 } catch {
50 reject(new Error('Invalid JSON: ' + jsonBody.slice(0, 200)))
51 }
52 })
53
54 socket.on('error', reject)
55 socket.setTimeout(10000, () => {
56 socket.destroy(new Error('Request timeout'))
57 })
58 })
59 }
60
61 rpc (method, params = {}) {
62 return this._rawRequest('POST', '/rpc', { method, params })
63 }
64
65 async send (roomId, text) {
66 return this.rpc('send', { roomId, text })
67 }
68
69 async messages (roomId, limit = 20) {
70 return this._rawRequest('GET', `/rpc/messages/${roomId}?limit=${limit}`)
71 }
72
73 async rooms () {
74 return this._rawRequest('GET', '/rpc/rooms')
75 }
76
77 async identity () {
78 return this._rawRequest('GET', '/rpc/identity')
79 }
80
81 async status () {
82 return this._rawRequest('GET', '/rpc/status')
83 }
84
85 async members (roomId) {
86 return this._rawRequest('GET', `/rpc/members/${roomId}`)
87 }
88
89 watch (roomId, onMessage) {
90 if (this._watching) return
91
92 this._watching = true
93 this._doWatch(roomId, onMessage)
94 }
95
96 _doWatch (roomId, onMessage) {
97 if (!this._watching) return
98
99 const req = http.request({
100 hostname: this.host,
101 port: this.port,
102 path: `/rpc/watch/${roomId}`,
103 method: 'GET',
104 headers: { Accept: 'text/event-stream' }
105 }, (res) => {
106 let buffer = ''
107
108 res.on('data', (chunk) => {
109 buffer += chunk.toString()
110 const events = buffer.split('\n\n')
111 buffer = events.pop() // keep incomplete tail
112
113 for (const event of events) {
114 if (!event.trim()) continue
115 const lines = event.split('\n')
116 let type = 'message'
117 let data = null
118
119 for (const line of lines) {
120 if (line.startsWith('event: ')) type = line.slice(7)
121 if (line.startsWith('data: ')) {
122 try {
123 data = JSON.parse(line.slice(6))
124 } catch {}
125 }
126 }
127
128 if (type === 'message' && data) {
129 onMessage(data)
130 }
131 }
132 })
133
134 res.on('end', () => {
135 if (this._watching) {
136 setTimeout(() => this._doWatch(roomId, onMessage), 2000)
137 }
138 })
139
140 res.on('error', () => {
141 if (this._watching) {
142 setTimeout(() => this._doWatch(roomId, onMessage), 5000)
143 }
144 })
145 })
146
147 req.on('error', () => {
148 if (this._watching) {
149 setTimeout(() => this._doWatch(roomId, onMessage), 5000)
150 }
151 })
152
153 this._watchAbort = req
154 req.end()
155 }
156
157 stopWatch () {
158 this._watching = false
159 if (this._watchAbort) {
160 this._watchAbort.destroy()
161 this._watchAbort = null
162 }
163 }
164}
165
166export { KeetClient }
bridge-matrix/lib/matrix-client.js +284 -0
……@@ -0,0 +1,284 @@
1import https from 'https'
2import http from 'http'
3
4class MatrixClient {
5 constructor (opts = {}) {
6 this.homeserver = opts.homeserver || 'matrix.org'
7 this.userId = opts.userId || null
8 this.accessToken = opts.accessToken || null
9
10 this._onMessage = opts.onMessage || null
11 this._onJoin = opts.onJoin || null
12 this._onLeave = opts.onLeave || null
13
14 this._nextBatch = null
15 this._syncing = false
16 this._txnId = 0
17 this._displayNameCache = new Map()
18 this._seenEvents = new Set()
19 this._maxSeenEvents = 5000
20 }
21
22 // Authenticate with username/password and get an access token
23 async login (homeserver, user, password) {
24 if (homeserver) this.homeserver = homeserver
25
26 const body = {
27 type: 'm.login.password',
28 identifier: {
29 type: 'm.id.user',
30 user
31 },
32 password
33 }
34
35 const res = await this._request('POST', '/_matrix/client/v3/login', body)
36 this.accessToken = res.access_token
37 this.userId = res.user_id
38 return res
39 }
40
41 // Long-poll the /sync endpoint
42 async sync (since, timeout) {
43 let path = '/_matrix/client/v3/sync?timeout=' + (timeout || 30000)
44 if (since) path += '&since=' + encodeURIComponent(since)
45
46 // Filter to only get room messages (reduce payload)
47 const filter = JSON.stringify({
48 room: {
49 timeline: { limit: 20 },
50 state: { lazy_load_members: true }
51 },
52 presence: { types: [] },
53 account_data: { types: [] }
54 })
55 path += '&filter=' + encodeURIComponent(filter)
56
57 return this._request('GET', path, null, (timeout || 30000) + 15000)
58 }
59
60 // Send a text message to a room
61 async sendMessage (roomId, text) {
62 const txnId = 'm' + Date.now() + '.' + (this._txnId++)
63 const encodedRoom = encodeURIComponent(roomId)
64 const path = `/_matrix/client/v3/rooms/${encodedRoom}/send/m.room.message/${txnId}`
65
66 return this._request('PUT', path, {
67 msgtype: 'm.text',
68 body: text
69 })
70 }
71
72 // Join a room by ID or alias
73 async joinRoom (roomIdOrAlias) {
74 const encoded = encodeURIComponent(roomIdOrAlias)
75 return this._request('POST', `/_matrix/client/v3/join/${encoded}`, {})
76 }
77
78 // Get room display name
79 async getRoomName (roomId) {
80 const encoded = encodeURIComponent(roomId)
81 try {
82 const res = await this._request('GET', `/_matrix/client/v3/rooms/${encoded}/state/m.room.name`)
83 return res.name || null
84 } catch {
85 return null
86 }
87 }
88
89 // Get display name for a user (cached)
90 async getDisplayName (userId) {
91 if (this._displayNameCache.has(userId)) {
92 return this._displayNameCache.get(userId)
93 }
94
95 try {
96 const encoded = encodeURIComponent(userId)
97 const res = await this._request('GET', `/_matrix/client/v3/profile/${encoded}/displayname`)
98 const name = res.displayname || userId
99 this._displayNameCache.set(userId, name)
100 return name
101 } catch {
102 this._displayNameCache.set(userId, userId)
103 return userId
104 }
105 }
106
107 // Start the long-polling sync loop
108 _startSync () {
109 if (this._syncing) return
110 this._syncing = true
111 this._syncLoop()
112 }
113
114 async _syncLoop () {
115 let backoff = 1000
116
117 while (this._syncing) {
118 try {
119 const res = await this.sync(this._nextBatch, 30000)
120 this._nextBatch = res.next_batch
121 backoff = 1000 // reset on success
122
123 // Process joined rooms
124 const joined = res.rooms?.join
125 if (joined) {
126 for (const [roomId, roomData] of Object.entries(joined)) {
127 const events = roomData.timeline?.events || []
128 for (const event of events) {
129 await this._handleEvent(roomId, event)
130 }
131 }
132 }
133
134 // Process invites (auto-join if callback present)
135 const invited = res.rooms?.invite
136 if (invited && this._onJoin) {
137 for (const roomId of Object.keys(invited)) {
138 this._onJoin(roomId)
139 }
140 }
141
142 // Process leaves
143 const left = res.rooms?.leave
144 if (left && this._onLeave) {
145 for (const roomId of Object.keys(left)) {
146 this._onLeave(roomId)
147 }
148 }
149 } catch (err) {
150 if (!this._syncing) break
151
152 // Exponential backoff on error
153 await sleep(backoff)
154 backoff = Math.min(backoff * 2, 30000)
155 }
156 }
157 }
158
159 async _handleEvent (roomId, event) {
160 if (event.type !== 'm.room.message') return
161 if (!event.content) return
162
163 // Skip own messages
164 if (event.sender === this.userId) return
165
166 // Dedup by event_id
167 if (this._seenEvents.has(event.event_id)) return
168
169 // Prune cache when it gets too large
170 if (this._seenEvents.size > this._maxSeenEvents) {
171 const arr = [...this._seenEvents]
172 this._seenEvents = new Set(arr.slice(-Math.floor(this._maxSeenEvents / 2)))
173 }
174 this._seenEvents.add(event.event_id)
175
176 // Extract text — prefer plain body over formatted
177 let text = event.content.body || ''
178
179 // If formatted_body exists and body is the same, use body (plain text)
180 // formatted_body is HTML, body is the plain text fallback
181 if (event.content.format === 'org.matrix.custom.html' && event.content.formatted_body) {
182 // body already contains the plain text version, use it as-is
183 // Only strip HTML if for some reason body is missing
184 if (!text) {
185 text = stripHtml(event.content.formatted_body)
186 }
187 }
188
189 // Get display name
190 const sender = await this.getDisplayName(event.sender)
191
192 if (this._onMessage) {
193 this._onMessage(roomId, sender, text, event.event_id)
194 }
195 }
196
197 // Stop the sync loop
198 stopSync () {
199 this._syncing = false
200 }
201
202 // Generic HTTPS request to the homeserver
203 _request (method, path, body, timeout) {
204 return new Promise((resolve, reject) => {
205 const isHttps = !this.homeserver.includes('://') || this.homeserver.startsWith('https')
206 const mod = isHttps ? https : http
207
208 let hostname = this.homeserver
209 .replace('https://', '')
210 .replace('http://', '')
211
212 // Extract port if present
213 let port = isHttps ? 443 : 80
214 const colonIdx = hostname.indexOf(':')
215 if (colonIdx !== -1) {
216 port = parseInt(hostname.slice(colonIdx + 1), 10)
217 hostname = hostname.slice(0, colonIdx)
218 }
219
220 const headers = {}
221 if (this.accessToken) {
222 headers.Authorization = 'Bearer ' + this.accessToken
223 }
224
225 let payload = null
226 if (body !== null && body !== undefined) {
227 payload = JSON.stringify(body)
228 headers['Content-Type'] = 'application/json'
229 headers['Content-Length'] = Buffer.byteLength(payload)
230 }
231
232 const req = mod.request({
233 hostname,
234 port,
235 path,
236 method,
237 headers
238 }, (res) => {
239 let data = ''
240 res.on('data', (chunk) => { data += chunk.toString() })
241 res.on('end', () => {
242 try {
243 const parsed = JSON.parse(data)
244 if (parsed.errcode) {
245 reject(new Error(`Matrix error: ${parsed.errcode} - ${parsed.error}`))
246 } else {
247 resolve(parsed)
248 }
249 } catch {
250 reject(new Error('Invalid JSON response: ' + data.slice(0, 200)))
251 }
252 })
253 })
254
255 req.on('error', reject)
256 req.setTimeout(timeout || 30000, () => {
257 req.destroy(new Error('Request timeout'))
258 })
259
260 if (payload) req.write(payload)
261 req.end()
262 })
263 }
264}
265
266// Strip HTML tags (for formatted_body fallback)
267function stripHtml (html) {
268 return html
269 .replace(/<br\s*\/?>/gi, '\n')
270 .replace(/<\/p>\s*<p>/gi, '\n\n')
271 .replace(/<[^>]+>/g, '')
272 .replace(/&amp;/g, '&')
273 .replace(/&lt;/g, '<')
274 .replace(/&gt;/g, '>')
275 .replace(/&quot;/g, '"')
276 .replace(/&#39;/g, "'")
277 .trim()
278}
279
280function sleep (ms) {
281 return new Promise(resolve => setTimeout(resolve, ms))
282}
283
284export { MatrixClient, stripHtml }
bridge-matrix/lib/router.js +69 -0
……@@ -0,0 +1,69 @@
1import { createHash } from 'crypto'
2
3class Router {
4 constructor (opts = {}) {
5 this._seen = new Map()
6 this._maxSeen = opts.maxSeen || 10000
7 this._ttl = opts.ttl || 60000
8 this._onExternalMessage = opts.onExternalMessage || null
9 this._onKeetMessage = opts.onKeetMessage || null
10 }
11
12 _hash (text, author) {
13 return createHash('sha256')
14 .update(author || '')
15 .update(text || '')
16 .digest('hex')
17 .slice(0, 16)
18 }
19
20 _isDuplicate (hash) {
21 const now = Date.now()
22
23 if (this._seen.size > this._maxSeen) {
24 for (const [k, ts] of this._seen) {
25 if (now - ts > this._ttl) this._seen.delete(k)
26 }
27 }
28
29 if (this._seen.has(hash)) {
30 const ts = this._seen.get(hash)
31 if (now - ts < this._ttl) return true
32 }
33
34 this._seen.set(hash, now)
35 return false
36 }
37
38 // External platform message -> relay to Keet
39 routeToKeet (author, text) {
40 const hash = this._hash(text, author)
41 if (this._isDuplicate(hash)) return false
42
43 if (this._onKeetMessage) {
44 this._onKeetMessage(author, text)
45 }
46 return true
47 }
48
49 // Keet message -> relay to external platform
50 routeToExternal (name, text) {
51 const hash = this._hash(text, name)
52 if (this._isDuplicate(hash)) return false
53
54 if (this._onExternalMessage) {
55 this._onExternalMessage(name, text)
56 }
57 return true
58 }
59
60 get seenCount () {
61 return this._seen.size
62 }
63
64 clearSeen () {
65 this._seen.clear()
66 }
67}
68
69export { Router }
bridge-matrix/package-lock.json +875 -0
……@@ -0,0 +1,875 @@
1{
2 "name": "bridge-matrix",
3 "version": "1.0.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "bridge-matrix",
9 "version": "1.0.0",
10 "license": "MIT",
11 "dependencies": {
12 "b4a": "^1.0.0",
13 "bridge-envelope": "file:../bridge-envelope"
14 },
15 "devDependencies": {
16 "brittle": "^3.0.0"
17 }
18 },
19 "../bridge-envelope": {
20 "version": "1.0.0",
21 "license": "MIT",
22 "dependencies": {
23 "b4a": "^1.0.0",
24 "compact-encoding": "^2.0.0"
25 },
26 "devDependencies": {
27 "brittle": "^3.0.0"
28 }
29 },
30 "node_modules/@jridgewell/resolve-uri": {
31 "version": "3.1.2",
32 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
33 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
34 "dev": true,
35 "license": "MIT",
36 "engines": {
37 "node": ">=6.0.0"
38 }
39 },
40 "node_modules/@jridgewell/sourcemap-codec": {
41 "version": "1.5.5",
42 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
43 "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
44 "dev": true,
45 "license": "MIT"
46 },
47 "node_modules/@jridgewell/trace-mapping": {
48 "version": "0.3.31",
49 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
50 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
51 "dev": true,
52 "license": "MIT",
53 "dependencies": {
54 "@jridgewell/resolve-uri": "^3.1.0",
55 "@jridgewell/sourcemap-codec": "^1.4.14"
56 }
57 },
58 "node_modules/@types/istanbul-lib-coverage": {
59 "version": "2.0.6",
60 "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
61 "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
62 "dev": true,
63 "license": "MIT"
64 },
65 "node_modules/b4a": {
66 "version": "1.7.4",
67 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.4.tgz",
68 "integrity": "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug==",
69 "license": "Apache-2.0",
70 "peerDependencies": {
71 "react-native-b4a": "*"
72 },
73 "peerDependenciesMeta": {
74 "react-native-b4a": {
75 "optional": true
76 }
77 }
78 },
79 "node_modules/bare-addon-resolve": {
80 "version": "1.10.0",
81 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
82 "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
83 "dev": true,
84 "license": "Apache-2.0",
85 "dependencies": {
86 "bare-module-resolve": "^1.10.0",
87 "bare-semver": "^1.0.0"
88 },
89 "peerDependencies": {
90 "bare-url": "*"
91 },
92 "peerDependenciesMeta": {
93 "bare-url": {
94 "optional": true
95 }
96 }
97 },
98 "node_modules/bare-ansi-escapes": {
99 "version": "2.2.3",
100 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
101 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
102 "dev": true,
103 "license": "Apache-2.0",
104 "dependencies": {
105 "bare-stream": "^2.6.5"
106 },
107 "peerDependencies": {
108 "bare-buffer": "*"
109 },
110 "peerDependenciesMeta": {
111 "bare-buffer": {
112 "optional": true
113 }
114 }
115 },
116 "node_modules/bare-assert": {
117 "version": "1.2.0",
118 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
119 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
120 "dev": true,
121 "license": "Apache-2.0",
122 "dependencies": {
123 "bare-inspect": "^3.1.2"
124 }
125 },
126 "node_modules/bare-bundle": {
127 "version": "1.10.0",
128 "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
129 "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
130 "dev": true,
131 "license": "Apache-2.0",
132 "peerDependencies": {
133 "bare-buffer": "*",
134 "bare-url": "*"
135 },
136 "peerDependenciesMeta": {
137 "bare-buffer": {
138 "optional": true
139 },
140 "bare-url": {
141 "optional": true
142 }
143 }
144 },
145 "node_modules/bare-cov": {
146 "version": "1.2.1",
147 "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz",
148 "integrity": "sha512-SHNqb/WG9udUEj8m5qGSehoUQ9o0Xzv8nrf4HIdmG+KNU/XdhPLhTP5VPrHnSAMAXhAzEB4Q+2IYZ0x9qPzN1g==",
149 "dev": true,
150 "license": "Apache-2.0",
151 "dependencies": {
152 "bare-fs": "^4.1.2",
153 "bare-inspector": "^4.0.1",
154 "bare-path": "^3.0.0",
155 "bare-process": "^4.2.1",
156 "bare-url": "^2.1.5",
157 "bare-v8-to-istanbul": "^1.0.2",
158 "picomatch": "^4.0.2",
159 "which-runtime": "^1.2.1"
160 }
161 },
162 "node_modules/bare-crypto": {
163 "version": "1.13.0",
164 "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.0.tgz",
165 "integrity": "sha512-RQl13yD+YTACWUZHMMck0C6LNjXgGgRyhVC557ag0xIrkxXf4IQwpnc8WOB58f4k5kSzKhIjy05oW3HUBrFpSQ==",
166 "dev": true,
167 "license": "Apache-2.0",
168 "dependencies": {
169 "bare-stream": "^2.6.3"
170 },
171 "peerDependencies": {
172 "bare-buffer": "*"
173 },
174 "peerDependenciesMeta": {
175 "bare-buffer": {
176 "optional": true
177 }
178 }
179 },
180 "node_modules/bare-debug-log": {
181 "version": "2.0.0",
182 "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
183 "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
184 "dev": true,
185 "license": "Apache-2.0",
186 "dependencies": {
187 "bare-os": "^3.0.1"
188 }
189 },
190 "node_modules/bare-dns": {
191 "version": "2.1.4",
192 "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
193 "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==",
194 "dev": true,
195 "license": "Apache-2.0",
196 "engines": {
197 "bare": ">=1.7.0"
198 }
199 },
200 "node_modules/bare-encoding": {
201 "version": "1.0.3",
202 "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
203 "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
204 "dev": true,
205 "license": "Apache-2.0",
206 "peerDependencies": {
207 "bare-buffer": "*"
208 },
209 "peerDependenciesMeta": {
210 "bare-buffer": {
211 "optional": true
212 }
213 }
214 },
215 "node_modules/bare-env": {
216 "version": "3.0.0",
217 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
218 "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
219 "dev": true,
220 "license": "Apache-2.0",
221 "dependencies": {
222 "bare-os": "^3.0.1"
223 }
224 },
225 "node_modules/bare-events": {
226 "version": "2.8.2",
227 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
228 "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
229 "dev": true,
230 "license": "Apache-2.0",
231 "peerDependencies": {
232 "bare-abort-controller": "*"
233 },
234 "peerDependenciesMeta": {
235 "bare-abort-controller": {
236 "optional": true
237 }
238 }
239 },
240 "node_modules/bare-format": {
241 "version": "1.0.1",
242 "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.1.tgz",
243 "integrity": "sha512-1oS+LZrWK6tnYnvNSHDGljc2MPunRxwhpFriuCgzNF+oklrnwmBKD91tS0yt+jpl2j3UgcSDzBIMiVTvLs9A8w==",
244 "dev": true,
245 "license": "Apache-2.0",
246 "dependencies": {
247 "bare-inspect": "^3.0.0"
248 }
249 },
250 "node_modules/bare-fs": {
251 "version": "4.5.4",
252 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz",
253 "integrity": "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==",
254 "dev": true,
255 "license": "Apache-2.0",
256 "dependencies": {
257 "bare-events": "^2.5.4",
258 "bare-path": "^3.0.0",
259 "bare-stream": "^2.6.4",
260 "bare-url": "^2.2.2",
261 "fast-fifo": "^1.3.2"
262 },
263 "engines": {
264 "bare": ">=1.16.0"
265 },
266 "peerDependencies": {
267 "bare-buffer": "*"
268 },
269 "peerDependenciesMeta": {
270 "bare-buffer": {
271 "optional": true
272 }
273 }
274 },
275 "node_modules/bare-hrtime": {
276 "version": "2.1.1",
277 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
278 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
279 "dev": true,
280 "license": "Apache-2.0"
281 },
282 "node_modules/bare-http-parser": {
283 "version": "1.0.1",
284 "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.0.1.tgz",
285 "integrity": "sha512-A3LTDTcELcmNJ3g5liIaS038v/BQxOhA9cjhBESn7eoV7QCuMoIRBKLDadDe08flxyLbxI2f+1l2MZ/5+HnKPA==",
286 "dev": true,
287 "license": "Apache-2.0"
288 },
289 "node_modules/bare-http1": {
290 "version": "4.2.3",
291 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.2.3.tgz",
292 "integrity": "sha512-zyvHF3xzvBJNpuw0QydE9etTn2AyC/TRVDWTo8Ed3LQEEGaFOkv6KHhaJPlf4eWEeTekk41Fowie+fTsXoNxkA==",
293 "dev": true,
294 "license": "Apache-2.0",
295 "dependencies": {
296 "bare-events": "^2.6.0",
297 "bare-http-parser": "^1.0.0",
298 "bare-stream": "^2.3.0",
299 "bare-tcp": "^2.2.0"
300 },
301 "peerDependencies": {
302 "bare-buffer": "*",
303 "bare-url": "*"
304 },
305 "peerDependenciesMeta": {
306 "bare-buffer": {
307 "optional": true
308 },
309 "bare-url": {
310 "optional": true
311 }
312 }
313 },
314 "node_modules/bare-https": {
315 "version": "2.1.2",
316 "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.2.tgz",
317 "integrity": "sha512-Q+TTydUDsuKQJvh8dX2dvOXCR9fM3xR5TBmKaFrs5p7Lj7XbKX7v4vIUJ36H0SXg2xCOQxXKIbjwrLg5tfJNYg==",
318 "dev": true,
319 "license": "Apache-2.0",
320 "dependencies": {
321 "bare-http1": "^4.0.0",
322 "bare-tcp": "^2.2.0",
323 "bare-tls": "^2.0.0"
324 }
325 },
326 "node_modules/bare-inspect": {
327 "version": "3.1.4",
328 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
329 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
330 "dev": true,
331 "license": "Apache-2.0",
332 "dependencies": {
333 "bare-ansi-escapes": "^2.1.0",
334 "bare-type": "^1.0.0"
335 },
336 "engines": {
337 "bare": ">=1.18.0"
338 }
339 },
340 "node_modules/bare-inspector": {
341 "version": "4.0.3",
342 "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-4.0.3.tgz",
343 "integrity": "sha512-yYk6+7tTVKJKHOja5+QBn989ETv8oVsmwoH0nCM+QYC6w5MC03GC4aQNK+lipAZSz1EaAj42WZ8Wewjpy0L9TA==",
344 "dev": true,
345 "license": "Apache-2.0",
346 "dependencies": {
347 "bare-events": "^2.1.0",
348 "bare-http1": "^4.0.0",
349 "bare-stream": "^2.0.0",
350 "bare-url": "^2.0.0",
351 "bare-ws": "^2.0.0"
352 },
353 "engines": {
354 "bare": ">=1.2.0"
355 },
356 "peerDependencies": {
357 "bare-tcp": "*"
358 },
359 "peerDependenciesMeta": {
360 "bare-tcp": {
361 "optional": true
362 }
363 }
364 },
365 "node_modules/bare-module": {
366 "version": "6.1.3",
367 "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz",
368 "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==",
369 "dev": true,
370 "license": "Apache-2.0",
371 "dependencies": {
372 "bare-bundle": "^1.3.0",
373 "bare-module-lexer": "^1.0.0",
374 "bare-module-resolve": "^1.8.0",
375 "bare-path": "^3.0.0",
376 "bare-url": "^2.0.1"
377 },
378 "engines": {
379 "bare": ">=1.23.0"
380 },
381 "peerDependencies": {
382 "bare-buffer": "*"
383 },
384 "peerDependenciesMeta": {
385 "bare-buffer": {
386 "optional": true
387 }
388 }
389 },
390 "node_modules/bare-module-lexer": {
391 "version": "1.4.7",
392 "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
393 "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
394 "dev": true,
395 "license": "Apache-2.0",
396 "dependencies": {
397 "require-addon": "^1.0.2"
398 },
399 "peerDependencies": {
400 "bare-buffer": "*"
401 },
402 "peerDependenciesMeta": {
403 "bare-buffer": {
404 "optional": true
405 }
406 }
407 },
408 "node_modules/bare-module-resolve": {
409 "version": "1.12.1",
410 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz",
411 "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==",
412 "dev": true,
413 "license": "Apache-2.0",
414 "dependencies": {
415 "bare-semver": "^1.0.0"
416 },
417 "peerDependencies": {
418 "bare-url": "*"
419 },
420 "peerDependenciesMeta": {
421 "bare-url": {
422 "optional": true
423 }
424 }
425 },
426 "node_modules/bare-net": {
427 "version": "2.2.0",
428 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.2.0.tgz",
429 "integrity": "sha512-UF7cAbHsGE+H6uEqWF5IULBow1x58chZz4g3ALgHtv7wZsFcCbRDt0JKWEumf5Oma3QWS1Q6aLi0Rpll8RElMg==",
430 "dev": true,
431 "license": "Apache-2.0",
432 "dependencies": {
433 "bare-events": "^2.2.2",
434 "bare-pipe": "^4.0.0",
435 "bare-stream": "^2.0.0",
436 "bare-tcp": "^2.0.0"
437 }
438 },
439 "node_modules/bare-os": {
440 "version": "3.6.2",
441 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
442 "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
443 "dev": true,
444 "license": "Apache-2.0",
445 "engines": {
446 "bare": ">=1.14.0"
447 }
448 },
449 "node_modules/bare-path": {
450 "version": "3.0.0",
451 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
452 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
453 "dev": true,
454 "license": "Apache-2.0",
455 "dependencies": {
456 "bare-os": "^3.0.1"
457 }
458 },
459 "node_modules/bare-pipe": {
460 "version": "4.1.3",
461 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.3.tgz",
462 "integrity": "sha512-DqQsx93rAzre6yJ9T6l/Vgh+X+bntkVMB1X5ggtXjXtqtMmF2Y2RVlCzxxy/09R6yeR9FSWBEUIaMYJL1/5VDA==",
463 "dev": true,
464 "license": "Apache-2.0",
465 "dependencies": {
466 "bare-events": "^2.0.0",
467 "bare-stream": "^2.0.0"
468 },
469 "engines": {
470 "bare": ">=1.16.0"
471 }
472 },
473 "node_modules/bare-process": {
474 "version": "4.2.2",
475 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.2.2.tgz",
476 "integrity": "sha512-ikzEw+HGLB+2lS/WLVEsqi8BXBwzleG3n7DYI0v6/YNklvNabOIGlLd1dof+7Jw5ob4jsBRPtyflweVahY7rFA==",
477 "dev": true,
478 "license": "Apache-2.0",
479 "dependencies": {
480 "bare-env": "^3.0.0",
481 "bare-events": "^2.3.1",
482 "bare-hrtime": "^2.0.0",
483 "bare-os": "^3.5.0",
484 "bare-pipe": "^4.0.0",
485 "bare-signals": "^4.0.0",
486 "bare-tty": "^5.0.0"
487 }
488 },
489 "node_modules/bare-semver": {
490 "version": "1.0.2",
491 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz",
492 "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==",
493 "dev": true,
494 "license": "Apache-2.0"
495 },
496 "node_modules/bare-signals": {
497 "version": "4.2.0",
498 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz",
499 "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==",
500 "dev": true,
501 "license": "Apache-2.0",
502 "dependencies": {
503 "bare-events": "^2.5.3",
504 "bare-os": "^3.3.1"
505 },
506 "engines": {
507 "bare": ">=1.7.0"
508 }
509 },
510 "node_modules/bare-stream": {
511 "version": "2.7.0",
512 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
513 "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
514 "dev": true,
515 "license": "Apache-2.0",
516 "dependencies": {
517 "streamx": "^2.21.0"
518 },
519 "peerDependencies": {
520 "bare-buffer": "*",
521 "bare-events": "*"
522 },
523 "peerDependenciesMeta": {
524 "bare-buffer": {
525 "optional": true
526 },
527 "bare-events": {
528 "optional": true
529 }
530 }
531 },
532 "node_modules/bare-subprocess": {
533 "version": "5.2.2",
534 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.2.tgz",
535 "integrity": "sha512-L6oXgQ1aWs25RtG5Ky0bDD06p3RAcVVrDDMWb1DfXpHtyEWxamcyIWUbSykxMTWrpLlmSj6ytbb6yoKehGFfmw==",
536 "dev": true,
537 "license": "Apache-2.0",
538 "dependencies": {
539 "bare-env": "^3.0.0",
540 "bare-events": "^2.5.4",
541 "bare-os": "^3.0.1",
542 "bare-pipe": "^4.0.0",
543 "bare-url": "^2.2.2"
544 },
545 "engines": {
546 "bare": ">=1.7.0"
547 },
548 "peerDependencies": {
549 "bare-buffer": "*"
550 },
551 "peerDependenciesMeta": {
552 "bare-buffer": {
553 "optional": true
554 }
555 }
556 },
557 "node_modules/bare-tcp": {
558 "version": "2.2.4",
559 "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.4.tgz",
560 "integrity": "sha512-vGjfMkyhS1lxp+Y4+gXRDR1ijZMK2cKx0RBwtYKUuZbD90ARCoM6bAiTa1HBztumCIC+NXb8eXdMFXyuYwlPyw==",
561 "dev": true,
562 "license": "Apache-2.0",
563 "dependencies": {
564 "bare-dns": "^2.0.4",
565 "bare-events": "^2.5.4",
566 "bare-stream": "^2.6.4"
567 },
568 "engines": {
569 "bare": ">=1.16.0"
570 }
571 },
572 "node_modules/bare-tls": {
573 "version": "2.1.7",
574 "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.1.7.tgz",
575 "integrity": "sha512-h6wcNXQdBeTX7fed9tjPp0/9cA/QfcBTv3ItgjnbUk4rWAU8bEFalZCZnUDdCK/t9zrNfJ+yvcPx4D/1Y6biyA==",
576 "dev": true,
577 "license": "Apache-2.0",
578 "dependencies": {
579 "bare-net": "^2.0.1",
580 "bare-stream": "^2.6.4"
581 },
582 "engines": {
583 "bare": ">=1.7.0"
584 }
585 },
586 "node_modules/bare-tty": {
587 "version": "5.0.3",
588 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.0.3.tgz",
589 "integrity": "sha512-jW24RBWRZOMHuSEWC9mh8wUKO5WUNl0UWUTNPn3yZ8qkOqwa8vaV2dR3a+BvblONPk7V35wuvK9P8508+judAg==",
590 "dev": true,
591 "license": "Apache-2.0",
592 "dependencies": {
593 "bare-events": "^2.2.0",
594 "bare-signals": "^4.0.0",
595 "bare-stream": "^2.0.0"
596 },
597 "engines": {
598 "bare": ">=1.16.0"
599 }
600 },
601 "node_modules/bare-type": {
602 "version": "1.1.0",
603 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
604 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
605 "dev": true,
606 "license": "Apache-2.0",
607 "engines": {
608 "bare": ">=1.2.0"
609 }
610 },
611 "node_modules/bare-url": {
612 "version": "2.3.2",
613 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
614 "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
615 "dev": true,
616 "license": "Apache-2.0",
617 "peer": true,
618 "dependencies": {
619 "bare-path": "^3.0.0"
620 }
621 },
622 "node_modules/bare-utils": {
623 "version": "1.5.1",
624 "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.5.1.tgz",
625 "integrity": "sha512-mxCkFvmDU3mlD/mb+pT64kKXOsx2KMsWLQbngN1LB+NOXfhfnRnyvpy3VZc6m7gzQxe57Bsi+aTCBqA4/S3elQ==",
626 "dev": true,
627 "license": "Apache-2.0",
628 "dependencies": {
629 "bare-debug-log": "^2.0.0",
630 "bare-encoding": "^1.0.0",
631 "bare-format": "^1.0.0",
632 "bare-inspect": "^3.0.0",
633 "bare-type": "^1.0.6"
634 }
635 },
636 "node_modules/bare-v8-to-istanbul": {
637 "version": "1.0.2",
638 "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
639 "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==",
640 "dev": true,
641 "license": "Apache-2.0",
642 "dependencies": {
643 "bare-assert": "^1.0.2",
644 "bare-fs": "^4.1.2",
645 "bare-module": "^6.1.2",
646 "bare-path": "^3.0.0",
647 "bare-process": "^4.2.0",
648 "bare-url": "^2.1.5",
649 "bare-utils": "^1.2.0",
650 "v8-to-istanbul": "^9.3.0",
651 "which-runtime": "^1.2.1"
652 }
653 },
654 "node_modules/bare-ws": {
655 "version": "2.0.4",
656 "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.0.4.tgz",
657 "integrity": "sha512-SQMXzBYna9dRj57Dz1/ag+VWHCRXbfCjMHgyfM2F2lhkVLzMjnVSZP72aVeFWPFqe494Rd70Kzhe2JElGwFlJQ==",
658 "dev": true,
659 "license": "Apache-2.0",
660 "dependencies": {
661 "bare-crypto": "^1.2.0",
662 "bare-events": "^2.3.1",
663 "bare-http1": "^4.0.0",
664 "bare-https": "^2.0.0",
665 "bare-stream": "^2.1.2"
666 },
667 "peerDependencies": {
668 "bare-buffer": "*",
669 "bare-url": "*"
670 },
671 "peerDependenciesMeta": {
672 "bare-buffer": {
673 "optional": true
674 },
675 "bare-url": {
676 "optional": true
677 }
678 }
679 },
680 "node_modules/bridge-envelope": {
681 "resolved": "../bridge-envelope",
682 "link": true
683 },
684 "node_modules/brittle": {
685 "version": "3.19.1",
686 "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz",
687 "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==",
688 "dev": true,
689 "license": "Apache-2.0",
690 "dependencies": {
691 "b4a": "^1.6.0",
692 "bare-assert": "^1.0.2",
693 "bare-cov": "^1.1.0",
694 "bare-fs": "^4.1.6",
695 "bare-os": "^3.6.1",
696 "bare-path": "^3.0.0",
697 "bare-process": "^4.2.1",
698 "bare-subprocess": "^5.0.0",
699 "bare-url": "^2.1.6",
700 "error-stack-parser": "^2.1.4",
701 "globbie": "^1.0.2",
702 "paparam": "^1.6.2",
703 "same-object": "^1.0.2",
704 "test-tmp": "^1.4.0",
705 "tmatch": "^5.0.0"
706 },
707 "bin": {
708 "brittle": "bin/node.js",
709 "brittle-bare": "bin/bare.js",
710 "brittle-node": "bin/node.js",
711 "brittle-pear": "bin/pear.js"
712 }
713 },
714 "node_modules/convert-source-map": {
715 "version": "2.0.0",
716 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
717 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
718 "dev": true,
719 "license": "MIT"
720 },
721 "node_modules/error-stack-parser": {
722 "version": "2.1.4",
723 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
724 "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
725 "dev": true,
726 "license": "MIT",
727 "dependencies": {
728 "stackframe": "^1.3.4"
729 }
730 },
731 "node_modules/events-universal": {
732 "version": "1.0.1",
733 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
734 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
735 "dev": true,
736 "license": "Apache-2.0",
737 "dependencies": {
738 "bare-events": "^2.7.0"
739 }
740 },
741 "node_modules/fast-fifo": {
742 "version": "1.3.2",
743 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
744 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
745 "dev": true,
746 "license": "MIT"
747 },
748 "node_modules/globbie": {
749 "version": "1.0.3",
750 "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz",
751 "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==",
752 "dev": true,
753 "license": "Apache-2.0",
754 "dependencies": {
755 "bare-fs": "^4.1.2",
756 "bare-path": "^3.0.0",
757 "bare-process": "^4.2.1",
758 "picomatch": "^4.0.2"
759 }
760 },
761 "node_modules/paparam": {
762 "version": "1.10.0",
763 "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.0.tgz",
764 "integrity": "sha512-p36QQrwU3X6fj5d2JN20B4lcZI/O3fxMVUHc1xD7MNe5bcOf3jIgE0CIK5Zv/s5YAUGlobTJ/kKXrh1FJVrs3w==",
765 "dev": true,
766 "license": "Apache-2.0"
767 },
768 "node_modules/picomatch": {
769 "version": "4.0.3",
770 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
771 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
772 "dev": true,
773 "license": "MIT",
774 "engines": {
775 "node": ">=12"
776 },
777 "funding": {
778 "url": "https://github.com/sponsors/jonschlinkert"
779 }
780 },
781 "node_modules/require-addon": {
782 "version": "1.2.0",
783 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
784 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
785 "dev": true,
786 "license": "Apache-2.0",
787 "dependencies": {
788 "bare-addon-resolve": "^1.3.0"
789 },
790 "engines": {
791 "bare": ">=1.10.0"
792 }
793 },
794 "node_modules/same-object": {
795 "version": "1.0.2",
796 "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz",
797 "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==",
798 "dev": true,
799 "license": "MIT"
800 },
801 "node_modules/stackframe": {
802 "version": "1.3.4",
803 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
804 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
805 "dev": true,
806 "license": "MIT"
807 },
808 "node_modules/streamx": {
809 "version": "2.23.0",
810 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
811 "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
812 "dev": true,
813 "license": "MIT",
814 "dependencies": {
815 "events-universal": "^1.0.0",
816 "fast-fifo": "^1.3.2",
817 "text-decoder": "^1.1.0"
818 }
819 },
820 "node_modules/test-tmp": {
821 "version": "1.4.0",
822 "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz",
823 "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==",
824 "dev": true,
825 "license": "MIT",
826 "dependencies": {
827 "bare-fs": "^4.0.1",
828 "bare-os": "^3.3.0",
829 "bare-path": "^3.0.0"
830 }
831 },
832 "node_modules/text-decoder": {
833 "version": "1.2.6",
834 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.6.tgz",
835 "integrity": "sha512-27FeW5GQFDfw0FpwMQhMagB7BztOOlmjcSRi97t2oplhKVTZtp0DZbSegSaXS5IIC6mxMvBG4AR1Sgc6BX3CQg==",
836 "dev": true,
837 "license": "Apache-2.0",
838 "dependencies": {
839 "b4a": "^1.6.4"
840 }
841 },
842 "node_modules/tmatch": {
843 "version": "5.0.0",
844 "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz",
845 "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==",
846 "dev": true,
847 "license": "ISC",
848 "engines": {
849 "node": ">=8"
850 }
851 },
852 "node_modules/v8-to-istanbul": {
853 "version": "9.3.0",
854 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
855 "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
856 "dev": true,
857 "license": "ISC",
858 "dependencies": {
859 "@jridgewell/trace-mapping": "^0.3.12",
860 "@types/istanbul-lib-coverage": "^2.0.1",
861 "convert-source-map": "^2.0.0"
862 },
863 "engines": {
864 "node": ">=10.12.0"
865 }
866 },
867 "node_modules/which-runtime": {
868 "version": "1.3.2",
869 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
870 "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
871 "dev": true,
872 "license": "Apache-2.0"
873 }
874 }
875}
bridge-matrix/package.json +22 -0
……@@ -0,0 +1,22 @@
1{
2 "name": "bridge-matrix",
3 "version": "1.0.0",
4 "description": "Matrix ↔ Keet bidirectional bridge",
5 "type": "module",
6 "main": "lib/bridge.js",
7 "pear": {
8 "name": "bridge-matrix",
9 "type": "terminal"
10 },
11 "scripts": {
12 "test": "brittle test/*.test.js"
13 },
14 "dependencies": {
15 "bridge-envelope": "file:../bridge-envelope",
16 "b4a": "^1.0.0"
17 },
18 "devDependencies": {
19 "brittle": "^3.0.0"
20 },
21 "license": "MIT"
22}
bridge-matrix/test/adapter.test.js +156 -0
……@@ -0,0 +1,156 @@
1import test from 'brittle'
2import { matrixToKeetText, keetToMatrixText, matrixToEnvelope, envelopeToMatrixText, stripHtml } from '../lib/adapter.js'
3import { TYPES, PLATFORMS } from 'bridge-envelope'
4
5test('matrixToKeetText - formats correctly', (t) => {
6 const result = matrixToKeetText('Alice', 'Hello world!')
7 t.is(result, '[Matrix] <Alice> Hello world!')
8})
9
10test('matrixToKeetText - preserves unicode', (t) => {
11 const result = matrixToKeetText('Bob', 'Hej hej!')
12 t.is(result, '[Matrix] <Bob> Hej hej!')
13})
14
15test('matrixToKeetText - empty text', (t) => {
16 const result = matrixToKeetText('user', '')
17 t.is(result, '[Matrix] <user> ')
18})
19
20test('keetToMatrixText - normal message', (t) => {
21 const msg = {
22 memberId: 'member-123',
23 name: 'fleeky',
24 text: 'Hello from Keet!'
25 }
26
27 const result = keetToMatrixText(msg, 'bridge-id')
28 t.ok(result)
29 t.is(result.sender, 'fleeky')
30 t.is(result.text, 'Hello from Keet!')
31})
32
33test('keetToMatrixText - skips bridge identity', (t) => {
34 const msg = {
35 memberId: 'bridge-id',
36 name: 'clode',
37 text: 'echoed message'
38 }
39
40 const result = keetToMatrixText(msg, 'bridge-id')
41 t.is(result, null, 'skips bridge identity messages')
42})
43
44test('keetToMatrixText - skips already-bridged messages', (t) => {
45 const msg = {
46 memberId: 'member-456',
47 name: 'someone',
48 text: '[Matrix] <Alice> hello'
49 }
50
51 const result = keetToMatrixText(msg, 'bridge-id')
52 t.is(result, null, 'skips already-bridged Matrix messages')
53})
54
55test('keetToMatrixText - skips system messages', (t) => {
56 const msg = {
57 memberId: 'member-789',
58 name: 'System',
59 text: 'User joined',
60 system: { type: 'join' }
61 }
62
63 const result = keetToMatrixText(msg, 'bridge-id')
64 t.is(result, null, 'skips system messages')
65})
66
67test('keetToMatrixText - skips empty text', (t) => {
68 const msg = {
69 memberId: 'member-abc',
70 name: 'User',
71 text: ' '
72 }
73
74 const result = keetToMatrixText(msg, 'bridge-id')
75 t.is(result, null)
76})
77
78test('keetToMatrixText - null input', (t) => {
79 const result = keetToMatrixText(null, 'bridge-id')
80 t.is(result, null)
81})
82
83test('keetToMatrixText - anonymous if no name', (t) => {
84 const msg = {
85 memberId: 'member-def',
86 text: 'hello'
87 }
88
89 const result = keetToMatrixText(msg, 'bridge-id')
90 t.is(result.sender, 'anonymous')
91})
92
93test('keetToMatrixText - uses username fallback', (t) => {
94 const msg = {
95 memberId: 'member-ghi',
96 username: 'user42',
97 text: 'hello'
98 }
99
100 const result = keetToMatrixText(msg, 'bridge-id')
101 t.is(result.sender, 'user42')
102})
103
104test('stripHtml - basic tags', (t) => {
105 t.is(stripHtml('<p>Hello</p>'), 'Hello')
106 t.is(stripHtml('<p>Line 1</p><p>Line 2</p>'), 'Line 1\n\nLine 2')
107 t.is(stripHtml('Hello<br/>World'), 'Hello\nWorld')
108 t.is(stripHtml('Hello<br>World'), 'Hello\nWorld')
109})
110
111test('stripHtml - entities', (t) => {
112 t.is(stripHtml('&amp; &lt; &gt; &quot; &#39;'), '& < > " \'')
113})
114
115test('stripHtml - nested tags', (t) => {
116 t.is(stripHtml('<p>Hello <a href="https://example.com">link</a></p>'), 'Hello link')
117 t.is(stripHtml('<p><span class="mention">@user</span> hi</p>'), '@user hi')
118})
119
120test('stripHtml - Matrix mention format', (t) => {
121 const html = '<a href="https://matrix.to/#/@alice:matrix.org">Alice</a> said hi'
122 t.is(stripHtml(html), 'Alice said hi')
123})
124
125test('matrixToEnvelope - creates correct envelope', (t) => {
126 const env = matrixToEnvelope('Alice', '!room:matrix.org', 'Hello!')
127
128 t.is(env.type, TYPES.TEXT)
129 t.is(env.platform, PLATFORMS.MATRIX)
130 t.is(env.author, 'Alice')
131 t.is(env.content, 'Hello!')
132 t.is(env.channel, '!room:matrix.org')
133 t.ok(env.id, 'has id')
134})
135
136test('envelopeToMatrixText - from Keet', (t) => {
137 const env = {
138 platform: PLATFORMS.KEET,
139 author: 'fleeky',
140 content: 'Hello world'
141 }
142 t.is(envelopeToMatrixText(env), '[Keet] <fleeky> Hello world')
143})
144
145test('envelopeToMatrixText - from IRC', (t) => {
146 const env = {
147 platform: PLATFORMS.IRC,
148 author: 'nick',
149 content: 'IRC message'
150 }
151 t.is(envelopeToMatrixText(env), '[IRC] <nick> IRC message')
152})
153
154test('envelopeToMatrixText - null envelope', (t) => {
155 t.is(envelopeToMatrixText(null), '')
156})
bridge-matrix/test/matrix-client.test.js +276 -0
……@@ -0,0 +1,276 @@
1import test from 'brittle'
2import { MatrixClient, stripHtml } from '../lib/matrix-client.js'
3
4test('MatrixClient constructor - defaults', (t) => {
5 const client = new MatrixClient()
6 t.is(client.homeserver, 'matrix.org')
7 t.is(client.userId, null)
8 t.is(client.accessToken, null)
9 t.is(client._syncing, false)
10 t.is(client._txnId, 0)
11})
12
13test('MatrixClient constructor - custom options', (t) => {
14 const onMsg = () => {}
15 const client = new MatrixClient({
16 homeserver: 'example.org',
17 userId: '@bot:example.org',
18 accessToken: 'syt_token123',
19 onMessage: onMsg
20 })
21 t.is(client.homeserver, 'example.org')
22 t.is(client.userId, '@bot:example.org')
23 t.is(client.accessToken, 'syt_token123')
24 t.is(client._onMessage, onMsg)
25})
26
27test('MatrixClient - stopSync sets flag', (t) => {
28 const client = new MatrixClient()
29 client._syncing = true
30 client.stopSync()
31 t.is(client._syncing, false)
32})
33
34test('MatrixClient - display name cache', async (t) => {
35 const client = new MatrixClient()
36
37 // Pre-populate cache
38 client._displayNameCache.set('@alice:matrix.org', 'Alice')
39
40 const name = await client.getDisplayName('@alice:matrix.org')
41 t.is(name, 'Alice')
42})
43
44test('MatrixClient - event dedup', (t) => {
45 const client = new MatrixClient()
46
47 client._seenEvents.add('$event1')
48 client._seenEvents.add('$event2')
49
50 t.is(client._seenEvents.has('$event1'), true)
51 t.is(client._seenEvents.has('$event3'), false)
52 t.is(client._seenEvents.size, 2)
53})
54
55test('MatrixClient - event dedup prunes at limit', (t) => {
56 const client = new MatrixClient()
57 client._maxSeenEvents = 10
58
59 // Fill past limit
60 for (let i = 0; i < 15; i++) {
61 client._seenEvents.add('$event' + i)
62 }
63
64 t.is(client._seenEvents.size, 15) // not pruned until _handleEvent
65})
66
67test('MatrixClient - _handleEvent skips own messages', async (t) => {
68 let called = false
69 const client = new MatrixClient({
70 userId: '@bot:matrix.org',
71 onMessage: () => { called = true }
72 })
73
74 await client._handleEvent('!room:matrix.org', {
75 type: 'm.room.message',
76 sender: '@bot:matrix.org',
77 event_id: '$own1',
78 content: { msgtype: 'm.text', body: 'my own msg' }
79 })
80
81 t.is(called, false, 'should not relay own messages')
82})
83
84test('MatrixClient - _handleEvent relays others messages', async (t) => {
85 let received = null
86 const client = new MatrixClient({
87 userId: '@bot:matrix.org',
88 onMessage: (roomId, sender, text, eventId) => {
89 received = { roomId, sender, text, eventId }
90 }
91 })
92
93 // Pre-populate display name cache
94 client._displayNameCache.set('@alice:matrix.org', 'Alice')
95
96 await client._handleEvent('!room:matrix.org', {
97 type: 'm.room.message',
98 sender: '@alice:matrix.org',
99 event_id: '$msg1',
100 content: { msgtype: 'm.text', body: 'Hello!' }
101 })
102
103 t.ok(received)
104 t.is(received.roomId, '!room:matrix.org')
105 t.is(received.sender, 'Alice')
106 t.is(received.text, 'Hello!')
107 t.is(received.eventId, '$msg1')
108})
109
110test('MatrixClient - _handleEvent dedup by event_id', async (t) => {
111 let count = 0
112 const client = new MatrixClient({
113 userId: '@bot:matrix.org',
114 onMessage: () => { count++ }
115 })
116 client._displayNameCache.set('@alice:matrix.org', 'Alice')
117
118 const event = {
119 type: 'm.room.message',
120 sender: '@alice:matrix.org',
121 event_id: '$dup1',
122 content: { msgtype: 'm.text', body: 'Hello!' }
123 }
124
125 await client._handleEvent('!room:matrix.org', event)
126 await client._handleEvent('!room:matrix.org', event)
127 await client._handleEvent('!room:matrix.org', event)
128
129 t.is(count, 1, 'only relayed once despite 3 deliveries')
130})
131
132test('MatrixClient - _handleEvent ignores non-message events', async (t) => {
133 let called = false
134 const client = new MatrixClient({
135 userId: '@bot:matrix.org',
136 onMessage: () => { called = true }
137 })
138
139 await client._handleEvent('!room:matrix.org', {
140 type: 'm.room.member',
141 sender: '@alice:matrix.org',
142 event_id: '$state1',
143 content: { membership: 'join' }
144 })
145
146 t.is(called, false, 'should ignore state events')
147})
148
149test('MatrixClient - _handleEvent ignores events with no content', async (t) => {
150 let called = false
151 const client = new MatrixClient({
152 userId: '@bot:matrix.org',
153 onMessage: () => { called = true }
154 })
155
156 await client._handleEvent('!room:matrix.org', {
157 type: 'm.room.message',
158 sender: '@alice:matrix.org',
159 event_id: '$nocontent1',
160 content: null
161 })
162
163 t.is(called, false, 'should ignore events with no content')
164})
165
166test('MatrixClient - _handleEvent uses plain body over formatted', async (t) => {
167 let received = null
168 const client = new MatrixClient({
169 userId: '@bot:matrix.org',
170 onMessage: (roomId, sender, text) => { received = text }
171 })
172 client._displayNameCache.set('@alice:matrix.org', 'Alice')
173
174 await client._handleEvent('!room:matrix.org', {
175 type: 'm.room.message',
176 sender: '@alice:matrix.org',
177 event_id: '$formatted1',
178 content: {
179 msgtype: 'm.text',
180 body: 'Hello **world**',
181 format: 'org.matrix.custom.html',
182 formatted_body: 'Hello <strong>world</strong>'
183 }
184 })
185
186 t.is(received, 'Hello **world**', 'uses plain body text, not HTML')
187})
188
189test('MatrixClient - txnId increments', (t) => {
190 const client = new MatrixClient()
191 t.is(client._txnId, 0)
192 // Note: can't easily test sendMessage without a server, but
193 // verify the counter is accessible
194 client._txnId++
195 t.is(client._txnId, 1)
196})
197
198test('stripHtml in matrix-client module', (t) => {
199 t.is(stripHtml('<p>Hello</p>'), 'Hello')
200 t.is(stripHtml('&amp; &lt;'), '& <')
201 t.is(stripHtml('Hello<br>World'), 'Hello\nWorld')
202})
203
204test('sync response parsing - extract messages from rooms.join', async (t) => {
205 // Simulate what _handleEvent would do with a sync response
206 const messages = []
207 const client = new MatrixClient({
208 userId: '@bot:matrix.org',
209 onMessage: (roomId, sender, text, eventId) => {
210 messages.push({ roomId, sender, text, eventId })
211 }
212 })
213 client._displayNameCache.set('@alice:matrix.org', 'Alice')
214 client._displayNameCache.set('@bob:matrix.org', 'Bob')
215
216 // Simulate sync response structure
217 const syncResponse = {
218 next_batch: 's1234',
219 rooms: {
220 join: {
221 '!room:matrix.org': {
222 timeline: {
223 events: [
224 {
225 type: 'm.room.message',
226 sender: '@alice:matrix.org',
227 event_id: '$ev1',
228 content: { msgtype: 'm.text', body: 'First message' }
229 },
230 {
231 type: 'm.room.message',
232 sender: '@bob:matrix.org',
233 event_id: '$ev2',
234 content: { msgtype: 'm.text', body: 'Second message' }
235 }
236 ]
237 }
238 }
239 }
240 }
241 }
242
243 // Process like the sync loop would
244 const joined = syncResponse.rooms.join
245 for (const [roomId, roomData] of Object.entries(joined)) {
246 const events = roomData.timeline?.events || []
247 for (const event of events) {
248 await client._handleEvent(roomId, event)
249 }
250 }
251
252 t.is(messages.length, 2)
253 t.is(messages[0].sender, 'Alice')
254 t.is(messages[0].text, 'First message')
255 t.is(messages[1].sender, 'Bob')
256 t.is(messages[1].text, 'Second message')
257})
258
259test('login response parsing', (t) => {
260 // Verify the client can process a login response structure
261 const client = new MatrixClient()
262
263 // Simulate what login() does with the response
264 const loginResponse = {
265 user_id: '@bot:matrix.org',
266 access_token: 'syt_long_token_here',
267 device_id: 'ABCDEF',
268 home_server: 'matrix.org'
269 }
270
271 client.accessToken = loginResponse.access_token
272 client.userId = loginResponse.user_id
273
274 t.is(client.accessToken, 'syt_long_token_here')
275 t.is(client.userId, '@bot:matrix.org')
276})
bridge-vault/index.js +4 -0
……@@ -0,0 +1,4 @@
1import { Vault } from './lib/vault.js'
2import { generateKey, deriveKey } from './lib/crypto.js'
3
4export { Vault, generateKey, deriveKey }
bridge-vault/lib/crypto.js +58 -0
……@@ -0,0 +1,58 @@
1import sodium from 'sodium-native'
2import b4a from 'b4a'
3
4const NONCE_BYTES = sodium.crypto_secretbox_NONCEBYTES
5const KEY_BYTES = sodium.crypto_secretbox_KEYBYTES
6const MAC_BYTES = sodium.crypto_secretbox_MACBYTES
7
8function generateKey () {
9 const key = b4a.allocUnsafe(KEY_BYTES)
10 sodium.randombytes_buf(key)
11 return key
12}
13
14function deriveKey (password, salt) {
15 if (!salt) {
16 salt = b4a.allocUnsafe(sodium.crypto_pwhash_SALTBYTES)
17 sodium.randombytes_buf(salt)
18 }
19 const key = b4a.allocUnsafe(KEY_BYTES)
20 sodium.crypto_pwhash(
21 key,
22 b4a.from(password),
23 salt,
24 sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
25 sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
26 sodium.crypto_pwhash_ALG_DEFAULT
27 )
28 return { key, salt }
29}
30
31function encrypt (plaintext, key) {
32 const nonce = b4a.allocUnsafe(NONCE_BYTES)
33 sodium.randombytes_buf(nonce)
34
35 const input = b4a.isBuffer(plaintext) ? plaintext : b4a.from(plaintext)
36 const cipher = b4a.allocUnsafe(input.length + MAC_BYTES)
37
38 sodium.crypto_secretbox_easy(cipher, input, nonce, key)
39
40 // Return nonce + ciphertext
41 const result = b4a.allocUnsafe(NONCE_BYTES + cipher.length)
42 b4a.copy(nonce, result, 0)
43 b4a.copy(cipher, result, NONCE_BYTES)
44 return result
45}
46
47function decrypt (encrypted, key) {
48 const nonce = encrypted.subarray(0, NONCE_BYTES)
49 const cipher = encrypted.subarray(NONCE_BYTES)
50
51 const plaintext = b4a.allocUnsafe(cipher.length - MAC_BYTES)
52 const ok = sodium.crypto_secretbox_open_easy(plaintext, cipher, nonce, key)
53 if (!ok) throw new Error('Decryption failed — wrong key or corrupted data')
54
55 return plaintext
56}
57
58export { generateKey, deriveKey, encrypt, decrypt, KEY_BYTES }
bridge-vault/lib/vault.js +96 -0
……@@ -0,0 +1,96 @@
1import Corestore from 'corestore'
2import Hyperbee from 'hyperbee'
3import b4a from 'b4a'
4import { encrypt, decrypt, generateKey } from './crypto.js'
5
6class Vault {
7 constructor (storage, opts = {}) {
8 this._storage = storage
9 this._key = opts.key || generateKey()
10 this._store = null
11 this._db = null
12 this._ready = false
13 }
14
15 async ready () {
16 if (this._ready) return
17
18 this._store = new Corestore(this._storage)
19 const core = this._store.get({ name: 'bridge-vault' })
20 this._db = new Hyperbee(core, {
21 keyEncoding: 'utf-8',
22 valueEncoding: 'binary'
23 })
24 await this._db.ready()
25 this._ready = true
26 }
27
28 _key4 (platform, name) {
29 return `cred/${platform}/${name}`
30 }
31
32 async put (platform, name, value) {
33 await this.ready()
34 const plaintext = b4a.from(JSON.stringify(value))
35 const encrypted = encrypt(plaintext, this._key)
36 await this._db.put(this._key4(platform, name), encrypted)
37 }
38
39 async get (platform, name) {
40 await this.ready()
41 const node = await this._db.get(this._key4(platform, name))
42 if (!node) return null
43
44 const plaintext = decrypt(node.value, this._key)
45 return JSON.parse(b4a.toString(plaintext))
46 }
47
48 async del (platform, name) {
49 await this.ready()
50 await this._db.del(this._key4(platform, name))
51 }
52
53 async list (platform) {
54 await this.ready()
55 const prefix = `cred/${platform}/`
56 const results = []
57
58 for await (const node of this._db.createReadStream({
59 gte: prefix,
60 lt: prefix + '\xff'
61 })) {
62 const name = node.key.slice(prefix.length)
63 try {
64 const plaintext = decrypt(node.value, this._key)
65 results.push({ name, value: JSON.parse(b4a.toString(plaintext)) })
66 } catch {
67 results.push({ name, value: null, error: 'decrypt failed' })
68 }
69 }
70
71 return results
72 }
73
74 async listPlatforms () {
75 await this.ready()
76 const platforms = new Set()
77 const prefix = 'cred/'
78
79 for await (const node of this._db.createReadStream({
80 gte: prefix,
81 lt: prefix + '\xff'
82 })) {
83 const parts = node.key.slice(prefix.length).split('/')
84 if (parts.length >= 1) platforms.add(parts[0])
85 }
86
87 return [...platforms]
88 }
89
90 async close () {
91 if (this._store) await this._store.close()
92 this._ready = false
93 }
94}
95
96export { Vault }
bridge-vault/package-lock.json +1445 -0
……@@ -0,0 +1,1445 @@
1{
2 "name": "bridge-vault",
3 "version": "1.0.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "bridge-vault",
9 "version": "1.0.0",
10 "license": "MIT",
11 "dependencies": {
12 "b4a": "^1.0.0",
13 "compact-encoding": "^2.0.0",
14 "corestore": "^6.0.0",
15 "hyperbee": "^2.0.0",
16 "hypercore": "^10.0.0",
17 "sodium-native": "^4.0.0"
18 },
19 "devDependencies": {
20 "brittle": "^3.0.0"
21 }
22 },
23 "node_modules/@hyperswarm/secret-stream": {
24 "version": "6.9.1",
25 "resolved": "https://registry.npmjs.org/@hyperswarm/secret-stream/-/secret-stream-6.9.1.tgz",
26 "integrity": "sha512-xb0S5y3YJwBakD77JOGBHlBxdp63mHClZoXBYoLv+9wH8e054ESKlmQptWqjJK5dv5VMUIVYOJB4MaOpB0JdGw==",
27 "license": "Apache-2.0",
28 "dependencies": {
29 "b4a": "^1.1.0",
30 "hypercore-crypto": "^3.3.1",
31 "noise-curve-ed": "^2.0.1",
32 "noise-handshake": "^4.0.0",
33 "sodium-secretstream": "^1.1.0",
34 "sodium-universal": "^5.0.0",
35 "streamx": "^2.14.0",
36 "timeout-refresh": "^2.0.0",
37 "unslab": "^1.3.0"
38 }
39 },
40 "node_modules/@hyperswarm/secret-stream/node_modules/sodium-native": {
41 "version": "5.0.10",
42 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
43 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
44 "license": "MIT",
45 "dependencies": {
46 "require-addon": "^1.1.0",
47 "which-runtime": "^1.2.1"
48 },
49 "engines": {
50 "bare": ">=1.16.0"
51 }
52 },
53 "node_modules/@hyperswarm/secret-stream/node_modules/sodium-universal": {
54 "version": "5.0.1",
55 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
56 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
57 "license": "MIT",
58 "dependencies": {
59 "sodium-native": "^5.0.1"
60 },
61 "peerDependencies": {
62 "sodium-javascript": "~0.8.0"
63 },
64 "peerDependenciesMeta": {
65 "sodium-javascript": {
66 "optional": true
67 }
68 }
69 },
70 "node_modules/@jridgewell/resolve-uri": {
71 "version": "3.1.2",
72 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
73 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
74 "dev": true,
75 "license": "MIT",
76 "engines": {
77 "node": ">=6.0.0"
78 }
79 },
80 "node_modules/@jridgewell/sourcemap-codec": {
81 "version": "1.5.5",
82 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
83 "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
84 "dev": true,
85 "license": "MIT"
86 },
87 "node_modules/@jridgewell/trace-mapping": {
88 "version": "0.3.31",
89 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
90 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
91 "dev": true,
92 "license": "MIT",
93 "dependencies": {
94 "@jridgewell/resolve-uri": "^3.1.0",
95 "@jridgewell/sourcemap-codec": "^1.4.14"
96 }
97 },
98 "node_modules/@types/istanbul-lib-coverage": {
99 "version": "2.0.6",
100 "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
101 "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
102 "dev": true,
103 "license": "MIT"
104 },
105 "node_modules/b4a": {
106 "version": "1.7.4",
107 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.4.tgz",
108 "integrity": "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug==",
109 "license": "Apache-2.0",
110 "peerDependencies": {
111 "react-native-b4a": "*"
112 },
113 "peerDependenciesMeta": {
114 "react-native-b4a": {
115 "optional": true
116 }
117 }
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 "license": "Apache-2.0",
124 "dependencies": {
125 "bare-module-resolve": "^1.10.0",
126 "bare-semver": "^1.0.0"
127 },
128 "peerDependencies": {
129 "bare-url": "*"
130 },
131 "peerDependenciesMeta": {
132 "bare-url": {
133 "optional": true
134 }
135 }
136 },
137 "node_modules/bare-ansi-escapes": {
138 "version": "2.2.3",
139 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
140 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
141 "dev": true,
142 "license": "Apache-2.0",
143 "dependencies": {
144 "bare-stream": "^2.6.5"
145 },
146 "peerDependencies": {
147 "bare-buffer": "*"
148 },
149 "peerDependenciesMeta": {
150 "bare-buffer": {
151 "optional": true
152 }
153 }
154 },
155 "node_modules/bare-assert": {
156 "version": "1.2.0",
157 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
158 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
159 "dev": true,
160 "license": "Apache-2.0",
161 "dependencies": {
162 "bare-inspect": "^3.1.2"
163 }
164 },
165 "node_modules/bare-bundle": {
166 "version": "1.10.0",
167 "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
168 "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
169 "dev": true,
170 "license": "Apache-2.0",
171 "peerDependencies": {
172 "bare-buffer": "*",
173 "bare-url": "*"
174 },
175 "peerDependenciesMeta": {
176 "bare-buffer": {
177 "optional": true
178 },
179 "bare-url": {
180 "optional": true
181 }
182 }
183 },
184 "node_modules/bare-cov": {
185 "version": "1.2.1",
186 "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz",
187 "integrity": "sha512-SHNqb/WG9udUEj8m5qGSehoUQ9o0Xzv8nrf4HIdmG+KNU/XdhPLhTP5VPrHnSAMAXhAzEB4Q+2IYZ0x9qPzN1g==",
188 "dev": true,
189 "license": "Apache-2.0",
190 "dependencies": {
191 "bare-fs": "^4.1.2",
192 "bare-inspector": "^4.0.1",
193 "bare-path": "^3.0.0",
194 "bare-process": "^4.2.1",
195 "bare-url": "^2.1.5",
196 "bare-v8-to-istanbul": "^1.0.2",
197 "picomatch": "^4.0.2",
198 "which-runtime": "^1.2.1"
199 }
200 },
201 "node_modules/bare-crypto": {
202 "version": "1.13.0",
203 "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.0.tgz",
204 "integrity": "sha512-RQl13yD+YTACWUZHMMck0C6LNjXgGgRyhVC557ag0xIrkxXf4IQwpnc8WOB58f4k5kSzKhIjy05oW3HUBrFpSQ==",
205 "dev": true,
206 "license": "Apache-2.0",
207 "dependencies": {
208 "bare-stream": "^2.6.3"
209 },
210 "peerDependencies": {
211 "bare-buffer": "*"
212 },
213 "peerDependenciesMeta": {
214 "bare-buffer": {
215 "optional": true
216 }
217 }
218 },
219 "node_modules/bare-debug-log": {
220 "version": "2.0.0",
221 "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
222 "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
223 "dev": true,
224 "license": "Apache-2.0",
225 "dependencies": {
226 "bare-os": "^3.0.1"
227 }
228 },
229 "node_modules/bare-dns": {
230 "version": "2.1.4",
231 "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
232 "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==",
233 "dev": true,
234 "license": "Apache-2.0",
235 "engines": {
236 "bare": ">=1.7.0"
237 }
238 },
239 "node_modules/bare-encoding": {
240 "version": "1.0.3",
241 "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
242 "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
243 "dev": true,
244 "license": "Apache-2.0",
245 "peerDependencies": {
246 "bare-buffer": "*"
247 },
248 "peerDependenciesMeta": {
249 "bare-buffer": {
250 "optional": true
251 }
252 }
253 },
254 "node_modules/bare-env": {
255 "version": "3.0.0",
256 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
257 "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
258 "dev": true,
259 "license": "Apache-2.0",
260 "dependencies": {
261 "bare-os": "^3.0.1"
262 }
263 },
264 "node_modules/bare-events": {
265 "version": "2.8.2",
266 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
267 "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
268 "license": "Apache-2.0",
269 "peerDependencies": {
270 "bare-abort-controller": "*"
271 },
272 "peerDependenciesMeta": {
273 "bare-abort-controller": {
274 "optional": true
275 }
276 }
277 },
278 "node_modules/bare-format": {
279 "version": "1.0.1",
280 "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.1.tgz",
281 "integrity": "sha512-1oS+LZrWK6tnYnvNSHDGljc2MPunRxwhpFriuCgzNF+oklrnwmBKD91tS0yt+jpl2j3UgcSDzBIMiVTvLs9A8w==",
282 "dev": true,
283 "license": "Apache-2.0",
284 "dependencies": {
285 "bare-inspect": "^3.0.0"
286 }
287 },
288 "node_modules/bare-fs": {
289 "version": "4.5.4",
290 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz",
291 "integrity": "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==",
292 "license": "Apache-2.0",
293 "dependencies": {
294 "bare-events": "^2.5.4",
295 "bare-path": "^3.0.0",
296 "bare-stream": "^2.6.4",
297 "bare-url": "^2.2.2",
298 "fast-fifo": "^1.3.2"
299 },
300 "engines": {
301 "bare": ">=1.16.0"
302 },
303 "peerDependencies": {
304 "bare-buffer": "*"
305 },
306 "peerDependenciesMeta": {
307 "bare-buffer": {
308 "optional": true
309 }
310 }
311 },
312 "node_modules/bare-hrtime": {
313 "version": "2.1.1",
314 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
315 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
316 "dev": true,
317 "license": "Apache-2.0"
318 },
319 "node_modules/bare-http-parser": {
320 "version": "1.0.1",
321 "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.0.1.tgz",
322 "integrity": "sha512-A3LTDTcELcmNJ3g5liIaS038v/BQxOhA9cjhBESn7eoV7QCuMoIRBKLDadDe08flxyLbxI2f+1l2MZ/5+HnKPA==",
323 "dev": true,
324 "license": "Apache-2.0"
325 },
326 "node_modules/bare-http1": {
327 "version": "4.2.3",
328 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.2.3.tgz",
329 "integrity": "sha512-zyvHF3xzvBJNpuw0QydE9etTn2AyC/TRVDWTo8Ed3LQEEGaFOkv6KHhaJPlf4eWEeTekk41Fowie+fTsXoNxkA==",
330 "dev": true,
331 "license": "Apache-2.0",
332 "dependencies": {
333 "bare-events": "^2.6.0",
334 "bare-http-parser": "^1.0.0",
335 "bare-stream": "^2.3.0",
336 "bare-tcp": "^2.2.0"
337 },
338 "peerDependencies": {
339 "bare-buffer": "*",
340 "bare-url": "*"
341 },
342 "peerDependenciesMeta": {
343 "bare-buffer": {
344 "optional": true
345 },
346 "bare-url": {
347 "optional": true
348 }
349 }
350 },
351 "node_modules/bare-https": {
352 "version": "2.1.2",
353 "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.2.tgz",
354 "integrity": "sha512-Q+TTydUDsuKQJvh8dX2dvOXCR9fM3xR5TBmKaFrs5p7Lj7XbKX7v4vIUJ36H0SXg2xCOQxXKIbjwrLg5tfJNYg==",
355 "dev": true,
356 "license": "Apache-2.0",
357 "dependencies": {
358 "bare-http1": "^4.0.0",
359 "bare-tcp": "^2.2.0",
360 "bare-tls": "^2.0.0"
361 }
362 },
363 "node_modules/bare-inspect": {
364 "version": "3.1.4",
365 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
366 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
367 "dev": true,
368 "license": "Apache-2.0",
369 "dependencies": {
370 "bare-ansi-escapes": "^2.1.0",
371 "bare-type": "^1.0.0"
372 },
373 "engines": {
374 "bare": ">=1.18.0"
375 }
376 },
377 "node_modules/bare-inspector": {
378 "version": "4.0.3",
379 "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-4.0.3.tgz",
380 "integrity": "sha512-yYk6+7tTVKJKHOja5+QBn989ETv8oVsmwoH0nCM+QYC6w5MC03GC4aQNK+lipAZSz1EaAj42WZ8Wewjpy0L9TA==",
381 "dev": true,
382 "license": "Apache-2.0",
383 "dependencies": {
384 "bare-events": "^2.1.0",
385 "bare-http1": "^4.0.0",
386 "bare-stream": "^2.0.0",
387 "bare-url": "^2.0.0",
388 "bare-ws": "^2.0.0"
389 },
390 "engines": {
391 "bare": ">=1.2.0"
392 },
393 "peerDependencies": {
394 "bare-tcp": "*"
395 },
396 "peerDependenciesMeta": {
397 "bare-tcp": {
398 "optional": true
399 }
400 }
401 },
402 "node_modules/bare-module": {
403 "version": "6.1.3",
404 "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz",
405 "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==",
406 "dev": true,
407 "license": "Apache-2.0",
408 "dependencies": {
409 "bare-bundle": "^1.3.0",
410 "bare-module-lexer": "^1.0.0",
411 "bare-module-resolve": "^1.8.0",
412 "bare-path": "^3.0.0",
413 "bare-url": "^2.0.1"
414 },
415 "engines": {
416 "bare": ">=1.23.0"
417 },
418 "peerDependencies": {
419 "bare-buffer": "*"
420 },
421 "peerDependenciesMeta": {
422 "bare-buffer": {
423 "optional": true
424 }
425 }
426 },
427 "node_modules/bare-module-lexer": {
428 "version": "1.4.7",
429 "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
430 "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
431 "dev": true,
432 "license": "Apache-2.0",
433 "dependencies": {
434 "require-addon": "^1.0.2"
435 },
436 "peerDependencies": {
437 "bare-buffer": "*"
438 },
439 "peerDependenciesMeta": {
440 "bare-buffer": {
441 "optional": true
442 }
443 }
444 },
445 "node_modules/bare-module-resolve": {
446 "version": "1.12.1",
447 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz",
448 "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==",
449 "license": "Apache-2.0",
450 "dependencies": {
451 "bare-semver": "^1.0.0"
452 },
453 "peerDependencies": {
454 "bare-url": "*"
455 },
456 "peerDependenciesMeta": {
457 "bare-url": {
458 "optional": true
459 }
460 }
461 },
462 "node_modules/bare-net": {
463 "version": "2.2.0",
464 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.2.0.tgz",
465 "integrity": "sha512-UF7cAbHsGE+H6uEqWF5IULBow1x58chZz4g3ALgHtv7wZsFcCbRDt0JKWEumf5Oma3QWS1Q6aLi0Rpll8RElMg==",
466 "dev": true,
467 "license": "Apache-2.0",
468 "dependencies": {
469 "bare-events": "^2.2.2",
470 "bare-pipe": "^4.0.0",
471 "bare-stream": "^2.0.0",
472 "bare-tcp": "^2.0.0"
473 }
474 },
475 "node_modules/bare-os": {
476 "version": "3.6.2",
477 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
478 "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
479 "license": "Apache-2.0",
480 "engines": {
481 "bare": ">=1.14.0"
482 }
483 },
484 "node_modules/bare-path": {
485 "version": "3.0.0",
486 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
487 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
488 "license": "Apache-2.0",
489 "dependencies": {
490 "bare-os": "^3.0.1"
491 }
492 },
493 "node_modules/bare-pipe": {
494 "version": "4.1.3",
495 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.3.tgz",
496 "integrity": "sha512-DqQsx93rAzre6yJ9T6l/Vgh+X+bntkVMB1X5ggtXjXtqtMmF2Y2RVlCzxxy/09R6yeR9FSWBEUIaMYJL1/5VDA==",
497 "dev": true,
498 "license": "Apache-2.0",
499 "dependencies": {
500 "bare-events": "^2.0.0",
501 "bare-stream": "^2.0.0"
502 },
503 "engines": {
504 "bare": ">=1.16.0"
505 }
506 },
507 "node_modules/bare-process": {
508 "version": "4.2.2",
509 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.2.2.tgz",
510 "integrity": "sha512-ikzEw+HGLB+2lS/WLVEsqi8BXBwzleG3n7DYI0v6/YNklvNabOIGlLd1dof+7Jw5ob4jsBRPtyflweVahY7rFA==",
511 "dev": true,
512 "license": "Apache-2.0",
513 "dependencies": {
514 "bare-env": "^3.0.0",
515 "bare-events": "^2.3.1",
516 "bare-hrtime": "^2.0.0",
517 "bare-os": "^3.5.0",
518 "bare-pipe": "^4.0.0",
519 "bare-signals": "^4.0.0",
520 "bare-tty": "^5.0.0"
521 }
522 },
523 "node_modules/bare-semver": {
524 "version": "1.0.2",
525 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz",
526 "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==",
527 "license": "Apache-2.0"
528 },
529 "node_modules/bare-signals": {
530 "version": "4.2.0",
531 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz",
532 "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==",
533 "dev": true,
534 "license": "Apache-2.0",
535 "dependencies": {
536 "bare-events": "^2.5.3",
537 "bare-os": "^3.3.1"
538 },
539 "engines": {
540 "bare": ">=1.7.0"
541 }
542 },
543 "node_modules/bare-stream": {
544 "version": "2.7.0",
545 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
546 "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
547 "license": "Apache-2.0",
548 "dependencies": {
549 "streamx": "^2.21.0"
550 },
551 "peerDependencies": {
552 "bare-buffer": "*",
553 "bare-events": "*"
554 },
555 "peerDependenciesMeta": {
556 "bare-buffer": {
557 "optional": true
558 },
559 "bare-events": {
560 "optional": true
561 }
562 }
563 },
564 "node_modules/bare-subprocess": {
565 "version": "5.2.2",
566 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.2.tgz",
567 "integrity": "sha512-L6oXgQ1aWs25RtG5Ky0bDD06p3RAcVVrDDMWb1DfXpHtyEWxamcyIWUbSykxMTWrpLlmSj6ytbb6yoKehGFfmw==",
568 "dev": true,
569 "license": "Apache-2.0",
570 "dependencies": {
571 "bare-env": "^3.0.0",
572 "bare-events": "^2.5.4",
573 "bare-os": "^3.0.1",
574 "bare-pipe": "^4.0.0",
575 "bare-url": "^2.2.2"
576 },
577 "engines": {
578 "bare": ">=1.7.0"
579 },
580 "peerDependencies": {
581 "bare-buffer": "*"
582 },
583 "peerDependenciesMeta": {
584 "bare-buffer": {
585 "optional": true
586 }
587 }
588 },
589 "node_modules/bare-tcp": {
590 "version": "2.2.4",
591 "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.4.tgz",
592 "integrity": "sha512-vGjfMkyhS1lxp+Y4+gXRDR1ijZMK2cKx0RBwtYKUuZbD90ARCoM6bAiTa1HBztumCIC+NXb8eXdMFXyuYwlPyw==",
593 "dev": true,
594 "license": "Apache-2.0",
595 "dependencies": {
596 "bare-dns": "^2.0.4",
597 "bare-events": "^2.5.4",
598 "bare-stream": "^2.6.4"
599 },
600 "engines": {
601 "bare": ">=1.16.0"
602 }
603 },
604 "node_modules/bare-tls": {
605 "version": "2.1.7",
606 "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.1.7.tgz",
607 "integrity": "sha512-h6wcNXQdBeTX7fed9tjPp0/9cA/QfcBTv3ItgjnbUk4rWAU8bEFalZCZnUDdCK/t9zrNfJ+yvcPx4D/1Y6biyA==",
608 "dev": true,
609 "license": "Apache-2.0",
610 "dependencies": {
611 "bare-net": "^2.0.1",
612 "bare-stream": "^2.6.4"
613 },
614 "engines": {
615 "bare": ">=1.7.0"
616 }
617 },
618 "node_modules/bare-tty": {
619 "version": "5.0.3",
620 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.0.3.tgz",
621 "integrity": "sha512-jW24RBWRZOMHuSEWC9mh8wUKO5WUNl0UWUTNPn3yZ8qkOqwa8vaV2dR3a+BvblONPk7V35wuvK9P8508+judAg==",
622 "dev": true,
623 "license": "Apache-2.0",
624 "dependencies": {
625 "bare-events": "^2.2.0",
626 "bare-signals": "^4.0.0",
627 "bare-stream": "^2.0.0"
628 },
629 "engines": {
630 "bare": ">=1.16.0"
631 }
632 },
633 "node_modules/bare-type": {
634 "version": "1.1.0",
635 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
636 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
637 "dev": true,
638 "license": "Apache-2.0",
639 "engines": {
640 "bare": ">=1.2.0"
641 }
642 },
643 "node_modules/bare-url": {
644 "version": "2.3.2",
645 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
646 "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
647 "license": "Apache-2.0",
648 "peer": true,
649 "dependencies": {
650 "bare-path": "^3.0.0"
651 }
652 },
653 "node_modules/bare-utils": {
654 "version": "1.5.1",
655 "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.5.1.tgz",
656 "integrity": "sha512-mxCkFvmDU3mlD/mb+pT64kKXOsx2KMsWLQbngN1LB+NOXfhfnRnyvpy3VZc6m7gzQxe57Bsi+aTCBqA4/S3elQ==",
657 "dev": true,
658 "license": "Apache-2.0",
659 "dependencies": {
660 "bare-debug-log": "^2.0.0",
661 "bare-encoding": "^1.0.0",
662 "bare-format": "^1.0.0",
663 "bare-inspect": "^3.0.0",
664 "bare-type": "^1.0.6"
665 }
666 },
667 "node_modules/bare-v8-to-istanbul": {
668 "version": "1.0.2",
669 "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
670 "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==",
671 "dev": true,
672 "license": "Apache-2.0",
673 "dependencies": {
674 "bare-assert": "^1.0.2",
675 "bare-fs": "^4.1.2",
676 "bare-module": "^6.1.2",
677 "bare-path": "^3.0.0",
678 "bare-process": "^4.2.0",
679 "bare-url": "^2.1.5",
680 "bare-utils": "^1.2.0",
681 "v8-to-istanbul": "^9.3.0",
682 "which-runtime": "^1.2.1"
683 }
684 },
685 "node_modules/bare-ws": {
686 "version": "2.0.4",
687 "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.0.4.tgz",
688 "integrity": "sha512-SQMXzBYna9dRj57Dz1/ag+VWHCRXbfCjMHgyfM2F2lhkVLzMjnVSZP72aVeFWPFqe494Rd70Kzhe2JElGwFlJQ==",
689 "dev": true,
690 "license": "Apache-2.0",
691 "dependencies": {
692 "bare-crypto": "^1.2.0",
693 "bare-events": "^2.3.1",
694 "bare-http1": "^4.0.0",
695 "bare-https": "^2.0.0",
696 "bare-stream": "^2.1.2"
697 },
698 "peerDependencies": {
699 "bare-buffer": "*",
700 "bare-url": "*"
701 },
702 "peerDependenciesMeta": {
703 "bare-buffer": {
704 "optional": true
705 },
706 "bare-url": {
707 "optional": true
708 }
709 }
710 },
711 "node_modules/big-sparse-array": {
712 "version": "1.0.3",
713 "resolved": "https://registry.npmjs.org/big-sparse-array/-/big-sparse-array-1.0.3.tgz",
714 "integrity": "sha512-6RjV/3mSZORlMdpUaQ6rUSpG637cZm0//E54YYGtQg1c1O+AbZP8UTdJ/TchsDZcTVLmyWZcseBfp2HBeXUXOQ==",
715 "license": "MIT"
716 },
717 "node_modules/brittle": {
718 "version": "3.19.1",
719 "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz",
720 "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==",
721 "dev": true,
722 "license": "Apache-2.0",
723 "dependencies": {
724 "b4a": "^1.6.0",
725 "bare-assert": "^1.0.2",
726 "bare-cov": "^1.1.0",
727 "bare-fs": "^4.1.6",
728 "bare-os": "^3.6.1",
729 "bare-path": "^3.0.0",
730 "bare-process": "^4.2.1",
731 "bare-subprocess": "^5.0.0",
732 "bare-url": "^2.1.6",
733 "error-stack-parser": "^2.1.4",
734 "globbie": "^1.0.2",
735 "paparam": "^1.6.2",
736 "same-object": "^1.0.2",
737 "test-tmp": "^1.4.0",
738 "tmatch": "^5.0.0"
739 },
740 "bin": {
741 "brittle": "bin/node.js",
742 "brittle-bare": "bin/bare.js",
743 "brittle-node": "bin/node.js",
744 "brittle-pear": "bin/pear.js"
745 }
746 },
747 "node_modules/codecs": {
748 "version": "3.1.0",
749 "resolved": "https://registry.npmjs.org/codecs/-/codecs-3.1.0.tgz",
750 "integrity": "sha512-Dqx8NwvBvnMeuPQdVKy/XEF71igjR5apxBvCGeV0pP1tXadOiaLvDTXt7xh+/5wI1ASB195mXQGJbw3Ml4YDWQ==",
751 "license": "MIT",
752 "dependencies": {
753 "b4a": "^1.6.3"
754 }
755 },
756 "node_modules/compact-encoding": {
757 "version": "2.18.0",
758 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-2.18.0.tgz",
759 "integrity": "sha512-goACAOlhMI2xo5jGOMUDfOLnGdRE1jGfyZ+zie8N5114nHrbPIqf6GLUtzbLof6DSyrERlYRm3EcBplte5LcQw==",
760 "license": "Apache-2.0",
761 "dependencies": {
762 "b4a": "^1.3.0"
763 }
764 },
765 "node_modules/convert-source-map": {
766 "version": "2.0.0",
767 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
768 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
769 "dev": true,
770 "license": "MIT"
771 },
772 "node_modules/corestore": {
773 "version": "6.18.4",
774 "resolved": "https://registry.npmjs.org/corestore/-/corestore-6.18.4.tgz",
775 "integrity": "sha512-KL1cOUyj3DFRNzeVRekGPRiGN4sIfGo7BsncSJQQBT3Rh2y1nQ32MOR9X5DlOMlWIT4txwqBzhzam6+JXtwFVw==",
776 "license": "MIT",
777 "dependencies": {
778 "b4a": "^1.6.4",
779 "hypercore": "^10.37.10",
780 "hypercore-crypto": "^3.4.0",
781 "hypercore-id-encoding": "^1.2.0",
782 "read-write-mutexify": "^2.1.0",
783 "ready-resource": "^1.0.0",
784 "safety-catch": "^1.0.1",
785 "sodium-universal": "^4.0.0",
786 "xache": "^1.1.0"
787 }
788 },
789 "node_modules/crc-native": {
790 "version": "1.1.8",
791 "resolved": "https://registry.npmjs.org/crc-native/-/crc-native-1.1.8.tgz",
792 "integrity": "sha512-R/RYbD8lhG5M1k0iLX49tJdwHM0ipevJPpZw2Wk7b4EShyvx2P0uaq79xyRpcknjiVGiUfjFA8Gyy0mOlhABvw==",
793 "license": "Apache-2.0",
794 "optional": true,
795 "dependencies": {
796 "require-addon": "^1.1.0"
797 }
798 },
799 "node_modules/crc-universal": {
800 "version": "1.0.4",
801 "resolved": "https://registry.npmjs.org/crc-universal/-/crc-universal-1.0.4.tgz",
802 "integrity": "sha512-CE9xWEI6Gd5V0Bdmj5NoWH3d7+EIe4zUpy1sv6uvKYznzsDP1vhiWsTBmL6q9IrH2P6RRshp+8AkhR4CMtY5Hg==",
803 "license": "ISC",
804 "optionalDependencies": {
805 "crc-native": "^1.0.3"
806 }
807 },
808 "node_modules/debounceify": {
809 "version": "1.1.0",
810 "resolved": "https://registry.npmjs.org/debounceify/-/debounceify-1.1.0.tgz",
811 "integrity": "sha512-eKuHDVfJVg+u/0nPy8P+fhnLgbyuTgVxuCRrS/R7EpDSMMkBDgSes41MJtSAY1F1hcqfHz3Zy/qpqHHIp/EhdA==",
812 "license": "MIT"
813 },
814 "node_modules/error-stack-parser": {
815 "version": "2.1.4",
816 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
817 "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
818 "dev": true,
819 "license": "MIT",
820 "dependencies": {
821 "stackframe": "^1.3.4"
822 }
823 },
824 "node_modules/events-universal": {
825 "version": "1.0.1",
826 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
827 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
828 "license": "Apache-2.0",
829 "dependencies": {
830 "bare-events": "^2.7.0"
831 }
832 },
833 "node_modules/fast-fifo": {
834 "version": "1.3.2",
835 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
836 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
837 "license": "MIT"
838 },
839 "node_modules/flat-tree": {
840 "version": "1.13.0",
841 "resolved": "https://registry.npmjs.org/flat-tree/-/flat-tree-1.13.0.tgz",
842 "integrity": "sha512-fT3HIuCPwHhFgJ20QYzDHgUG0zMmFg5cHvFiFo5h+QMSJ28TihsEVY0f8HGliuO+pOzmvjMx1odToeaEWkTnyQ==",
843 "license": "MIT"
844 },
845 "node_modules/fs-native-extensions": {
846 "version": "1.4.5",
847 "resolved": "https://registry.npmjs.org/fs-native-extensions/-/fs-native-extensions-1.4.5.tgz",
848 "integrity": "sha512-ekV0T//iDm4AvhOcuPaHpxub4DI7HvY5ucLJVDvi7T2J+NZkQ9S6MuvgP0yeQvoqNUaAGyLjVYb1905BF9bpmg==",
849 "license": "Apache-2.0",
850 "optional": true,
851 "dependencies": {
852 "require-addon": "^1.1.0",
853 "which-runtime": "^1.2.0"
854 }
855 },
856 "node_modules/globbie": {
857 "version": "1.0.3",
858 "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz",
859 "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==",
860 "dev": true,
861 "license": "Apache-2.0",
862 "dependencies": {
863 "bare-fs": "^4.1.2",
864 "bare-path": "^3.0.0",
865 "bare-process": "^4.2.1",
866 "picomatch": "^4.0.2"
867 }
868 },
869 "node_modules/hyperbee": {
870 "version": "2.27.3",
871 "resolved": "https://registry.npmjs.org/hyperbee/-/hyperbee-2.27.3.tgz",
872 "integrity": "sha512-PXURH2U4juUZyJRKHTrY5z1zX851pmI1Q0jfv5F/hCIErDt/ND8jOZuxc3hfOLM9f0W3qJEDTMlV5AJBkVPy8w==",
873 "license": "MIT",
874 "dependencies": {
875 "b4a": "^1.6.0",
876 "codecs": "^3.0.0",
877 "debounceify": "^1.0.0",
878 "hypercore-errors": "^1.0.0",
879 "mutexify": "^1.4.0",
880 "protocol-buffers-encodings": "^1.2.0",
881 "rache": "^1.0.0",
882 "ready-resource": "^1.0.0",
883 "resolve-reject-promise": "^1.1.0",
884 "safety-catch": "^1.0.2",
885 "streamx": "^2.12.4",
886 "unslab": "^1.2.0"
887 }
888 },
889 "node_modules/hypercore": {
890 "version": "10.38.2",
891 "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-10.38.2.tgz",
892 "integrity": "sha512-2vx42g2RKdP/o2XzlxKfvgEaZ3+h/B855gCR8iscUDnxUoOZej7vXZ2buex0sweDUEW5aEfMxFR6o5hncio3Nw==",
893 "license": "MIT",
894 "dependencies": {
895 "@hyperswarm/secret-stream": "^6.0.0",
896 "b4a": "^1.1.0",
897 "bare-events": "^2.2.0",
898 "big-sparse-array": "^1.0.3",
899 "compact-encoding": "^2.11.0",
900 "crc-universal": "^1.0.2",
901 "fast-fifo": "^1.3.0",
902 "flat-tree": "^1.9.0",
903 "hypercore-crypto": "^3.2.1",
904 "hypercore-errors": "^1.2.0",
905 "hypercore-id-encoding": "^1.2.0",
906 "is-options": "^1.0.1",
907 "protomux": "^3.5.0",
908 "quickbit-universal": "^2.2.0",
909 "random-access-file": "^4.0.0",
910 "random-array-iterator": "^1.0.0",
911 "safety-catch": "^1.0.1",
912 "sodium-universal": "^4.0.0",
913 "streamx": "^2.12.4",
914 "unslab": "^1.3.0",
915 "xache": "^1.1.0",
916 "z32": "^1.0.0"
917 }
918 },
919 "node_modules/hypercore-crypto": {
920 "version": "3.6.1",
921 "resolved": "https://registry.npmjs.org/hypercore-crypto/-/hypercore-crypto-3.6.1.tgz",
922 "integrity": "sha512-ltIz2uDwy9pO/ZGTvqcjzyBkvt6O4cVm4r/nNxh0GFs/RbQtqP/i4wCvLEdmU7ptgtnw7fI67WYD1aHPuv4OVA==",
923 "license": "MIT",
924 "dependencies": {
925 "b4a": "^1.6.6",
926 "compact-encoding": "^2.15.0",
927 "sodium-universal": "^5.0.0"
928 }
929 },
930 "node_modules/hypercore-crypto/node_modules/sodium-native": {
931 "version": "5.0.10",
932 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
933 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
934 "license": "MIT",
935 "dependencies": {
936 "require-addon": "^1.1.0",
937 "which-runtime": "^1.2.1"
938 },
939 "engines": {
940 "bare": ">=1.16.0"
941 }
942 },
943 "node_modules/hypercore-crypto/node_modules/sodium-universal": {
944 "version": "5.0.1",
945 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
946 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
947 "license": "MIT",
948 "dependencies": {
949 "sodium-native": "^5.0.1"
950 },
951 "peerDependencies": {
952 "sodium-javascript": "~0.8.0"
953 },
954 "peerDependenciesMeta": {
955 "sodium-javascript": {
956 "optional": true
957 }
958 }
959 },
960 "node_modules/hypercore-errors": {
961 "version": "1.5.0",
962 "resolved": "https://registry.npmjs.org/hypercore-errors/-/hypercore-errors-1.5.0.tgz",
963 "integrity": "sha512-5KQ/SuDxsvet+7qWA35Ay6zdD9WyAHQoyWHGcPUTbmJBd300gvNIJoi3oma7kp4TTCSzii6qYumNZe/s0j/saQ==",
964 "license": "Apache-2.0",
965 "dependencies": {
966 "hypercore-id-encoding": "^1.3.0"
967 }
968 },
969 "node_modules/hypercore-id-encoding": {
970 "version": "1.3.0",
971 "resolved": "https://registry.npmjs.org/hypercore-id-encoding/-/hypercore-id-encoding-1.3.0.tgz",
972 "integrity": "sha512-W6sHdGo5h7LXEsoWfKf/KfuROZmZRQDlGqJF2EPHW+noCK66Vvr0+zE6cL0vqQi18s0kQPeN7Sq3QyR0Ytc2VQ==",
973 "license": "Apache-2.0",
974 "dependencies": {
975 "b4a": "^1.5.3",
976 "z32": "^1.0.0"
977 }
978 },
979 "node_modules/is-options": {
980 "version": "1.0.2",
981 "resolved": "https://registry.npmjs.org/is-options/-/is-options-1.0.2.tgz",
982 "integrity": "sha512-u+Ai74c8Q74aS8BuHwPdI1jptGOT1FQXgCq8/zv0xRuE+wRgSMEJLj8lVO8Zp9BeGb29BXY6AsNPinfqjkr7Fg==",
983 "license": "MIT",
984 "dependencies": {
985 "b4a": "^1.1.1"
986 }
987 },
988 "node_modules/mutexify": {
989 "version": "1.4.0",
990 "resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz",
991 "integrity": "sha512-pbYSsOrSB/AKN5h/WzzLRMFgZhClWccf2XIB4RSMC8JbquiB0e0/SH5AIfdQMdyHmYtv4seU7yV/TvAwPLJ1Yg==",
992 "license": "MIT",
993 "dependencies": {
994 "queue-tick": "^1.0.0"
995 }
996 },
997 "node_modules/nanoassert": {
998 "version": "2.0.0",
999 "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz",
1000 "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==",
1001 "license": "ISC"
1002 },
1003 "node_modules/noise-curve-ed": {
1004 "version": "2.1.0",
1005 "resolved": "https://registry.npmjs.org/noise-curve-ed/-/noise-curve-ed-2.1.0.tgz",
1006 "integrity": "sha512-zAzJx+VwZM3w6EA1hTmDhJfvAnCeBQn/1FAeZ0LtGxCcCtlAK/uJXQVF/eDVUOaAZ286lHlx77WJ+qj9SmsRRg==",
1007 "license": "ISC",
1008 "dependencies": {
1009 "b4a": "^1.1.0",
1010 "nanoassert": "^2.0.0",
1011 "sodium-universal": "^5.0.0"
1012 }
1013 },
1014 "node_modules/noise-curve-ed/node_modules/sodium-native": {
1015 "version": "5.0.10",
1016 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
1017 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
1018 "license": "MIT",
1019 "dependencies": {
1020 "require-addon": "^1.1.0",
1021 "which-runtime": "^1.2.1"
1022 },
1023 "engines": {
1024 "bare": ">=1.16.0"
1025 }
1026 },
1027 "node_modules/noise-curve-ed/node_modules/sodium-universal": {
1028 "version": "5.0.1",
1029 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1030 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1031 "license": "MIT",
1032 "dependencies": {
1033 "sodium-native": "^5.0.1"
1034 },
1035 "peerDependencies": {
1036 "sodium-javascript": "~0.8.0"
1037 },
1038 "peerDependenciesMeta": {
1039 "sodium-javascript": {
1040 "optional": true
1041 }
1042 }
1043 },
1044 "node_modules/noise-handshake": {
1045 "version": "4.2.0",
1046 "resolved": "https://registry.npmjs.org/noise-handshake/-/noise-handshake-4.2.0.tgz",
1047 "integrity": "sha512-9O/VTNX/E2/AToyMTTDU0J/4WhaXMTdqc2DHs9vf+snoZ0cenSBq0dNYTVV1snYYEkmo6QeRrYMxtqtoYnY+LA==",
1048 "license": "Apache-2.0",
1049 "dependencies": {
1050 "b4a": "^1.1.0",
1051 "nanoassert": "^2.0.0",
1052 "sodium-universal": "^5.0.0"
1053 }
1054 },
1055 "node_modules/noise-handshake/node_modules/sodium-native": {
1056 "version": "5.0.10",
1057 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
1058 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
1059 "license": "MIT",
1060 "dependencies": {
1061 "require-addon": "^1.1.0",
1062 "which-runtime": "^1.2.1"
1063 },
1064 "engines": {
1065 "bare": ">=1.16.0"
1066 }
1067 },
1068 "node_modules/noise-handshake/node_modules/sodium-universal": {
1069 "version": "5.0.1",
1070 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1071 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1072 "license": "MIT",
1073 "dependencies": {
1074 "sodium-native": "^5.0.1"
1075 },
1076 "peerDependencies": {
1077 "sodium-javascript": "~0.8.0"
1078 },
1079 "peerDependenciesMeta": {
1080 "sodium-javascript": {
1081 "optional": true
1082 }
1083 }
1084 },
1085 "node_modules/paparam": {
1086 "version": "1.10.0",
1087 "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.0.tgz",
1088 "integrity": "sha512-p36QQrwU3X6fj5d2JN20B4lcZI/O3fxMVUHc1xD7MNe5bcOf3jIgE0CIK5Zv/s5YAUGlobTJ/kKXrh1FJVrs3w==",
1089 "dev": true,
1090 "license": "Apache-2.0"
1091 },
1092 "node_modules/picomatch": {
1093 "version": "4.0.3",
1094 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
1095 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
1096 "dev": true,
1097 "license": "MIT",
1098 "engines": {
1099 "node": ">=12"
1100 },
1101 "funding": {
1102 "url": "https://github.com/sponsors/jonschlinkert"
1103 }
1104 },
1105 "node_modules/protocol-buffers-encodings": {
1106 "version": "1.2.0",
1107 "resolved": "https://registry.npmjs.org/protocol-buffers-encodings/-/protocol-buffers-encodings-1.2.0.tgz",
1108 "integrity": "sha512-daeNPuKh1NlLD1uDfbLpD+xyUTc07nEtfHwmBZmt/vH0B7VOM+JOCOpDcx9ZRpqHjAiIkGqyTDi+wfGSl17R9w==",
1109 "license": "MIT",
1110 "dependencies": {
1111 "b4a": "^1.6.0",
1112 "signed-varint": "^2.0.1",
1113 "varint": "5.0.0"
1114 }
1115 },
1116 "node_modules/protomux": {
1117 "version": "3.10.1",
1118 "resolved": "https://registry.npmjs.org/protomux/-/protomux-3.10.1.tgz",
1119 "integrity": "sha512-jgBqx8ZyaBWea/DFG4eOu1scOaeBwcnagiRC1XFVrjeGt7oAb0Pk5udPpBUpJ4DJBRjra50jD6YcZiQQTRqaaA==",
1120 "license": "MIT",
1121 "dependencies": {
1122 "b4a": "^1.3.1",
1123 "compact-encoding": "^2.5.1",
1124 "queue-tick": "^1.0.0",
1125 "safety-catch": "^1.0.1",
1126 "unslab": "^1.3.0"
1127 }
1128 },
1129 "node_modules/queue-tick": {
1130 "version": "1.0.1",
1131 "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
1132 "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
1133 "license": "MIT"
1134 },
1135 "node_modules/quickbit-native": {
1136 "version": "2.4.8",
1137 "resolved": "https://registry.npmjs.org/quickbit-native/-/quickbit-native-2.4.8.tgz",
1138 "integrity": "sha512-FcCcqI+nIAWGknqhtrYT5TSD7t/N+Xd8ctM+2PrIIBuwOi5hx0SxAvuPtzLIEMfT/2h9+fhBakUe2uALOHX6yw==",
1139 "license": "Apache-2.0",
1140 "optional": true,
1141 "dependencies": {
1142 "require-addon": "^1.1.0"
1143 }
1144 },
1145 "node_modules/quickbit-universal": {
1146 "version": "2.2.0",
1147 "resolved": "https://registry.npmjs.org/quickbit-universal/-/quickbit-universal-2.2.0.tgz",
1148 "integrity": "sha512-w02i1R8n7+6pEKTud8DfF8zbFY9o7RtPlUc3jWbtCkDKvhbx/AvV7oNnz4/TcmsPGpSJS+fq5Ud6RH6+YPvSGg==",
1149 "license": "ISC",
1150 "dependencies": {
1151 "b4a": "^1.6.0",
1152 "simdle-universal": "^1.1.0"
1153 },
1154 "optionalDependencies": {
1155 "quickbit-native": "^2.2.0"
1156 }
1157 },
1158 "node_modules/rache": {
1159 "version": "1.0.0",
1160 "resolved": "https://registry.npmjs.org/rache/-/rache-1.0.0.tgz",
1161 "integrity": "sha512-e0k0g0w/8jOCB+7YqCIlOa+OJ38k0wrYS4x18pMSmqOvLKoyhmMhmQyCcvfY6VaP8D75cqkEnlakXs+RYYLqNg==",
1162 "license": "Apache-2.0"
1163 },
1164 "node_modules/random-access-file": {
1165 "version": "4.1.2",
1166 "resolved": "https://registry.npmjs.org/random-access-file/-/random-access-file-4.1.2.tgz",
1167 "integrity": "sha512-GQM6R78DceZDcQod8KxlDFwXIiUvlvuy1EOzxTDsjuDjW5NlnlZi0MOk6iI4itAj/2vcvdqcEExYbVpC/dJcEw==",
1168 "license": "MIT",
1169 "dependencies": {
1170 "bare-fs": "^4.0.1",
1171 "bare-path": "^3.0.0",
1172 "random-access-storage": "^3.0.0"
1173 },
1174 "optionalDependencies": {
1175 "fs-native-extensions": "^1.3.1"
1176 }
1177 },
1178 "node_modules/random-access-storage": {
1179 "version": "3.0.2",
1180 "resolved": "https://registry.npmjs.org/random-access-storage/-/random-access-storage-3.0.2.tgz",
1181 "integrity": "sha512-Es9maUyWdJXWKckKy9s1+vT+DEgAt+PBb9lxPaake/0EDUsHehloKGv9v1zimS2V3gpFAcQXubvc1Rgci2sDPQ==",
1182 "license": "MIT",
1183 "dependencies": {
1184 "bare-events": "^2.2.0",
1185 "queue-tick": "^1.0.0"
1186 }
1187 },
1188 "node_modules/random-array-iterator": {
1189 "version": "1.0.0",
1190 "resolved": "https://registry.npmjs.org/random-array-iterator/-/random-array-iterator-1.0.0.tgz",
1191 "integrity": "sha512-u7xCM93XqKEvPTP6xZp2ehttcAemKnh73oKNf1FvzuVCfpt6dILDt1Kxl1LeBjm2iNIeR49VGFhy4Iz3yOun+Q==",
1192 "license": "MIT"
1193 },
1194 "node_modules/read-write-mutexify": {
1195 "version": "2.1.0",
1196 "resolved": "https://registry.npmjs.org/read-write-mutexify/-/read-write-mutexify-2.1.0.tgz",
1197 "integrity": "sha512-fDw/p5/acI1ytVY1UbxEDma/ej1yJH/n9NcjS9YNzcE6sPBPWdlru3ydRa/UBowUg4zqOvNMD5SOGYJrlQ6MzQ==",
1198 "license": "MIT"
1199 },
1200 "node_modules/ready-resource": {
1201 "version": "1.2.0",
1202 "resolved": "https://registry.npmjs.org/ready-resource/-/ready-resource-1.2.0.tgz",
1203 "integrity": "sha512-nfcco/8iAFV0M+2PYnmIc+/xY0iRb35d42HFHQ7AfjulbGEAFa+XWpByfwSyeVeiBoMLLFVMv1HixxNCqzSQ1g==",
1204 "license": "MIT",
1205 "dependencies": {
1206 "bare-events": "^2.2.0"
1207 }
1208 },
1209 "node_modules/require-addon": {
1210 "version": "1.2.0",
1211 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
1212 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
1213 "license": "Apache-2.0",
1214 "dependencies": {
1215 "bare-addon-resolve": "^1.3.0"
1216 },
1217 "engines": {
1218 "bare": ">=1.10.0"
1219 }
1220 },
1221 "node_modules/resolve-reject-promise": {
1222 "version": "1.1.0",
1223 "resolved": "https://registry.npmjs.org/resolve-reject-promise/-/resolve-reject-promise-1.1.0.tgz",
1224 "integrity": "sha512-LWsTOA91AqzBTjSGgX79Tc130pwcBK6xjpJEO+qRT5IKZ6bGnHKcc8QL3upUBcWuU8OTIDzKK2VNSwmmlqvAVg==",
1225 "license": "MIT"
1226 },
1227 "node_modules/safety-catch": {
1228 "version": "1.0.2",
1229 "resolved": "https://registry.npmjs.org/safety-catch/-/safety-catch-1.0.2.tgz",
1230 "integrity": "sha512-C1UYVZ4dtbBxEtvOcpjBaaD27nP8MlvyAQEp2fOTOEe6pfUpk1cDUxij6BR1jZup6rSyUTaBBplK7LanskrULA==",
1231 "license": "MIT"
1232 },
1233 "node_modules/same-object": {
1234 "version": "1.0.2",
1235 "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz",
1236 "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==",
1237 "dev": true,
1238 "license": "MIT"
1239 },
1240 "node_modules/signed-varint": {
1241 "version": "2.0.1",
1242 "resolved": "https://registry.npmjs.org/signed-varint/-/signed-varint-2.0.1.tgz",
1243 "integrity": "sha512-abgDPg1106vuZZOvw7cFwdCABddfJRz5akcCcchzTbhyhYnsG31y4AlZEgp315T7W3nQq5P4xeOm186ZiPVFzw==",
1244 "license": "MIT",
1245 "dependencies": {
1246 "varint": "~5.0.0"
1247 }
1248 },
1249 "node_modules/simdle-native": {
1250 "version": "1.3.9",
1251 "resolved": "https://registry.npmjs.org/simdle-native/-/simdle-native-1.3.9.tgz",
1252 "integrity": "sha512-Isc8sP4OiiIU0mpslD4GHEnR0VQWvR/54WN7YtwEDkNdTJVWtpmvsSvsgRlw5BNGxdYXlVRegdnrSu10H/PhvA==",
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 "license": "ISC",
1265 "dependencies": {
1266 "b4a": "^1.6.0"
1267 },
1268 "optionalDependencies": {
1269 "simdle-native": "^1.1.1"
1270 }
1271 },
1272 "node_modules/sodium-native": {
1273 "version": "4.3.3",
1274 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz",
1275 "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==",
1276 "license": "MIT",
1277 "dependencies": {
1278 "require-addon": "^1.1.0"
1279 }
1280 },
1281 "node_modules/sodium-secretstream": {
1282 "version": "1.2.0",
1283 "resolved": "https://registry.npmjs.org/sodium-secretstream/-/sodium-secretstream-1.2.0.tgz",
1284 "integrity": "sha512-q/DbraNFXm1KfCiiZvapmz5UC3OlpirYFIvBK2MhGaOFSb3gRyk8OXTi17UI9SGfshQNCpsVvlopogbzZNyW6Q==",
1285 "license": "MIT",
1286 "dependencies": {
1287 "b4a": "^1.1.1",
1288 "sodium-universal": "^5.0.0"
1289 }
1290 },
1291 "node_modules/sodium-secretstream/node_modules/sodium-native": {
1292 "version": "5.0.10",
1293 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
1294 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
1295 "license": "MIT",
1296 "dependencies": {
1297 "require-addon": "^1.1.0",
1298 "which-runtime": "^1.2.1"
1299 },
1300 "engines": {
1301 "bare": ">=1.16.0"
1302 }
1303 },
1304 "node_modules/sodium-secretstream/node_modules/sodium-universal": {
1305 "version": "5.0.1",
1306 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1307 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1308 "license": "MIT",
1309 "dependencies": {
1310 "sodium-native": "^5.0.1"
1311 },
1312 "peerDependencies": {
1313 "sodium-javascript": "~0.8.0"
1314 },
1315 "peerDependenciesMeta": {
1316 "sodium-javascript": {
1317 "optional": true
1318 }
1319 }
1320 },
1321 "node_modules/sodium-universal": {
1322 "version": "4.0.1",
1323 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-4.0.1.tgz",
1324 "integrity": "sha512-sNp13PrxYLaUFHTGoDKkSDFvoEu51bfzE12RwGlqU1fcrkpAOK0NvizaJzOWV0Omtk9me2+Pnbjcf/l0efxuGQ==",
1325 "license": "MIT",
1326 "dependencies": {
1327 "sodium-native": "^4.0.0"
1328 },
1329 "peerDependencies": {
1330 "sodium-javascript": "~0.8.0"
1331 },
1332 "peerDependenciesMeta": {
1333 "sodium-javascript": {
1334 "optional": true
1335 }
1336 }
1337 },
1338 "node_modules/stackframe": {
1339 "version": "1.3.4",
1340 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
1341 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
1342 "dev": true,
1343 "license": "MIT"
1344 },
1345 "node_modules/streamx": {
1346 "version": "2.23.0",
1347 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
1348 "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
1349 "license": "MIT",
1350 "dependencies": {
1351 "events-universal": "^1.0.0",
1352 "fast-fifo": "^1.3.2",
1353 "text-decoder": "^1.1.0"
1354 }
1355 },
1356 "node_modules/test-tmp": {
1357 "version": "1.4.0",
1358 "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz",
1359 "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==",
1360 "dev": true,
1361 "license": "MIT",
1362 "dependencies": {
1363 "bare-fs": "^4.0.1",
1364 "bare-os": "^3.3.0",
1365 "bare-path": "^3.0.0"
1366 }
1367 },
1368 "node_modules/text-decoder": {
1369 "version": "1.2.6",
1370 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.6.tgz",
1371 "integrity": "sha512-27FeW5GQFDfw0FpwMQhMagB7BztOOlmjcSRi97t2oplhKVTZtp0DZbSegSaXS5IIC6mxMvBG4AR1Sgc6BX3CQg==",
1372 "license": "Apache-2.0",
1373 "dependencies": {
1374 "b4a": "^1.6.4"
1375 }
1376 },
1377 "node_modules/timeout-refresh": {
1378 "version": "2.0.1",
1379 "resolved": "https://registry.npmjs.org/timeout-refresh/-/timeout-refresh-2.0.1.tgz",
1380 "integrity": "sha512-SVqEcMZBsZF9mA78rjzCrYrUs37LMJk3ShZ851ygZYW1cMeIjs9mL57KO6Iv5mmjSQnOe/29/VAfGXo+oRCiVw==",
1381 "license": "MIT"
1382 },
1383 "node_modules/tmatch": {
1384 "version": "5.0.0",
1385 "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz",
1386 "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==",
1387 "dev": true,
1388 "license": "ISC",
1389 "engines": {
1390 "node": ">=8"
1391 }
1392 },
1393 "node_modules/unslab": {
1394 "version": "1.3.0",
1395 "resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz",
1396 "integrity": "sha512-YATkfKAFj47kTzmiQrWXMyRvaVrHsW6MEALa4bm+FhiA2YG4oira+Z3DXN6LrYOYn2Y8eO94Lwl9DOHjs1FpoQ==",
1397 "license": "Apache-2.0",
1398 "dependencies": {
1399 "b4a": "^1.6.6"
1400 }
1401 },
1402 "node_modules/v8-to-istanbul": {
1403 "version": "9.3.0",
1404 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
1405 "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
1406 "dev": true,
1407 "license": "ISC",
1408 "dependencies": {
1409 "@jridgewell/trace-mapping": "^0.3.12",
1410 "@types/istanbul-lib-coverage": "^2.0.1",
1411 "convert-source-map": "^2.0.0"
1412 },
1413 "engines": {
1414 "node": ">=10.12.0"
1415 }
1416 },
1417 "node_modules/varint": {
1418 "version": "5.0.0",
1419 "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz",
1420 "integrity": "sha512-gC13b/bWrqQoKY2EmROCZ+AR0jitc6DnDGaQ6Ls9QpKmuSgJB1eQ7H3KETtQm7qSdMWMKCmsshyCmUwMLh3OAA==",
1421 "license": "MIT"
1422 },
1423 "node_modules/which-runtime": {
1424 "version": "1.3.2",
1425 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
1426 "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
1427 "license": "Apache-2.0"
1428 },
1429 "node_modules/xache": {
1430 "version": "1.2.1",
1431 "resolved": "https://registry.npmjs.org/xache/-/xache-1.2.1.tgz",
1432 "integrity": "sha512-igRS6jPreJ54ABdzhh4mCDXcz+XMaWO2q1ABRV2yWYuk29jlp8VT7UBdCqNkX7rpYBbXsebVVKkwIuYZjyZNqA==",
1433 "license": "MIT"
1434 },
1435 "node_modules/z32": {
1436 "version": "1.1.0",
1437 "resolved": "https://registry.npmjs.org/z32/-/z32-1.1.0.tgz",
1438 "integrity": "sha512-1WUHy+VS6d0HPNspDxvLssBbeQjXMjSnpv0vH82vRAUfg847NmX3OXozp/hRP5jPhxBbrVzrgvAt+UsGNzRFQQ==",
1439 "license": "MIT",
1440 "dependencies": {
1441 "b4a": "^1.5.3"
1442 }
1443 }
1444 }
1445}
bridge-vault/package.json +22 -0
……@@ -0,0 +1,22 @@
1{
2 "name": "bridge-vault",
3 "version": "1.0.0",
4 "description": "Encrypted P2P credential store for bridge configurations",
5 "type": "module",
6 "main": "index.js",
7 "scripts": {
8 "test": "brittle test/*.test.js"
9 },
10 "dependencies": {
11 "hyperbee": "^2.0.0",
12 "hypercore": "^10.0.0",
13 "corestore": "^6.0.0",
14 "sodium-native": "^4.0.0",
15 "b4a": "^1.0.0",
16 "compact-encoding": "^2.0.0"
17 },
18 "devDependencies": {
19 "brittle": "^3.0.0"
20 },
21 "license": "MIT"
22}
bridge-vault/test/vault.test.js +214 -0
……@@ -0,0 +1,214 @@
1import test from 'brittle'
2import { Vault, generateKey, deriveKey } from '../index.js'
3import b4a from 'b4a'
4import { tmpdir } from 'os'
5import { join } from 'path'
6import { randomBytes } from 'crypto'
7
8function tmpStorage () {
9 return join(tmpdir(), 'bridge-vault-test-' + randomBytes(8).toString('hex'))
10}
11
12test('Vault - put and get credential', async (t) => {
13 const vault = new Vault(tmpStorage())
14 await vault.ready()
15
16 await vault.put('irc', 'libera', {
17 server: 'irc.libera.chat',
18 port: 6697,
19 nick: 'testbot',
20 password: 'secret123'
21 })
22
23 const cred = await vault.get('irc', 'libera')
24 t.is(cred.server, 'irc.libera.chat')
25 t.is(cred.port, 6697)
26 t.is(cred.nick, 'testbot')
27 t.is(cred.password, 'secret123')
28
29 await vault.close()
30})
31
32test('Vault - get nonexistent returns null', async (t) => {
33 const vault = new Vault(tmpStorage())
34 await vault.ready()
35
36 const cred = await vault.get('irc', 'nonexistent')
37 t.is(cred, null)
38
39 await vault.close()
40})
41
42test('Vault - delete credential', async (t) => {
43 const vault = new Vault(tmpStorage())
44 await vault.ready()
45
46 await vault.put('discord', 'mybot', { token: 'abc123' })
47 t.ok(await vault.get('discord', 'mybot'))
48
49 await vault.del('discord', 'mybot')
50 t.is(await vault.get('discord', 'mybot'), null)
51
52 await vault.close()
53})
54
55test('Vault - update credential', async (t) => {
56 const vault = new Vault(tmpStorage())
57 await vault.ready()
58
59 await vault.put('irc', 'server1', { nick: 'old' })
60 await vault.put('irc', 'server1', { nick: 'new' })
61
62 const cred = await vault.get('irc', 'server1')
63 t.is(cred.nick, 'new')
64
65 await vault.close()
66})
67
68test('Vault - list credentials for platform', async (t) => {
69 const vault = new Vault(tmpStorage())
70 await vault.ready()
71
72 await vault.put('irc', 'libera', { nick: 'bot1' })
73 await vault.put('irc', 'oftc', { nick: 'bot2' })
74 await vault.put('discord', 'server1', { token: 'xyz' })
75
76 const ircCreds = await vault.list('irc')
77 t.is(ircCreds.length, 2)
78 t.ok(ircCreds.find(c => c.name === 'libera'))
79 t.ok(ircCreds.find(c => c.name === 'oftc'))
80
81 const discordCreds = await vault.list('discord')
82 t.is(discordCreds.length, 1)
83 t.is(discordCreds[0].name, 'server1')
84
85 await vault.close()
86})
87
88test('Vault - listPlatforms', async (t) => {
89 const vault = new Vault(tmpStorage())
90 await vault.ready()
91
92 await vault.put('irc', 'a', { x: 1 })
93 await vault.put('discord', 'b', { x: 2 })
94 await vault.put('matrix', 'c', { x: 3 })
95
96 const platforms = await vault.listPlatforms()
97 t.is(platforms.length, 3)
98 t.ok(platforms.includes('irc'))
99 t.ok(platforms.includes('discord'))
100 t.ok(platforms.includes('matrix'))
101
102 await vault.close()
103})
104
105test('Vault - encryption works (wrong key fails)', async (t) => {
106 const storage = tmpStorage()
107 const key1 = generateKey()
108 const key2 = generateKey()
109
110 const vault1 = new Vault(storage, { key: key1 })
111 await vault1.ready()
112 await vault1.put('irc', 'test', { secret: 'hidden' })
113 await vault1.close()
114
115 const vault2 = new Vault(storage, { key: key2 })
116 await vault2.ready()
117
118 try {
119 await vault2.get('irc', 'test')
120 t.fail('should have thrown')
121 } catch (err) {
122 t.ok(err.message.includes('Decryption failed'), 'wrong key throws')
123 }
124
125 await vault2.close()
126})
127
128test('Vault - same key reopens vault', async (t) => {
129 const storage = tmpStorage()
130 const key = generateKey()
131
132 const vault1 = new Vault(storage, { key })
133 await vault1.ready()
134 await vault1.put('irc', 'test', { value: 42 })
135 await vault1.close()
136
137 const vault2 = new Vault(storage, { key })
138 await vault2.ready()
139 const cred = await vault2.get('irc', 'test')
140 t.is(cred.value, 42)
141 await vault2.close()
142})
143
144test('Vault - complex credential values', async (t) => {
145 const vault = new Vault(tmpStorage())
146 await vault.ready()
147
148 await vault.put('mastodon', 'instance1', {
149 baseUrl: 'https://mastodon.social',
150 accessToken: 'token123',
151 scopes: ['read', 'write', 'push'],
152 metadata: {
153 createdAt: '2024-01-01',
154 expiresAt: null,
155 refreshToken: 'refresh456'
156 }
157 })
158
159 const cred = await vault.get('mastodon', 'instance1')
160 t.is(cred.baseUrl, 'https://mastodon.social')
161 t.alike(cred.scopes, ['read', 'write', 'push'])
162 t.is(cred.metadata.refreshToken, 'refresh456')
163 t.is(cred.metadata.expiresAt, null)
164
165 await vault.close()
166})
167
168test('generateKey - returns buffer of correct size', (t) => {
169 const key = generateKey()
170 t.ok(b4a.isBuffer(key))
171 t.is(key.length, 32)
172})
173
174test('deriveKey - derives from password', (t) => {
175 const { key, salt } = deriveKey('my-password')
176 t.ok(b4a.isBuffer(key))
177 t.is(key.length, 32)
178 t.ok(b4a.isBuffer(salt))
179})
180
181test('deriveKey - same password + salt gives same key', (t) => {
182 const { key: key1, salt } = deriveKey('password')
183 const { key: key2 } = deriveKey('password', salt)
184 t.ok(b4a.equals(key1, key2))
185})
186
187test('deriveKey - different passwords give different keys', (t) => {
188 const { key: key1, salt } = deriveKey('password1')
189 const { key: key2 } = deriveKey('password2', salt)
190 t.absent(b4a.equals(key1, key2))
191})
192
193test('Vault - empty list returns empty array', async (t) => {
194 const vault = new Vault(tmpStorage())
195 await vault.ready()
196
197 const creds = await vault.list('nonexistent')
198 t.alike(creds, [])
199
200 await vault.close()
201})
202
203test('Vault - ready is idempotent', async (t) => {
204 const vault = new Vault(tmpStorage())
205 await vault.ready()
206 await vault.ready()
207 await vault.ready()
208
209 await vault.put('irc', 'test', { ok: true })
210 const cred = await vault.get('irc', 'test')
211 t.is(cred.ok, true)
212
213 await vault.close()
214})
docs/ARCHITECTURE.md +185 -0
……@@ -0,0 +1,185 @@
1# Architecture
2
3## The Communication Graph
4
5The core idea: a P2P communication graph where each bridge is a **plane** connecting two (or more) spaces. There is no single bridge — there are many. The system is the graph itself.
6
7```
8 ┌─────────┐
9 ┌────│ Keet │────┐
10 │ └─────────┘ │
11 │ │ │
12┌───┴───┐ ┌──┴──┐ ┌────┴────┐
13│ IRC │ │Disc.│ │Mastodon │
14│ plane │ │plane│ │ plane │
15└───┬───┘ └──┬──┘ └────┬────┘
16 │ │ │
17┌───┴───┐ │ ┌────┴────┐
18│Libera │ │ │Fediverse│
19│ Chat │ │ └─────────┘
20└───────┘ ┌─┴──────┐
21 │Discord │
22 │servers │
23 └────────┘
24```
25
26Each bridge is a standalone process. Keet is the P2P hub (because it's already P2P), but any bridge can connect any two spaces.
27
28**No single point of failure.** Kill one bridge, others keep working. Add a new bridge, the graph grows. Run bridges on different machines — the graph is distributed.
29
30## The Fracture Principle
31
32Any "universal bridge" eventually breaks into many bridges. Design for this from day one:
33
34- Each bridge is independently useful and independently deployable
35- Each panel is a standalone app that adds a capability
36- Each daemon is a standalone automation script
37- The system is the emergent property of all pieces running together
38- No central registry — bridges discover each other via Hyperswarm
39
40## Five Tiers
41
42### Tier 1: Foundation Libraries (npm packages)
43Small, focused packages that all other projects import.
44
45- **bridge-envelope** — Universal message schema + compact-encoding codec
46- **bridge-vault** — Encrypted P2P credential store (Hyperbee + keypair encryption)
47
48### Tier 2: Platform Bridges (Pear terminal apps)
49Each bridge connects one external platform to Keet. Standalone.
50
51- **bridge-irc** — IRC channels <-> Keet rooms
52- **bridge-discord** — Discord channels <-> Keet rooms
53- **bridge-matrix** — Matrix rooms <-> Keet rooms
54- **bridge-mastodon** — Fediverse posts <-> Keet rooms
55- **bridge-email** — IMAP/SMTP inboxes <-> Keet rooms
56
57### Tier 3: Workspace Panels (Pear apps)
58Each panel adds a social capability. Can attach to a Keet room or run standalone.
59
60- **hyper-wiki** — P2P wiki (Hyperdrive-backed, versioned)
61- **hyper-board** — P2P kanban board
62- **hyper-feed** — P2P social feed/timeline
63
64### Tier 4: Automation (Pear apps)
65Event-driven automation attached to rooms.
66
67- **hyper-daemon** — Trigger-action runtime (like IFTTT but P2P)
68 - daemon-crosspost — selective syndication across platforms
69 - daemon-summarize — auto-summarize conversations into wiki pages
70
71### Tier 5: Meta-App
72Composes everything into a unified experience.
73
74- **hyper-social** — unified hub with tiling WM layout for panels
75
76## Dependency Graph
77
78```
79bridge-envelope (npm pkg, no deps)
80 │
81 ├──────────────────────────────┐
82 │ │
83bridge-vault (npm pkg) Each bridge/panel/daemon app
84 │
85 └──── hyper-social (meta-app, composes all above)
86```
87
88**Dependency arrows go DOWN, not up.** Each project depends on at most `bridge-envelope` + `bridge-vault`. No circular dependencies. Each project is a leaf node except the meta-app.
89
90## Hub API: WebSocket + JSON-RPC 2.0
91
92All projects talk to keet-daemon via a single protocol:
93
94```
95┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐
96│ WebUI │ │ TUI │ │ GUI │ │ Godot │
97│ (browser)│ │(terminal)│ │ (Pear) │ │ ┌───────┐ ┌──────┐ │
98│ │ │ │ │ │ │ │ WS │ │ UDX │ │
99│ │ │ │ │ │ │ │(social)│ │(game)│ │
100└────┬─────┘ └────┬─────┘ └────┬─────┘ │ └───┬───┘ └──┬───┘ │
101 │ │ │ └──────┼────────┼─────┘
102 │ │ │ │ │
103 └──────┬──────┘──────┬──────┘───────────────┘ │
104 │ │ │
105 ┌──────┴─────────────┴───────────────┐ ┌─────────┴──────┐
106 │ keet-daemon WebSocket API │ │ Hyperswarm │
107 │ ws://localhost:3117/ws │ │ (direct P2P) │
108 │ JSON-RPC 2.0 │ │ via UDX/UDP │
109 └────────────────────────────────────┘ └────────────────┘
110```
111
112See [NETWORKING.md](NETWORKING.md) for protocol details.
113See [GODOT.md](GODOT.md) for the two-tier Godot architecture.
114
115## Panel Architecture (Modular Rooms)
116
117From the Keet conversation: "like a tiling WM but for different social networks."
118
119Each panel is a widget that can be composed into a room view:
120
121```
122┌─────────────────────────────────────────────┐
123│ Room: AI Town │
124├──────────────────┬──────────────────────────┤
125│ │ │
126│ Chat Panel │ Wiki Panel │
127│ (real-time │ (collaborative │
128│ messages) │ notes) │
129│ │ │
130├──────────────────┼──────────────────────────┤
131│ │ │
132│ Feed Panel │ Board Panel │
133│ (aggregated │ (kanban tasks) │
134│ timeline) │ │
135│ │ │
136└──────────────────┴──────────────────────────┘
137```
138
139Each panel type has:
140- A **data source** (hyper-wiki store, hyper-board store, hyper-feed store)
141- A **renderer** per interface type (HTML div, TUI box, Godot Control/Node2D/Node3D)
142- A **controller** that handles user input
143- An **API endpoint** on the hub
144
145The tiling layout is configurable per room:
146- Chat-only room = single panel
147- Wiki room = chat + wiki side by side
148- Project room = chat + board + wiki
149- Feed room = feed + chat
150
151## Interface Types
152
153| Interface | Best for | Technology |
154|-----------|----------|------------|
155| WebUI | Dashboards, config, rich content | HTML/CSS/JS, browser |
156| TUI | Power users, SSH, quick status | Raw ANSI (like keet-tui) |
157| GUI | Always-on hub, system tray | Pear desktop app |
158| Godot 2D | Chat apps, feed readers, 2D games | GDScript + WebSocketPeer |
159| Godot 3D | Social spaces, 3D games | GDScript + GDExtension |
160
161All interfaces share the same backend via WebSocket + JSON-RPC 2.0.
162
163## Holepunch Conventions
164
165Every project follows Holepunch's patterns:
166
167```
168project-name/
169├── package.json # "type": "module", pear config
170├── index.js # Main export / entry point
171├── lib/ # Implementation modules
172│ ├── feature-a.js # kebab-case filenames
173│ └── feature-b.js # PascalCase classes, camelCase functions
174├── test/ # brittle tests
175│ └── basic.test.js
176└── README.md
177```
178
179**Naming:**
180- Bridges: `bridge-<platform>` (bridge-irc, bridge-discord)
181- Panels/tools: `hyper-<function>` (hyper-wiki, hyper-board)
182- Shared libs: `bridge-<function>` (bridge-envelope, bridge-vault)
183- Meta-app: `hyper-social`
184
185**Dependencies:** Each project symlinks `node_modules` from keet-cli (zero installs for Pear deps). Only platform-specific deps are new (e.g., `irc-framework` for bridge-irc).
docs/COMMUNITY-EMERGENCE.md +622 -0
……@@ -0,0 +1,622 @@
1# Community Emergence, Governance, and Privacy
2
3How communities form, govern themselves, and maintain sovereignty in a decentralized social graph. Companion to [SOCIAL-GRAPH-NAVIGATION.md](SOCIAL-GRAPH-NAVIGATION.md) — that document covers the *what* (graph navigation, sheaf theory, parsing pipeline). This document covers the *how* and the *should we*.
4
5## Case Study: Blacksky
6
7Before abstractions, a concrete example. **Blacksky** is the clearest demonstration of community emergence on a decentralized protocol.
8
9### What It Is
10
11A BIPOC community infrastructure project founded by **Rudy Fraser** — a mutual aid organizer from Queens, NY who became a self-taught programmer. Built on AT Protocol (same protocol as Bluesky) but running fully independent infrastructure:
12
13| Component | Blacksky's Instance |
14|-----------|-------------------|
15| Personal Data Server | `blacksky.app` |
16| Full-network relay | `atproto.africa` (all 36M+ AT Protocol accounts) |
17| Moderation labeler | `@blackskyweb.xyz` |
18| Web client | `blacksky.community` |
19| Governance | People's Assembly (self-hosted Polis) |
20| Economic tools | Blacksky Cash, Papertree, GatherFor |
21
22**2.5 million feed users. 370K+ monthly active. $0 marketing. 100% organic growth.** Over 1,100 individual financial contributors through OpenCollective.
23
24The entire tech stack is called **rsky** (Rust AT Protocol implementation). It interoperates with Bluesky's infrastructure but is not dependent on it.
25
26Fraser's stated purpose: *"My purpose with Blacksky is not to be the next Black social network; it's to be the last one."*
27
28### How It Actually Emerged
29
30**Stage 1 — Intentional Seeding.** A software engineer named Aveta personally invited ~1,000 Black users to Bluesky using invite codes, requiring each invitee to have someone **vouch for their character**. Before her work, the platform was overwhelmingly white. She didn't just invite people — she created a trust network.
31
32**Stage 2 — Graph Detection.** Fraser scraped Aveta's follow trains — the social graph itself — to build the Blacksky feed algorithm. He didn't ask people to self-identify. He detected community structure from **interaction patterns**. Same math as our MI/clustering pipeline, applied to follow graphs instead of room membership.
33
34**Stage 3 — Naming.** The feed was called "Blacksky." The statistical cluster got a label. This is the critical moment — more on this below.
35
36**Stage 4 — Identity Formation.** People started identifying AS Blacksky members. The label became identity. Behavior changed — people posted differently knowing they'd appear on the Blacksky feed.
37
38**Stage 5 — Crisis-Driven Crystallization.** A death threat against Aveta went insufficiently addressed by Bluesky. CEO offered "composable moderation" as a solution. Fraser's response: that's outsourcing responsibility. Then he built those community safety tools himself — but better than anything the platform would provide. The criticism and the construction coexist. Separately, Bluesky allowed racial slurs in usernames. Users staged a posting strike.
39
40**Stage 6 — Infrastructure Sovereignty.** Own PDS, own relay, own moderation, own governance (People's Assembly), own migration tool (Tektite — move your identity with one click), own web client, own economic tools. The only difference between Blacksky and Bluesky became **jurisdiction** — Terms of Service, moderation policy, governance. The protocol layer remained shared.
41
42### Key Principles from Blacksky
43
44**Community = mutual accountability.** Fraser's definition: *"Community really means mutual accountability between the one and the many."* Not a member list. Not a set of rooms. A pattern of accountability that persists.
45
46**Moderation = shared context.** *"Context-aware, Black moderators moderating anti-Black content. Our moderators must have a shared understanding of anti-Blackness, white supremacy, internalized racism, and how online misogynoir can lead to violence in the real world."* Not rules. Context.
47
48**Safety = infrastructure.** *"An internet where marginalized users are safe is an internet where all users are safe."* Safety is not a policy layer — it's the infrastructure itself.
49
50**Economic sovereignty = community sovereignty.** Fraser built Papertree (mutual aid pooling) and GatherFor (rotating savings circles from West African and Caribbean traditions) BEFORE Blacksky. The economic infrastructure preceded the social infrastructure. Over 3.5 years, these tools facilitated over $250,000 in community funds.
51
52**Governance through deliberation, not voting.** The People's Assembly uses Polis — an open-source tool where members submit statements, vote on others' statements, and machine learning finds clusters of agreement. Not majority rule — consensus-finding. Day-to-day moderation is handled by the team using shared understanding, not rigid rules.
53
54## The Phase Transition Model
55
56A community is not a static object. It's a phase transition in a social system.
57
58### Supercooled Liquid → Crystal
59
60Below a certain density of interaction, there's no community — just people in rooms. Above that density, community crystallizes. The transition is not gradual. It's a phase change.
61
62```
63 ┌──────────────── Phase transition ──────────────┐
64 │ │
65People → Statistical → Named → Identity → Governance → Sovereignty
66in rooms pattern cluster formation emergence (infra control)
67 │ ↑ │
68 │ │ │
69 │ NUCLEATION POINT │
70 │ (someone names the pattern) │
71 └─────────────────────────────────────────────────────────────┘
72 Feedback loop
73```
74
75**The nucleation point is the naming act.** Aveta created the conditions (high-MI cluster). Fraser provided the seed crystal (the name "Blacksky"). The community crystallized around the name.
76
77In physics: supercooled liquid has all the structure needed for crystallization but needs a nucleation point (a seed crystal, a scratch on the container wall). In social: the membership overlap has all the structure needed for community — but needs someone to name it.
78
79### Implications for System Design
80
81**The graph navigation pipeline can detect supercooled liquid** — clusters of rooms with high MI, ready to crystallize. But the system should NOT automatically name clusters. Naming is a political act.
82
83```
84System detects: "These 12 rooms share 70% of their members"
85System surfaces: Visual cluster in graph view, proximity in sidebar
86System does NOT: Auto-label "Community #47" or suggest a name
87
88Human acts: Someone says "hey, we should have a registry for this"
89Human names: "Hypercore Dev" — the crystal forms
90Human governs: Registry governance room = community meta-room
91```
92
93**Emergence needs midwives, not managers.** The system creates conditions for nucleation. Humans decide when and whether to nucleate.
94
95## Three Levels of Connector Theory
96
97Room-level connectors were covered in [SOCIAL-GRAPH-NAVIGATION.md](SOCIAL-GRAPH-NAVIGATION.md). The full picture has three levels, each a sheaf on the level below.
98
99### Level 1: Room Connectors (Membership)
100
101```
102MI(Room_A, Room_B) = log2(P(A,B) / (P(A) × P(B)))
103
104Connector profile: pattern of MI with neighboring rooms
105A room's social "meaning" IS its connector set
106```
107
108Two rooms with high MI share members more than chance predicts. The connector profile captures a room's social role — a dev room connects differently than a social room.
109
110### Level 2: Community Connectors (Room Overlap Between Clusters)
111
112```
113MI(Community_X, Community_Y) = f(shared rooms between clusters)
114
115Bridge rooms ARE the connectors between communities.
116A room in two communities IS the connection.
117```
118
119Communities have connector profiles — what kinds of communities they connect to and how. A tech community connects to other tech communities through shared tooling rooms. A geographic community connects through location-based rooms.
120
121### Level 3: Meta-Community Connectors (Identity Overlap Across Platforms)
122
123```
124MI(MetaCom_on_Keet, MetaCom_on_Bluesky) = f(shared identities across bridges)
125
126Bridge USERS are the connectors at this level.
127People on both platforms ARE the cross-platform connection.
128```
129
130The same community existing on Keet and Bluesky is connected through the people who are on both. At Level 2, bridge rooms connect communities. At Level 3, bridge users connect meta-communities.
131
132### Cohomological Obstructions at Each Level
133
134| Level | Obstruction | Example |
135|-------|-------------|---------|
136| 1 (Rooms) | Rooms that don't cluster cleanly | A room serving two distinct purposes |
137| 2 (Communities) | Incompatible governance at overlaps | Two registries with conflicting moderation |
138| 3 (Meta-communities) | Platform features that don't translate | Discord reactions can't map to IRC |
139
140These aren't bugs. They're structural information about the social space.
141
142## Communities Are Living Languages
143
144This is not a metaphor. The mathematical structure is identical.
145
146| Language | Social |
147|----------|--------|
148| Word | Room |
149| Sentence | User's room list |
150| Grammar (descriptive) | Actual community norms |
151| Grammar (prescriptive) | Governance rules |
152| Speaker | Community member |
153| Dialect | Subgroup |
154| Language family | Community cluster |
155| Loanword | Norm imported from another community |
156| Pidgin | Bridge room norms (simplified cross-community) |
157| Creole | Mature bridge community (own norms, own identity) |
158| Compartment boundary | Community boundary |
159
160### Prescriptive vs Descriptive Grammar
161
162Every living language has this tension: how people ACTUALLY speak (descriptive) vs how people SAY you should speak (prescriptive). Dictionaries describe. Style guides prescribe. The tension is productive — it drives language evolution.
163
164Every community has the same tension: how people ACTUALLY behave (descriptive norms) vs governance rules (prescriptive norms). The parsing pipeline extracts descriptive norms from interaction data. Governance deliberation (People's Assembly, community votes) produces prescriptive norms. The tension between them drives community evolution.
165
166**The pipeline can detect prescriptive norms from descriptive data** — if a community consistently sanctions behavior X, "don't do X" is a norm regardless of whether it's written down. The meta-pattern of enforcement IS the grammar.
167
168### Pidgins and Creoles at Community Boundaries
169
170When two language communities meet, a pidgin forms — simplified rules for basic communication. If interaction continues, the pidgin becomes a creole — a full language with its own grammar, native speakers, literary tradition.
171
172**Blacksky is a social creole.** It emerged at the boundary between Black Twitter (community) and Bluesky (platform). The norms are neither Twitter norms nor Bluesky norms — they're Blacksky norms, born from the tension at the boundary.
173
174In hyper-social: **bridge rooms are pidgin spaces.** The norms are simplified — minimum needed for cross-community interaction. Over time, if interaction continues, bridge norms enrich. The bridge community becomes its own thing — a creole community with members from both sides but norms of its own.
175
176The IRC bridge to Keet will have pidgin norms initially. If the bridged communities continue interacting, those norms will develop into something richer. The bridge community (people active in both) will develop its own culture that is neither IRC culture nor Keet culture.
177
178## What Data We're Counting
179
180### Data That Exists (Per Node)
181
182Each node knows — and ONLY knows:
183
184```
185Your rooms → getRecentRooms()
186Members of YOUR rooms → getMemberList(roomId)
187Public room listings → discovery registries you follow
188```
189
190No node knows other people's room lists, membership of rooms they're not in, or the "full social graph." The global graph is a theoretical object that no one computes, sees, or stores.
191
192### The Local Co-occurrence Matrix
193
194Each user computes from their own data:
195
196```
197For each pair of rooms (A, B) I'm in:
198 overlap = members(A) ∩ members(B)
199 count[A][B] = |overlap|
200
201P(A) = |members(A)| / total_unique_members_I_see
202P(B) = |members(B)| / total_unique_members_I_see
203P(A,B) = |overlap| / total_unique_members_I_see
204
205MI(A, B) = log2(P(A,B) / (P(A) × P(B)))
206```
207
208Your MI values are YOUR view. Someone in different rooms gets different MI. **That's the sheaf** — each user is a local section with no requirement for global consistency.
209
210### Two Distinct Graphs
211
212**Room Topology Graph (Public)**
213- Which rooms exist in which discovery registries
214- Room metadata: name, topic, avatar (public by choice — rooms opt into registries)
215- Registry-to-room relationships, bridge rooms
216- Anyone following the registry can see this
217
218**Social/Membership Graph (Private, Local)**
219- Who's in what rooms (only YOUR rooms)
220- Membership overlap (only rooms YOU share)
221- MI, MST, clusters — all computed locally
222- Only YOU see YOUR graph
223
224### What We Never Count
225
226- Message content (never)
227- Individual behavior patterns or posting frequency (never)
228- Cross-room user tracking beyond shared rooms (never)
229- Timing correlations between rooms (never)
230- Reading patterns or lurking behavior (never)
231
232## Privacy Architecture
233
234### Privacy Through Architecture, Not Policy
235
236```
237Discord: "We PROMISE not to read your DMs" → Policy (breakable)
238Keet: "We CAN'T read your DMs" → Architecture (unbreakable)
239
240Discord: "We PROMISE not to sell your graph" → Policy (breakable)
241Us: "No one HAS your social graph" → Architecture (unbreakable)
242```
243
244The P2P architecture NATURALLY provides the privacy properties we want. There's no central server to surveil. Each node has a limited view. The social graph is inherently fragmented across nodes. This isn't a limitation — it's the design.
245
246### Inherent Information Leakage
247
248If you're in rooms A, B, C and user X is also in all three — you know X is in rooms A, B, C. This is unavoidable in any shared space. If you're at the same bar as someone, you know they're at that bar.
249
250```
251Level 0: I know X is in room A (we're both there) → Inherent
252Level 1: I know X is in rooms A and B (we share both) → Inherent
253Level 2: I can infer X is likely in similar rooms (MI) → Computed locally
254Level 3: I can model X's social neighborhood → Computed locally
255```
256
257Levels 0-1 are unavoidable in any social system. Levels 2-3 are what the computation enables — but only from data you already have. The computation doesn't create new information. It surfaces structure in existing information.
258
259### The Aggregation Attack
260
261The dangerous scenario:
262
263```
264User A sees: X in rooms {1, 2, 3}
265User B sees: X in rooms {4, 5, 6}
266User C sees: X in rooms {7, 8, 9}
267
268If A, B, C share data → they reconstruct X's full room list {1-9}
269```
270
271**Mitigation: Never aggregate.** The system has NO mechanism for combining local graphs. No "share your graph" feature. No "global statistics" endpoint. No API for exporting membership data. Each user's computation stays local.
272
273### The Observation Is an Intervention
274
275Detecting a community changes the community. Naming "Blacksky" changed how people self-identified and interacted. The feed algorithm changed behavior. Showing someone their social graph changes their social graph.
276
277The graph navigation system is not a neutral observation tool. It's a social intervention. The choice of what to surface, what to highlight, what to hide — these are design decisions that shape the social space. The tool participates in the dynamics it measures.
278
279**Design consequence:** Be explicit about what the tool shows and why. Don't pretend neutrality. Surface the choices.
280
281## Governance
282
283### What Governance Exists (Current System)
284
285| Layer | Governor | Scope | Power |
286|-------|----------|-------|-------|
287| Room | Room creator/admin | One room | Moderate content, manage members, set roles |
288| Registry | Autobase writers | Room listings | Add/remove rooms from discovery index |
289
290A room admin is a **bartender, not a surveillance state.** They see what happens in their bar. They don't follow you home. They don't know what other bars you go to.
291
292A registry admin has even LESS power — they curate listings (which rooms to show), but can't see room content, room membership, or anything inside the rooms they index.
293
294### How Governance Emerges
295
296**Step 1: Interaction density crosses threshold.** People are in rooms. Membership overlap creates high-MI clusters. No governance yet — just statistical structure.
297
298**Step 2: Someone names the cluster.** A user or group creates a discovery registry. The cluster gets a label. This is the nucleation point.
299
300**Step 3: Naming creates identity.** People start identifying with the label. "I'm part of the Hypercore Dev community." Behavior shifts.
301
302**Step 4: Identity creates accountability.** "We don't do that here." Norms emerge from shared identity. Not written rules — behavioral patterns. The descriptive grammar of the community.
303
304**Step 5: Accountability creates governance.** Norms are formalized. Moderation policies are written. Deliberation processes are established. The prescriptive grammar of the community.
305
306**Step 6: Governance creates infrastructure.** The community builds or adopts moderation services, economic tools, communication infrastructure. Sovereignty follows from infrastructure control.
307
308This sequence can't be shortcut. You can't impose governance on a community that hasn't formed identity. You can't form identity around a cluster that hasn't been named. You can't name a cluster that doesn't exist in the data. The pipeline must run in order.
309
310### Moderation as Local Knowledge
311
312Blacksky's moderation works because moderators share context that can't be reduced to rules. Anti-Blackness, misogynoir, internalized racism — these require understanding, not keyword matching.
313
314**Moderation is a section of the community sheaf.** Locally consistent within the community, but non-transferable. You can't export Blacksky's moderation to a gaming community. The context doesn't serialize.
315
316In the language analogy: you can't translate grammar rules between languages. French grammar doesn't apply to Japanese. Each community's governance is internally consistent but contextually bound.
317
318What DOES transfer across communities is **signal, not enforcement:**
319
320```
321Community A's moderation → bridge → Community B receives SIGNAL
322 Community B decides what to do with signal
323
324Not: "User X is banned" (enforcement transfer)
325But: "User X was flagged for anti-Black harassment" (signal transfer)
326
327Community B applies its OWN context to the signal.
328```
329
330This is Blacksky's actual model — the labeler produces labels. Users and rooms decide independently whether to apply them (Off / Warn / Hide per label type). Moderation is a subscribable service, not an imposed authority.
331
332### Moderation as a Subscribable Service
333
334```
335Moderation Service M
336 ├── Scans content (automated + human review)
337 ├── Produces labels (anti-Black harassment, misogynoir, etc.)
338 └── Publishes labels to subscribers
339
340Room A subscribes to M → applies labels according to room policy
341Room B subscribes to M → applies different label settings
342Room C doesn't subscribe → doesn't see labels
343Room D subscribes to M and N → sees both sets of labels
344```
345
346In graph terms, moderation subscriptions are an **edge type** — a trust relationship between rooms and moderation services. Different rooms subscribe to different services. Users in overlapping rooms see the interaction of multiple moderation layers.
347
348### The Recursive Self-Reference Problem
349
350The social graph contains its own governance. Governance modifies the graph (banning changes membership). The modified graph changes governance (different members → different norms).
351
352```
353Social graph → detection → communities → governance
354 ↑ │
355 └────── enforcement modifies graph ←───┘
356```
357
358This is a fixed-point problem. The language pipeline has the same structure: grammar extracted from corpus determines how new sentences are parsed, which feeds back into the corpus. Linas handles this with iterative refinement until convergence.
359
360In social governance: communities iteratively refine their norms through repeated interaction and deliberation. **Instability is a feature.** A community in stable equilibrium is dead or authoritarian. A community in unstable equilibrium is alive — norms shift, people push back, deliberation happens, norms adjust. The social grammar is never final.
361
362## The Meta-Community Tension
363
364Hyper-social's vision: bridges connect platforms. A community can span Keet, IRC, Discord, Matrix, Mastodon, email. The community IS the people and their relationships — the platform is substrate.
365
366Blacksky demonstrates this partially — the community existed as "Black Twitter" before, on a different platform. The community migrated. The identity migrated. The social graph persisted because it's a property of the PEOPLE, not the platform.
367
368### Three Competing Sovereignties
369
370```
371PROTOCOL SOVEREIGNTY Each platform has its own rules, moderation, identity
372COMMUNITY SOVEREIGNTY The community wants consistent norms regardless of platform
373INDIVIDUAL SOVEREIGNTY Each person wants control over their identity and expression
374```
375
376These conflict at boundaries:
377
378**At the bridge (protocol vs community):** A user banned by Blacksky's labeler on Bluesky — should they be banned in the bridged Keet room? Protocol sovereignty says Keet moderation is per-room. Community sovereignty says our moderation covers our community.
379
380**At the identity layer (community vs individual):** The community wants "same person across platforms" for accountability. The individual wants pseudonymity and context-dependent identity. The community needs to recognize Rudy across platforms. Rudy might want different presentation in different contexts.
381
382**At the governance layer (all three):** A meta-community's People's Assembly makes a decision: "no X-type content." On Bluesky, enforced via labeler. On Keet, via room admin. On IRC, via channel ops. Three different enforcement mechanisms for one governance decision.
383
384### Resolution: Federation, Not Union
385
386The meta-community is a federation of platform-specific instances with local autonomy:
387
3881. **Shared membership** — the same people, different handles
3892. **Shared norms** — agreed through deliberation, enforced locally per platform
3903. **Shared bridges** — message flow across platforms
3914. **Shared moderation signals** — not enforcement, signal
392
393Governance is **advisory across bridges, binding within rooms.** The meta-community's People's Assembly says "we don't allow X." Within each platform, local governance enforces according to local mechanisms. The bridge translates intent, not mechanism.
394
395Identity mapping is **opt-in and partial.** You can link your Keet keypair to your Bluesky DID — but only if you choose to. You can be pseudonymous on one platform and named on another. The community sees "Rudy" regardless of platform, but the mapping is Rudy's choice.
396
397### The Bridge Community as Creole
398
399People active on both sides of a bridge — in both the Keet rooms and the IRC channels — form their own community. Their norms are neither Keet norms nor IRC norms. They're bridge norms — a social creole.
400
401Over time, bridge communities develop their own culture, their own governance, their own identity. This is not planned — it emerges from sustained cross-platform interaction. The bridge room starts as pidgin (basic message passing). If interaction persists, it becomes creole (rich culture with its own grammar).
402
403### The Fracture Principle Revisited
404
405From [ARCHITECTURE.md](ARCHITECTURE.md): *"The communication graph will fracture. Any 'universal bridge' eventually breaks into many bridges."*
406
407Blacksky validates this: started as a feed (one platform feature), fractured into own PDS + own relay + own moderation + own governance + own economic tools + own web client. Each piece independently useful. The system is the emergent property of all pieces running together.
408
409A meta-community spanning Keet + Bluesky + IRC + Matrix will similarly fracture into:
410- Bridge per platform pair (not one universal bridge)
411- Moderation service per community (not one universal moderator)
412- Governance per community (not one universal governance)
413- Identity mapping per person (not one universal identity)
414
415The fracture is not failure — it's the natural topology of social space. Non-trivial cohomology = real tensions that can't be flattened.
416
417## Economic Sovereignty
418
419Fraser inverted the usual tech stack:
420
421```
422Usual tech model: Mutual aid model:
423Protocol Protocol
424Infrastructure Economic solidarity (mutual aid pooling)
425Social features Infrastructure (funded by community)
426Monetization (extract) Social features (emergent from funded infra)
427```
428
429The economic layer is not on top (monetization to extract value). It's near the bottom (foundation for sovereignty). Communities that fund their own infrastructure answer to no platform.
430
431In the language analogy: a language community that controls its own publishing (newspapers, schools, internet) maintains its language. A community dependent on another's infrastructure assimilates over time. Economic sovereignty IS cultural sovereignty IS community sovereignty.
432
433### Economic Edges in the Graph
434
435Rooms with economic cooperation have a qualitatively different connection than rooms with only membership overlap. A community where members pool funds for infrastructure has a bond beyond shared interests.
436
437```
438Membership edge: "We share members" → statistical connection
439Governance edge: "We share moderation" → trust connection
440Economic edge: "We share treasury" → solidarity connection
441```
442
443Communities with all three edge types are the most robust. This maps to Fraser's mutual aid principle: financial solidarity creates the conditions for social infrastructure, which creates the conditions for community governance.
444
445### Sustainability Without Extraction
446
447Blacksky's economic model:
448- 1,100+ individual contributors on OpenCollective
449- Papertree: mutual aid pooling (no applications, no proof of hardship — trust)
450- GatherFor: susu circles (rotating savings from West African/Caribbean traditions)
451- Blacksky Cash: peer-to-peer payments with group pooling
452
453Success metric: not transaction volume, but **ongoing participation, repeat contributions, collective decision-making around need.** The economic system measures community health, not profit.
454
455This is a pattern any decentralized community can follow: fund infrastructure collectively, own it collectively, govern it collectively. The alternative — depending on platform goodwill or venture capital — leads to the dynamics that made Black Twitter precarious on Twitter's infrastructure.
456
457## Cyberspace Principles
458
459### The Hacker Ethic Applied
460
461**"Access to computers should be unlimited and total."**
462Anyone can run a node. No account required. No approval process. No app store. `bare index.js` and you're in.
463
464**"All information should be free."**
465The PROTOCOL is free. The CODE is free. Room listings are free (public discovery). But people's social graphs are THEIR information. Freedom of information is not surveillance of people.
466
467**"Mistrust authority — promote decentralization."**
468No central graph authority. No aggregation server. Local computation only. The architecture makes centralization physically impossible.
469
470**"Hackers should be judged by their hacking."**
471Pseudonymous identity. Your key is your identity. Reputation from behavior in rooms, not from profiles managed by a corporation.
472
473### The Sysadmin Scope
474
475```
476Traditional sysadmin: P2P room admin:
477Reads all messages ✓ Reads messages in THEIR room ✓
478Sees all users ✓ Sees members of THEIR room ✓
479Monitors all activity ✓ Monitors THEIR room activity ✓
480Controls all access ✓ Controls THEIR room access ✓
481 Knows about other rooms ✗
482 Sees members' other activities ✗
483 Has cross-room surveillance ✗
484```
485
486The room admin's scope is strictly bounded. No governance layer should see beyond its scope:
487- Room admin sees room data
488- Registry admin sees registry data (listings, not content or membership)
489- Nobody sees the full graph
490
491This is the **principle of least privilege** applied to social structure.
492
493### Privacy Through Cryptography, Not Policy
494
495Build tools that empower, not surveil. Make the system transparent (open source, open protocol). Make it accessible (anyone can run a node, no gatekeepers). Resist centralization even when it's more convenient.
496
497The social graph navigation system should embody:
4981. **No central authority** — local computation, no aggregation
4992. **User sovereignty** — your graph is yours, you control what you see
5003. **Pseudonymity** — graph reveals social structure, not identity
5014. **Opt-in only** — discovery is consensual, rooms choose to be listed
5025. **No surveillance** — no entity can see the full graph
5036. **Interoperability** — bridges are sheaf morphisms, not walled gardens
504
505## Can Social Structure Be Parsed?
506
507The deepest question. Language has structure because it's generated by finite rules. Is social structure similarly compressible?
508
509**Yes, with four caveats:**
510
511**1. Multiple valid parses.** "I saw the man with the telescope" has two parse trees. A social graph can have multiple valid community structures. The ambiguity is real, not a failure of the algorithm.
512
513**2. The observation is an intervention.** Showing the parse changes the thing being parsed. The pipeline has political implications. What you surface shapes what people see and how they behave.
514
515**3. Social grammar evolves fast.** Language grammar changes over decades. Social norms change over weeks. The pipeline must run continuously.
516
517**4. Power asymmetry.** In language, speakers contribute roughly equally to grammar. In social, admins and active members contribute disproportionately. The pipeline must either account for this (weighted MI) or deliberately not (equal weighting — a political choice).
518
519## Implementation Phases
520
521### Phase 1: Local-Only Graph (Safe)
522
523```
524Data source: keet-daemon API (your rooms, your members)
525Computation: local, in-process, never exported
526Storage: local only
527Sharing: none
528Risk: minimal — same data you already see, structured
529```
530
531### Phase 2: Registry Graph (Safe — Already Public)
532
533```
534Data source: discovery registries (public by design)
535Computation: can be shared (public data)
536Storage: replicated via Autobase
537Sharing: inherent (registries are designed to be shared)
538Risk: none — this data is already public
539```
540
541### Phase 3: Moderation Services (Careful — Opt-In)
542
543```
544Data source: content analysis (within subscribed rooms)
545Computation: by moderation service operators
546Sharing: labels published to subscribers (not content)
547Risk: moderate — moderation requires seeing content
548Mitigation: opt-in per room, transparent labeling, no content export
549```
550
551### Phase 4: Deliberation Tools (Careful — Requires Identity)
552
553```
554Data source: community member opinions
555Computation: consensus-finding (Polis-style dimensionality reduction)
556Sharing: aggregated opinion clusters (not individual votes)
557Risk: moderate — opinion data is sensitive
558Mitigation: anonymous voting, only cluster-level results published
559```
560
561### Phase 5: Economic Tools (Most Sensitive)
562
563```
564Data source: financial contributions
565Computation: treasury management, mutual aid distribution
566Sharing: treasury balances (not individual contributions)
567Risk: high — financial data requires strong privacy
568Mitigation: cryptographic privacy, no individual transaction visibility
569```
570
571**Skip global aggregate statistics entirely.** Local computation + public registries cover 90% of the value. Aggregate stats add marginal value with real privacy risk.
572
573## Design Principles
574
575```
576 1. EMERGENCE NEEDS MIDWIVES, NOT MANAGERS
577 Detect clusters. Let humans name them. Naming is political.
578
579 2. COMMUNITIES ARE LIVING LANGUAGES
580 Parse them like language. Expect evolution. Expect dialects.
581 Expect pidgins at boundaries. Expect creoles over time.
582
583 3. GOVERNANCE IS LOCAL KNOWLEDGE
584 Can't be centralized without losing context. Each community
585 moderates with shared understanding. Moderation crosses
586 bridges as signal, not enforcement.
587
588 4. ECONOMIC SOVEREIGNTY IS THE FOUNDATION
589 Communities that fund their own infrastructure are sovereign.
590 Mutual aid precedes social infrastructure.
591
592 5. THE OBSERVATION IS AN INTERVENTION
593 The graph view shapes the graph. Design choices are political.
594 Don't pretend neutrality. Surface the choices.
595
596 6. META-COMMUNITIES ARE FEDERATIONS, NOT UNIONS
597 Cross-platform communities have local autonomy per platform.
598 Governance is advisory across bridges, binding within rooms.
599
600 7. THE FRACTURE IS THE TOPOLOGY
601 Non-trivial cohomology = real social tensions. Don't resolve
602 them. Surface them. Let communities negotiate.
603
604 8. INSTABILITY IS HEALTH
605 Communities that stop evolving are dead or authoritarian.
606 The grammar is never final. The parse is always provisional.
607
608 9. PRIVACY THROUGH ARCHITECTURE
609 No aggregation. Local computation only. No God's-eye view.
610 Make surveillance impossible, not just prohibited.
611
61210. THREE SOVEREIGNTIES IN PRODUCTIVE TENSION
613 Protocol (platform rules), Community (shared norms),
614 Individual (personal freedom). The tension is the feature.
615```
616
617## Connection to Other Documents
618
619- [SOCIAL-GRAPH-NAVIGATION.md](SOCIAL-GRAPH-NAVIGATION.md) — Graph navigation mechanics, sheaf theory, the parsing pipeline, dual navigation model
620- [ARCHITECTURE.md](ARCHITECTURE.md) — System overview, the fracture principle, bridge architecture
621- [NETWORKING.md](NETWORKING.md) — Protocol decisions for bridge communication
622- [PROJECTS.md](PROJECTS.md) — All standalone projects in the ecosystem
docs/GODOT.md +351 -0
……@@ -0,0 +1,351 @@
1# Godot 4 Integration
2
3Godot is the engine for all interactive interfaces — from simple 2D chat clients to immersive 3D multiplayer games. Not just 3D. Godot handles **2D apps, 2D games, 3D apps, AND 3D games**.
4
5## Two-Tier Architecture
6
7```
8Tier 1 (Social/Apps -- WebSocket + JSON-RPC 2.0):
9 Godot -> WebSocketPeer -> keet-daemon (ws://localhost:3117/ws)
10 Good for: chat, feeds, wikis, boards, casual games
11 Latency: ~1-5ms (WebSocket on localhost, bidirectional)
12 Implementation: Pure GDScript. No compilation. No plugins to install.
13
14Tier 2 (Gaming -- Native UDP):
15 Godot -> HyperswarmMultiplayerPeer (GDExtension) -> libudx -> UDP
16 Good for: real-time multiplayer, competitive games, FPS, racing
17 Latency: <5ms (direct UDP, no IPC, no serialization)
18 Implementation: C++ GDExtension. Compile once per platform.
19```
20
21A game can use BOTH tiers simultaneously — Tier 1 for chat/social features, Tier 2 for gameplay networking.
22
23## Use Cases by Category
24
25| Category | Examples | Tier |
26|----------|----------|------|
27| 2D apps | Chat client, feed reader, wiki browser, kanban board, notification dashboard | Tier 1 |
28| 2D games | Card games, board games, puzzle games — all P2P multiplayer via Keet rooms | Tier 1 |
29| 3D apps | Virtual rooms with avatars, spatial wikis, data visualization, 3D file browser | Tier 1 |
30| 3D games | FPS, MMO, sandbox, racing — full multiplayer via Hyperswarm/UDX | Tier 2 |
31
32## Tier 1: keet-social Plugin (Pure GDScript)
33
34For social features and casual games. No compilation needed.
35
36### Plugin Structure
37
38```
39addons/keet-social/
40├── plugin.cfg
41├── keet_client.gd -- WebSocket client for hub API (JSON-RPC 2.0)
42├── keet_chat.gd -- Chat panel (send/receive messages)
43├── keet_room.gd -- Room management (join/leave/list)
44├── keet_identity.gd -- Identity and profile management
45├── keet_feed.gd -- Feed timeline display
46├── keet_wiki.gd -- Wiki page viewer/editor
47├── keet_board.gd -- Kanban board
48└── demo/
49 ├── chat_2d.tscn -- 2D chat app
50 ├── feed_2d.tscn -- 2D feed reader
51 ├── room_browser.tscn -- Room management UI
52 ├── wiki_editor.tscn -- Wiki page editor
53 ├── social_space_3d.tscn -- 3D social room with avatars
54 └── dashboard.tscn -- Unified control panel
55```
56
57### How It Works
58
59```gdscript
60# keet_client.gd -- core WebSocket connection
61extends Node
62
63var ws := WebSocketPeer.new()
64var _next_id := 1
65var _pending := {} # id -> callback
66
67func _ready():
68 ws.connect_to_url("ws://localhost:3117/ws")
69
70func _process(_delta):
71 ws.poll()
72 while ws.get_available_packet_count() > 0:
73 var data = ws.get_packet().get_string_from_utf8()
74 var msg = JSON.parse_string(data)
75 if msg.has("id") and _pending.has(msg.id):
76 _pending[msg.id].call(msg.get("result"), msg.get("error"))
77 _pending.erase(msg.id)
78 elif msg.has("method"):
79 # Server push notification
80 event_received.emit(msg.method, msg.params)
81
82func call_method(method: String, params: Dictionary, callback: Callable) -> void:
83 var id = _next_id
84 _next_id += 1
85 _pending[id] = callback
86 var req = {"jsonrpc": "2.0", "id": id, "method": method, "params": params}
87 ws.send_text(JSON.stringify(req))
88
89func subscribe(room_id: String) -> void:
90 call_method("subscribe", {"roomId": room_id}, func(_r, _e): pass)
91
92signal event_received(method: String, params: Dictionary)
93```
94
95### GDScript Usage
96
97```gdscript
98# Send a message
99keet_client.call_method("send", {
100 "roomId": "ar5jd5...",
101 "text": "Hello from Godot!"
102}, func(result, error):
103 if error:
104 push_error("Send failed: " + str(error))
105)
106
107# Subscribe to room and handle events
108keet_client.subscribe("ar5jd5...")
109keet_client.event_received.connect(func(method, params):
110 if method == "message":
111 add_chat_message(params.from, params.text)
112)
113
114# List rooms
115keet_client.call_method("rooms.list", {}, func(result, _error):
116 for room in result:
117 print(room.name, " - ", room.id)
118)
119```
120
121## Tier 2: HyperswarmMultiplayerPeer (C++ GDExtension)
122
123For real-time multiplayer games. Compiles libudx into a native shared library that Godot loads directly.
124
125### Why GDExtension, Not a Sidecar
126
127| Approach | Latency | Runtime Deps | Overhead |
128|----------|---------|-------------|----------|
129| HTTP to keet-daemon | ~50-200ms | keet-daemon running | HTTP headers per request |
130| JSON-RPC sidecar (Mauve v2) | ~10-50ms | hyper-sdk-rpc running | TCP + JSON parse |
131| GDExtension (native UDX) | <5ms | None -- compiled in | Zero IPC overhead |
132
133The GDExtension compiles libudx into a .so/.dylib/.dll that Godot loads at startup. No external processes, no IPC, no JSON serialization. One-time build cost, zero runtime overhead.
134
135### How GDExtension Loading Works
136
137GDExtension is Godot's plugin system for native code. You compile your C/C++/Rust code into a platform-specific shared library, place it in the `addons/` folder with a manifest file, and Godot dynamically loads it. No custom Godot build required — works with any standard Godot binary.
138
139**The manifest (`.gdextension` file) maps platforms to libraries:**
140
141```ini
142[configuration]
143entry_symbol = "hyperswarm_peer_init"
144compatibility_minimum = "4.3"
145
146[libraries]
147linux.x86_64 = "res://addons/hyperswarm_peer/bin/libhyperswarm_peer.linux.x86_64.so"
148macos.universal = "res://addons/hyperswarm_peer/bin/libhyperswarm_peer.macos.universal.dylib"
149windows.x86_64 = "res://addons/hyperswarm_peer/bin/libhyperswarm_peer.windows.x86_64.dll"
150```
151
152When Godot starts, it reads this file, finds the right library for the current platform, and `dlopen()`s it. Your native classes become available in GDScript just like built-in types.
153
154### Plugin Structure
155
156```
157addons/hyperswarm_peer/
158├── hyperswarm_peer.gdextension # Extension manifest
159├── bin/
160│ ├── libhyperswarm_peer.linux.x86_64.so
161│ ├── libhyperswarm_peer.macos.universal.dylib
162│ └── libhyperswarm_peer.windows.x86_64.dll
163├── src/
164│ ├── register_types.cpp # GDExtension entry point
165│ ├── hyperswarm_multiplayer_peer.h
166│ ├── hyperswarm_multiplayer_peer.cpp
167│ ├── udx_transport.h # Wraps libudx
168│ ├── udx_transport.cpp
169│ ├── dht_discovery.h # Wraps libhyperdht
170│ └── dht_discovery.cpp
171└── thirdparty/
172 ├── libudx/ # C library (Holepunch)
173 ├── libhyperdht/ # C library (if available)
174 └── libsodium/ # For Noise encryption
175```
176
177### GDScript Usage
178
179```gdscript
180# Host a game
181var peer = HyperswarmMultiplayerPeer.new()
182peer.create_server("my-game-topic-as-hex-string")
183multiplayer.multiplayer_peer = peer
184
185# Join a game
186var peer = HyperswarmMultiplayerPeer.new()
187peer.create_client("my-game-topic-as-hex-string")
188multiplayer.multiplayer_peer = peer
189
190# P2P mesh (no server)
191var peer = HyperswarmMultiplayerPeer.new()
192peer.create_mesh(my_unique_id, "my-game-topic")
193multiplayer.multiplayer_peer = peer
194
195# All standard Godot multiplayer works:
196@rpc("any_peer", "unreliable", 1)
197func sync_position(pos: Vector3):
198 position = pos
199
200@rpc("any_peer", "reliable")
201func fire_weapon(weapon_id: int):
202 spawn_projectile(weapon_id)
203```
204
205### Godot 4 Multiplayer Architecture
206
207Godot 4 has a modular, layered multiplayer system:
208
209```
210+--------------------------------------------------+
211| Application Layer |
212| @rpc functions, MultiplayerSpawner, |
213| MultiplayerSynchronizer |
214+--------------------------------------------------+
215| SceneMultiplayer (API layer) |
216| Authority, peer IDs, channels |
217+--------------------------------------------------+
218| MultiplayerPeer (transport -- SWAPPABLE) |
219| ENet | WebSocket | WebRTC | Custom Extension |
220+--------------------------------------------------+
221```
222
223**The transport layer is fully pluggable.** Godot provides `MultiplayerPeerExtension` with 23 virtual methods you override to plug in ANY transport. Everything above (RPCs, spawners, synchronizers) works unchanged.
224
225### MultiplayerPeerExtension: 23 Virtual Methods
226
227**Packet I/O:**
228- `_get_packet_script()` -> PackedByteArray — get next incoming packet
229- `_put_packet_script(buffer)` -> Error — send packet
230- `_get_available_packet_count()` -> int
231- `_get_max_packet_size()` -> int
232
233**Peer Management:**
234- `_poll()` — poll for events (called each frame)
235- `_close()` — close connection
236- `_set_target_peer(id)` — 0=broadcast, negative=exclude
237- `_get_packet_peer()` -> sender ID
238- `_get_unique_id()` -> this peer's ID
239- `_is_server()` -> bool
240- `_get_connection_status()` -> disconnected/connecting/connected
241- `_disconnect_peer(id, force)`
242
243**Transfer Control:**
244- `_set_transfer_mode(mode)` — UNRELIABLE / UNRELIABLE_ORDERED / RELIABLE
245- `_get_transfer_mode()` -> mode
246- `_set_transfer_channel(ch)` / `_get_transfer_channel()`
247- `_get_packet_channel()` / `_get_packet_mode()`
248
249**Connection Control:**
250- `_set_refuse_new_connections(bool)` / `_is_refusing_new_connections()`
251- `_is_server_relay_supported()` -> bool
252
253### ENet Mesh (P2P Reference)
254
255Godot 4 added `create_mesh(unique_id)` + `add_mesh_peer(id, connection)` for full P2P mesh networking where every peer connects directly. No central server. This maps well to Hyperswarm's topology.
256
257### Key Multiplayer Nodes
258
259- **MultiplayerSynchronizer** — auto-sync properties across peers
260 - `REPLICATION_MODE_ALWAYS` (periodic) vs `REPLICATION_MODE_ON_CHANGE` (delta)
261 - Visibility filters for interest management
262 - Configurable tick rate via `replication_interval`
263
264- **MultiplayerSpawner** — auto-replicate dynamically created scenes
265 - Handles mid-game joins and reconnections
266
267- **@rpc annotation** — declarative RPC: `@rpc("any_peer", "unreliable", "call_local", 1)`
268 - Modes: any_peer, authority
269 - Sync: call_local, call_remote
270 - Transfer: reliable, unreliable, unreliable_ordered
271 - Channel: integer for multiplexing
272
273### What Godot Does NOT Provide
274
275These must be implemented manually or via addons:
276- Client-side prediction
277- Server reconciliation
278- Network interpolation
279- Lag compensation
280- Snapshot system
281
282**Netfox addon** fills these gaps (prediction, rollback, interpolation). Consider bundling with Tier 2 games.
283
284### Built-in Transports for Reference
285
286| Transport | Protocol | NAT | Best For |
287|-----------|----------|-----|----------|
288| ENetMultiplayerPeer | UDP (ENet) | No | Desktop games, low latency |
289| WebSocketMultiplayerPeer | TCP (WS) | Yes | Browser games |
290| WebRTCMultiplayerPeer | UDP (DTLS) | Yes | Browser P2P |
291| OfflineMultiplayerPeer | None | N/A | Single-player testing |
292
293### Existing P2P Peer Implementations
294
295- **godot-iroh** (Rust GDExtension) — P2P networking via Iroh. Closest reference to our approach. Uses the same MultiplayerPeerExtension pattern.
296- **steam-multiplayer-peer** (C++ GDExtension) — Steam Sockets. Good C++ reference for GDExtension build patterns.
297
298## HyperGodot: History and Context
299
300RangerMauve (Mauve Signweaver) created HyperGodot — the original P2P game engine integration.
301
302### Three Generations
303
304**V1 (Godot 3) — Complete, working:**
305- HTTP sidecar pattern: hyper-gateway runs alongside Godot (port 4973)
306- SSE for events, extension messages for gossip, HyperRequest URL rewriting
307- ~35MB binary sidecar per platform
308- Demos: third-person shooter, witchy world (755MB)
309
310**Port (Godot 4.0) — Abandoned:**
311- Direct port of v1 by Attila772
312- 62 commits of frustration
313- GDScript 2.0 broke signals, JSON, threads, File/Directory API all at once
314- Abandoned
315
316**V2 (Godot 4.3) — Proof of concept, 1 commit:**
317- JSON-RPC over TCP (hyper-sdk-rpc, port 4772)
318- StreamPeerTCP + newline-delimited JSON
319- Method chaining: `"sdk.getDrive(hostname).readdir(path)"`
320- Architecturally cleaner but barely started
321
322### Our Approach: Beyond Sidecars
323
324Mauve's approaches all use the sidecar pattern — run a Node.js process alongside Godot. We improve on this:
325
326**Tier 1:** keet-daemon IS the sidecar. Already running, full API. No new process needed. But we use WebSocket instead of HTTP/SSE.
327
328**Tier 2:** Skip the sidecar entirely. GDExtension links directly against libudx. No external processes, no IPC, no serialization. Direct native code.
329
330## Build Order
331
3321. **Phase 4: keet-social plugin** (~600 LOC GDScript)
333 - WebSocket client to keet-daemon
334 - Chat, feed, wiki, board panels as Godot scenes
335 - 2D demos: chat app, feed reader, dashboard
336 - 3D demo: social room with avatars
337 - Pure GDScript, no compilation
338
3392. **Phase 5: HyperswarmMultiplayerPeer** (~2000 LOC C++)
340 - C++ GDExtension wrapping libudx
341 - All 23 MultiplayerPeerExtension virtual methods
342 - DHT-based peer discovery
343 - NAT holepunching
344 - Three transfer modes mapped to UDX
345 - Interactive mode for low-latency gaming
346 - Demo: multiplayer game using standard @rpc
347
3483. **Phase 7: Game demos**
349 - 2D card game (P2P, no server)
350 - 3D FPS arena (mesh networking, fast sync)
351 - Social world (3D rooms with chat + wiki panels)
docs/NETWORKING.md +171 -0
……@@ -0,0 +1,171 @@
1# Networking: WebSocket + JSON-RPC 2.0
2
3## Why WebSocket, Not HTTP/SSE
4
5The original plan used HTTP requests + Server-Sent Events (SSE). Research showed this is inferior in every dimension.
6
7### What SSE Is (and Why It's Bad Here)
8
9SSE (Server-Sent Events) is a one-way streaming protocol where the server pushes text events to the client over a kept-alive HTTP connection. The client can only listen — to send data back, it must make separate HTTP requests. This means two separate protocols for bidirectional communication.
10
11### Protocol Comparison
12
13| Dimension | HTTP/SSE | WebSocket + JSON-RPC 2.0 |
14|-----------|----------|--------------------------|
15| Per-message overhead | ~400 bytes (HTTP headers) | 2-6 bytes (WS frame) |
16| Direction | Two protocols (HTTP up, SSE down) | Single bidirectional connection |
17| Browser connection limit | 6 per domain (SSE) | No practical limit |
18| Binary support | No (SSE is text-only) | Yes (binary frames) |
19| Godot support | HTTPRequest has 30s delay bugs | WebSocketPeer works cleanly |
20| Complexity | Two protocols to maintain | One protocol for everything |
21| Latency | ~50-200ms (HTTP round-trip) | ~1-5ms (persistent connection) |
22
23**Bottom line:** WebSocket is 100x less overhead, truly bidirectional, works everywhere, and has clean support in every client (browser, Godot, bare-ws, CLI tools).
24
25### What We Use Instead
26
27```
28keet-daemon (:3117/ws) <-- single WebSocket endpoint
29 |
30 |-- Godot: WebSocketPeer (built-in, no plugins)
31 |-- Browser: native WebSocket API
32 |-- TUI: bare-ws
33 |-- CLI: websocat (or HTTP fallback for curl)
34```
35
36## JSON-RPC 2.0 Protocol
37
38All messages follow the JSON-RPC 2.0 specification over a single WebSocket connection.
39
40### Message Types
41
42**Command (client -> daemon):**
43```json
44{"jsonrpc":"2.0","id":1,"method":"send","params":{"roomId":"abc","text":"hello"}}
45```
46
47**Response (daemon -> client):**
48```json
49{"jsonrpc":"2.0","id":1,"result":{"ok":true}}
50```
51
52**Event notification (daemon -> client, no id = server push):**
53```json
54{"jsonrpc":"2.0","method":"message","params":{"roomId":"abc","text":"world","from":"alice"}}
55```
56
57**Subscribe to room events:**
58```json
59{"jsonrpc":"2.0","id":2,"method":"subscribe","params":{"roomId":"abc"}}
60```
61
62**Unsubscribe:**
63```json
64{"jsonrpc":"2.0","id":3,"method":"unsubscribe","params":{"roomId":"abc"}}
65```
66
67**Error:**
68```json
69{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}
70```
71
72### Available Methods
73
74**Chat:**
75- `send` — send a message to a room
76- `messages` — get message history
77- `subscribe` — subscribe to real-time room events (messages, joins, leaves)
78- `unsubscribe` — stop receiving events for a room
79
80**Room Management:**
81- `rooms.list` — list all joined rooms
82- `rooms.get` — get room details
83- `rooms.join` — join a room by invite
84
85**Service Control:**
86- `services.list` — list running bridges/panels/daemons
87- `services.start` — start a service
88- `services.stop` — stop a service
89
90**Panel APIs:**
91- `feed.list` / `feed.get` — timeline
92- `wiki.pages` / `wiki.get` / `wiki.put` — wiki
93- `board.cards` / `board.move` — kanban
94
95**Identity:**
96- `identity.get` — get identity info
97- `identity.graph` — cross-platform identity graph
98
99**Config:**
100- `config.get` / `config.set` — hub configuration
101
102### HTTP Fallback
103
104A minimal HTTP endpoint remains for tools that don't support WebSocket:
105
106```
107GET /health -> {"ok":true,"version":"1.0.0"}
108GET /api/rooms -> room list (JSON)
109POST /api/send -> send message (JSON body)
110```
111
112This is for `curl`, health checks, and monitoring only. All real-time communication uses WebSocket.
113
114## Downstream Effects
115
116Switching from HTTP/SSE to WebSocket affects all bridges and daemons.
117
118### What Changes
119
120**1. keet-daemon (the server)**
121- Add WebSocket transport using `bare-ws` alongside existing HTTP
122- JSON-RPC 2.0 message dispatch (method routing, id tracking)
123- `subscribe`/`unsubscribe` for room-level event push
124
125**2. keet-client.js (shared across all bridges)**
126
127Before (HTTP polling):
128```javascript
129// Poll every 2 seconds for new messages
130setInterval(async () => {
131 const msgs = await fetch('http://localhost:3117/rpc/messages/' + roomId)
132 // diff against last known messages...
133}, 2000)
134```
135
136After (WebSocket subscribe):
137```javascript
138// Persistent connection, instant push
139const ws = new WebSocket('ws://localhost:3117/ws')
140ws.send(JSON.stringify({
141 jsonrpc: '2.0', id: 1,
142 method: 'subscribe',
143 params: { roomId }
144}))
145ws.on('message', (data) => {
146 const msg = JSON.parse(data)
147 if (msg.method === 'message') handleMessage(msg.params)
148})
149```
150
151**3. Each bridge's relay loop**
152- Replace `_startKeetPoll()` / `setInterval` with WebSocket subscribe
153- The adapter/router logic stays identical — only the transport changes
154
155**4. hyper-daemon triggers**
156- Switch from HTTP polling to WebSocket subscribe for "new message" triggers
157
158### What Does NOT Change
159
160- **bridge-envelope** — message schema is transport-agnostic
161- **bridge-vault** — credential storage, no networking
162- **Adapter logic** — IRC<->envelope<->Keet normalization stays identical
163- **Router logic** — dedup, routing rules unchanged
164- **Godot side** — already designed for WebSocket from the start
165
166### Order of Implementation
167
168Changes cascade from server outward:
1691. keet-daemon gets WebSocket transport (everything else depends on this)
1702. keet-client.js rewritten (shared by all bridges)
1713. Each bridge updated (mostly just swapping the import and removing polling)
docs/PROJECTS.md +286 -0
……@@ -0,0 +1,286 @@
1# Standalone Projects
2
3Each project is independently useful. Make one work well, then move on to the next. The system is the emergent property of all pieces running together.
4
5## Project Map
6
7| # | Project | Type | What it does | ~LOC |
8|---|---------|------|-------------|------|
9| 1 | bridge-envelope | npm pkg | Universal message schema + compact-encoding codec | 200 |
10| 2 | bridge-vault | npm pkg | Encrypted P2P credential store | 300 |
11| 3 | bridge-irc | Pear app | IRC <-> Keet bridge | 500 |
12| 4 | bridge-discord | Pear app | Discord <-> Keet bridge | 600 |
13| 5 | bridge-matrix | Pear app | Matrix <-> Keet bridge | 600 |
14| 6 | bridge-mastodon | Pear app | Mastodon <-> Keet bridge | 700 |
15| 7 | bridge-email | Pear app | Email <-> Keet bridge | 800 |
16| 8 | hyper-wiki | Pear app | P2P wiki (Hyperdrive-backed) | 800 |
17| 9 | hyper-board | Pear app | P2P kanban board | 700 |
18| 10 | hyper-feed | Pear app | P2P social feed/timeline | 500 |
19| 11 | hyper-daemon | Pear app | P2P automation runtime | 600 |
20| 12 | hyper-social | Pear app | Meta-app composing everything | 1500 |
21
22**Plus interfaces:**
23
24| # | Interface | Type | What it does | ~LOC |
25|---|-----------|------|-------------|------|
26| 13 | Hub API Server | Pear app | WebSocket + JSON-RPC 2.0 + HTTP fallback | 300 |
27| 14 | WebUI | HTML/JS | Browser-based dashboard | 800 |
28| 15 | TUI | Pear app | Terminal interface | 400 |
29| 16 | keet-social plugin | GDScript | Godot Tier 1 (social/apps via WebSocket) | 600 |
30| 17 | HyperswarmMultiplayerPeer | C++ GDExtension | Godot Tier 2 (gaming via UDX/UDP) | 2000 |
31
32## Foundation Libraries
33
34### bridge-envelope (~200 LOC)
35
36Universal message schema with compact-encoding codec. Every bridge normalizes messages into this format before routing.
37
38```javascript
39import { encode, decode, Envelope } from 'bridge-envelope'
40
41const msg = new Envelope({
42 type: 'chat', // chat | post | email | event
43 platform: 'irc', // source platform
44 author: { name: 'alice', id: 'alice@libera' },
45 content: { text: 'hello world' },
46 timestamp: Date.now(),
47 hash: null // computed on encode
48})
49
50const buf = encode(msg) // compact binary
51const decoded = decode(buf) // back to Envelope
52```
53
54**Done when:** any bridge can import and use it for message normalization.
55
56### bridge-vault (~300 LOC)
57
58Encrypted credential store. Each bridge stores its API keys/tokens/passwords here, encrypted with the user's Keet keypair.
59
60```javascript
61import { Vault } from 'bridge-vault'
62
63const vault = new Vault(corestoreInstance)
64await vault.ready()
65
66await vault.set('irc', { server: 'irc.libera.chat', nick: 'mybot' })
67await vault.set('discord', { token: 'bot-token-here' })
68
69const creds = await vault.get('irc')
70await vault.delete('discord')
71```
72
73Backed by Hyperbee. Replicates P2P (encrypted — only the key owner can decrypt).
74
75**Done when:** any bridge can securely store and retrieve credentials.
76
77## Platform Bridges
78
79Each bridge is a standalone Pear terminal app connecting one platform to Keet.
80
81### bridge-irc (~500 LOC)
82
83The first bridge. Proves the pattern.
84
85```
86bridge-irc/
87├── package.json # Pear terminal app
88├── index.js # Entry: parse args, start bridge
89├── lib/
90│ ├── irc-client.js # IRC connection (irc-framework)
91│ ├── keet-client.js # WebSocket client to keet-daemon
92│ ├── adapter.js # IRC <-> Envelope normalization
93│ ├── router.js # Bidirectional relay + content-hash dedup
94│ └── config.js # Config loading
95└── test/
96```
97
98**Usage:**
99```bash
100pear run --dev . --server irc.libera.chat --channel "#test" --keet-room ar5jd5...
101```
102
103**What it does:**
1041. Connects to an IRC server + channel
1052. Connects to keet-daemon via WebSocket (ws://localhost:3117/ws)
1063. Subscribes to Keet room events
1074. IRC messages -> envelope -> send to Keet
1085. Keet messages -> envelope -> send to IRC
1096. Content-hash dedup prevents echo loops
1107. Reconnects automatically on disconnect
111
112### bridge-discord (~600 LOC)
113
114Second bridge. Validates archetype reuse.
115
116- Discord.js bot <-> keet-daemon WebSocket
117- Handles: embeds, reactions, threads, file uploads
118- Shares ~80% of adapter logic with bridge-irc
119- Platform-specific: Discord requires a bot token from Developer Portal
120
121### bridge-matrix (~600 LOC)
122
123Matrix rooms <-> Keet rooms.
124
125- matrix-js-sdk or simple HTTP API
126- End-to-end encryption: Matrix has its own E2EE, Keet has its own — bridge decrypts/re-encrypts at the boundary
127- Platform-specific: Matrix homeserver URL + access token
128
129### bridge-mastodon (~700 LOC)
130
131Mastodon/Fediverse <-> Keet. Different archetype from chat bridges.
132
133- Short posts, hashtags, boosts (not real-time chat)
134- ActivityPub protocol for federated communication
135- POSSE pattern: originate in Keet, syndicate to Fediverse
136- Platform-specific: Mastodon instance URL + API token
137
138### bridge-email (~800 LOC)
139
140Email <-> Keet. Long-form archetype.
141
142- IMAP polling for incoming + SMTP for outgoing
143- Thread mapping: email threads <-> Keet threads
144- Attachment handling: files stored in Hyperdrive
145- Platform-specific: IMAP/SMTP credentials
146
147## Workspace Panels
148
149Each panel adds a capability to a room. Can run standalone or compose into the meta-app.
150
151### hyper-wiki (~800 LOC)
152
153P2P wiki. Like a local Notion but decentralized.
154
155```
156hyper-wiki/
157├── package.json
158├── index.js
159├── lib/
160│ ├── wiki-store.js # Hyperdrive-backed page storage
161│ ├── versioning.js # Page history (Hyperdrive snapshots)
162│ ├── editor.js # Markdown editor
163│ ├── search.js # Full-text search (Hyperbee index)
164│ └── keet-hook.js # Optional: "pin to wiki" from Keet chat
165└── test/
166```
167
168**Standalone features:**
169- Create/edit/delete wiki pages (Markdown)
170- Version history via Hyperdrive snapshots
171- Full-text search
172- P2P replication — share wiki with anyone via Hyperswarm
173
174### hyper-board (~700 LOC)
175
176P2P kanban board.
177
178- Task cards with status lanes (todo / in-progress / done)
179- Assignments, labels, due dates
180- Keet integration: create tasks from chat messages
181- P2P sync: multiple users see the same board
182
183### hyper-feed (~500 LOC)
184
185Aggregated social timeline.
186
187- Collects messages from all bridges into a unified feed
188- Filtering: by platform, room, author, content type
189- Chronological and threaded views
190- P2P: share your feed with followers
191
192## Automation
193
194### hyper-daemon (~600 LOC)
195
196P2P automation runtime — like IFTTT but decentralized.
197
198```
199hyper-daemon/
200├── package.json
201├── index.js
202├── lib/
203│ ├── runtime.js # Script execution engine
204│ ├── triggers.js # Event matching
205│ ├── actions.js # Built-in actions
206│ └── store.js # Daemon state (Hyperbee)
207├── daemons/
208│ ├── crosspost.js # Syndicate to specific platforms
209│ ├── summarize.js # Auto-summarize chat to wiki
210│ └── notify.js # Push notifications
211└── test/
212```
213
214**Triggers:** new Keet message, schedule (cron-like), file change, WebSocket event
215**Actions:** send Keet message, write to Hyperdrive, call HTTP endpoint
216**Scripts:** stored in Hyperbee, replicate P2P, users can fork/share
217
218## Meta-App
219
220### hyper-social (~1500 LOC)
221
222Composes all bridges + panels + daemons into a unified experience.
223
224**What's ONLY in the meta-app (not standalone):**
225- Routing configuration UI — composing multiple bridges
226- Modular room compositor — tiling WM layout
227- Panel docking system — how panels attach to rooms
228- Unified identity view — cross-platform identity graph
229- Service orchestration — start/stop/monitor all bridges
230
231## Build Order
232
233One project at a time. Each complete before the next.
234
235### Phase 1: Foundation
2361. **bridge-envelope** — message schema + codec
2372. **bridge-vault** — credential store
238
239### Phase 2: First Bridges
2403. **bridge-irc** — proves the pattern
2414. **bridge-discord** — validates archetype reuse
242
243### Phase 3: Panels + Automation
2445. **hyper-wiki** — first panel
2456. **hyper-daemon** — automation
246
247### Phase 4: More Bridges
2487. **bridge-mastodon** — social archetype
2498. **bridge-email** — long-form archetype
2509. **bridge-matrix** — chat archetype variant
251
252### Phase 5: Board + Feed
25310. **hyper-board** — kanban
25411. **hyper-feed** — timeline
255
256### Phase 6: Meta-App
25712. **hyper-social** — composites everything
258
259### Interface Build Order (parallel to above)
260
261See [GODOT.md](GODOT.md) for Godot-specific phases.
262
2631. **Hub API Server** — WebSocket + JSON-RPC 2.0 (enables all interfaces)
2642. **WebUI** — browser dashboard
2653. **TUI** — terminal interface
2664. **keet-social plugin** — Godot Tier 1 (GDScript)
2675. **HyperswarmMultiplayerPeer** — Godot Tier 2 (C++ GDExtension)
2686. **GUI** — Pear desktop app
2697. **Game demos** — showcasing different patterns
270
271## Verification Strategy
272
273| Project | How to verify |
274|---------|---------------|
275| bridge-envelope | `brittle test/` — encode/decode roundtrip |
276| bridge-vault | `brittle test/` — store/retrieve/encrypt/decrypt |
277| bridge-irc | Start keet-daemon + bridge-irc + join IRC -> verify bidirectional messages |
278| bridge-discord | Start keet-daemon + bridge-discord + Discord test server -> verify |
279| bridge-matrix | Start keet-daemon + bridge-matrix + Matrix test room -> verify |
280| hyper-wiki | Create page, edit, version history, replicate to second peer |
281| hyper-board | Create card, move lanes, assign, sync with second peer |
282| hyper-feed | Subscribe to room, see messages in feed, filter |
283| hyper-daemon | Create trigger, fire event, verify action executed |
284| hyper-social | Start with 2+ bridges, verify unified routing + tiling |
285| keet-social plugin | Run 2D chat demo in Godot, send/receive messages |
286| HyperswarmMultiplayerPeer | Two Godot instances discover via DHT, play multiplayer |
docs/README.md +62 -0
……@@ -0,0 +1,62 @@
1# Hyper-Social Documentation
2
3P2P social layer built on Hypercore Protocol and Keet. A family of interoperable standalone projects — bridges, panels, daemons, interfaces — that together form a decentralized communication graph.
4
5## Documentation Index
6
7| Document | What it covers |
8|----------|---------------|
9| [ARCHITECTURE.md](ARCHITECTURE.md) | System overview, communication graph, dependency graph, the "fracture" principle |
10| [PROJECTS.md](PROJECTS.md) | All 15 standalone projects — what they do, structure, build order |
11| [NETWORKING.md](NETWORKING.md) | Protocol decisions: WebSocket + JSON-RPC 2.0, why not HTTP/SSE, message format |
12| [GODOT.md](GODOT.md) | Godot 4 integration: two-tier architecture, keet-social plugin, GDExtension, libudx |
13| [UDX-GAMING.md](UDX-GAMING.md) | Deep dive: libudx for real-time gaming, packet overhead, NAT holepunching |
14| [SOCIAL-GRAPH-NAVIGATION.md](SOCIAL-GRAPH-NAVIGATION.md) | Graph-based navigation, sheaf theory, parsing social structure like language |
15| [COMMUNITY-EMERGENCE.md](COMMUNITY-EMERGENCE.md) | How communities form, govern themselves, privacy architecture, Blacksky case study |
16
17## Quick Summary
18
19**What is this?** A P2P social ecosystem where each piece works standalone. Bridges connect platforms (IRC, Discord, Matrix, Mastodon, email) to Keet P2P rooms. Panels add capabilities (wiki, kanban, feed). Daemons automate actions. Interfaces let you use it all from browser, terminal, desktop, or Godot.
20
21**Core principle:** The communication graph will fracture. Any "universal bridge" eventually breaks into many bridges. Design for this from day one. Each bridge is independently useful. The system is the emergent property of all pieces running together.
22
23**This mirrors Holepunch's ecosystem:**
24- `hypercore` works alone (append-only logs)
25- `hyperbee` works alone (database on hypercore)
26- `hyperswarm` works alone (P2P networking)
27- **Keet composes them all** into a chat app
28
29We do the same: each project is independently useful. `hyper-social` is the whole family, not one app.
30
31## Key Technologies
32
33| Technology | Role |
34|-----------|------|
35| Keet / keet-daemon | P2P chat hub (port 3117) |
36| WebSocket + JSON-RPC 2.0 | Hub API protocol (replaces HTTP/SSE) |
37| Hypercore | Append-only logs (data storage) |
38| Hyperbee | Key-value database on Hypercore |
39| Hyperdrive | P2P filesystem |
40| Hyperswarm | Peer discovery via DHT |
41| UDX (libudx) | Reliable UDP streams (gaming) |
42| Godot 4 | 2D/3D engine for apps and games |
43| Pear Runtime | P2P app runtime (deploys via `pear://` URLs) |
44
45## Directory Layout
46
47```
48hyper-social/
49├── docs/ # This documentation
50├── bridge-envelope/ # Universal message schema (npm pkg)
51├── bridge-vault/ # Encrypted credential store (npm pkg)
52├── bridge-irc/ # IRC <-> Keet bridge (Pear app)
53├── bridge-discord/ # Discord <-> Keet bridge (Pear app)
54├── bridge-matrix/ # Matrix <-> Keet bridge (Pear app)
55├── bridge-mastodon/ # Mastodon <-> Keet bridge (Pear app)
56├── bridge-email/ # Email <-> Keet bridge (Pear app)
57├── hyper-wiki/ # P2P wiki (Pear app)
58├── hyper-board/ # P2P kanban board (Pear app)
59├── hyper-feed/ # P2P social feed (Pear app)
60├── hyper-daemon/ # P2P automation runtime (Pear app)
61└── hyper-social/ # Meta-app composing everything (Pear app)
62```
docs/SOCIAL-GRAPH-NAVIGATION.md +380 -0
……@@ -0,0 +1,380 @@
1# Social Graph Navigation
2
3## The Problem with Trees
4
5Discord's structure is a tree:
6
7```
8User
9├── Folder (client-side)
10│ ├── Server
11│ │ ├── Category
12│ │ │ ├── #channel
13│ │ │ │ └── thread
14│ │ │ └── #channel
15│ │ └── Category
16│ └── Server
17└── Folder
18```
19
20Every node has exactly one parent. Simple to render, simple to navigate. But the tree is an artifact of client-server architecture — Discord has "servers" because there ARE servers. The tree mirrors the deployment topology.
21
22In P2P, there are no servers. There's no natural root for the tree. The underlying social reality was never a tree in the first place.
23
24## What Actually Exists
25
26The real social structure is a graph — specifically a hypergraph:
27
28- **People** know people (social graph)
29- **People** are in rooms (bipartite graph: people ↔ rooms)
30- **Rooms** relate to topics (rooms ↔ topics)
31- **Rooms** can belong to multiple groups (rooms ↔ registries)
32- **Topics** overlap (topic graph)
33
34Discord forces this into a tree by bundling four orthogonal concerns into one hierarchy node (the "server"):
35
361. **Permission boundary** — who can see what
372. **Discovery scope** — what rooms are browsable together
383. **Social context** — this community of people
394. **Administrative domain** — who has mod power
40
41Keet already unbundles these:
42
43| Concern | Discord | Keet P2P |
44|---------|---------|----------|
45| Permissions | Per-server roles | Per-room (room admin, member roles) |
46| Discovery | Browse channels in a server | Per-registry (Autobase indexing rooms) |
47| Social context | Server membership | Per-room membership overlap |
48| Administration | Server owner/mods | Per-room creator/admins |
49
50Four orthogonal axes. No hierarchy. Already a graph.
51
52## Graph vs Tree: What Changes
53
54**In a tree:** groups own rooms. Top-down. A channel belongs to exactly one server.
55
56**In a graph:** rooms exist independently. Groups are views/filters/sets. A room can be in multiple groups simultaneously. Groups can overlap. A room that bridges two groups is a first-class structural feature, not an edge case.
57
58```
59 [Group A]───#general───[Group B]
60 │ │ │
61 #dev-talk #announcements #art
62 │ │
63 [Group C]──────────────────┘
64```
65
66**What you gain:**
67- Rooms aren't siloed — a dev room can exist in both a "Hypercore" group and a "Bare Runtime" group
68- No duplicate rooms across communities
69- Bridge rooms (rooms in multiple groups) are natural, not special-cased
70- Natural for P2P — no server owner, just overlapping sets of peers
71
72**What you lose:**
73- Tree renders trivially as a sidebar. Graphs don't.
74- Navigation becomes "where am I" instead of "drill down"
75- Permissions get complex — who controls a room in 3 groups?
76
77## Sheaf Theory (Linas Vepstas' Framework)
78
79A sheaf assigns data to open sets of a topological space with two axioms:
80
811. **Locality** — if two sections agree on every overlap, they're the same
822. **Gluing** — compatible local sections can be glued into a global section
83
84And the critical concept: **cohomology** — the obstruction to gluing. When local data can't be made globally consistent, you get non-trivial cohomology. That's not an error. It's structural information.
85
86### Applied to the Social Graph
87
88**Each room is an open set.** It has local data — messages, members, roles, shared context, culture. Rooms overlap where they share members. The overlap is the set of people who see both rooms.
89
90**Identity is a section of the sheaf.** One keypair (global section), but context-dependent presentation (display name, avatar, what you share) that can vary locally. The sheaf condition: consistency is required on overlaps (shared members see the same you), freedom exists where contexts don't overlap.
91
92**Groups are an open cover.** A discovery registry covers a set of rooms. Multiple registries can cover the same room. The intersection of two registries is the rooms they share. Group-level data (name, governance, membership rules) must be compatible where registries overlap.
93
94**Discord's tree is a trivial covering.** Every channel belongs to exactly one server. No overlaps. Trivial topology. Trivial cohomology. Everything glues trivially. That's why it "just works" — and why it's limited.
95
96**A graph covering has non-trivial topology.** A room in two registries creates an overlap. Conflicting governance between registries is non-trivial cohomology — a real structural feature of the social space. The math surfaces real tensions that a tree architecture hides.
97
98### Recursive Groups
99
100A discovery registry is an Autobase. An Autobase is writable by its members. Those members coordinate by chatting — in a room. So a group's governance room IS its registry, viewed differently:
101
102```
103"Holepunch Dev" = {
104 as Registry: indexes [#hypercore, #bare, #pear-apps]
105 as Room: where admins discuss which rooms to include
106 same Autobase, different sections of the same sheaf
107}
108```
109
110A group can BE a room. The meta-room's members decide which rooms belong. This is what Discord's server admin channel is — but hardcoded. In a graph with sheaf structure, it's just another node with edges.
111
112## Parsing the Social Graph Like Language
113
114### The Language Learning Pipeline
115
116The OpenCog language learning pipeline (`opencog-kb/language-learning/mvp/`):
117
118```
119corpus text
120 → pair counting (word A next to word B)
121 → MI (which pairs are statistically significant)
122 → MST parse (connect highest-MI pairs into trees)
123 → clustering (words with similar connectors = same category)
124 → grammar (rules extracted from clusters)
125```
126
127### The Same Pipeline, Applied to Social
128
129| Language | Social |
130|----------|--------|
131| word | room |
132| sentence | a user's room list |
133| co-occurrence | two rooms share a member |
134| MI(word_A, word_B) | MI(room_A, room_B) |
135| parse tree | community structure |
136| grammatical category | group type |
137| connector | social affordance |
138
139**Step 1: Pair counting**
140
141A "sentence" is a user's room list. Each user generates one sentence. The "words" are the rooms they're in.
142
143```
144for each user U:
145 rooms = U.rooms()
146 for each pair (R1, R2) in rooms:
147 count[R1][R2] += 1
148```
149
150Two rooms co-occurring in the same user's room list = two words co-occurring in the same sentence.
151
152**Step 2: MI calculation**
153
154```
155MI(room_A, room_B) = log2(P(A,B) / (P(A) × P(B)))
156```
157
158- **High MI:** rooms that share members more than chance predicts. Meaningfully connected — the same people chose both.
159- **Low MI:** overlap is coincidental (both rooms are huge).
160- **Negative MI:** rooms that anti-correlate. People in room A are specifically NOT in room B. Community boundary.
161
162Same formula as `compute-mi.scm`. Same AtomSpace representation:
163
164```scheme
165; Language:
166(EvaluationLink (Predicate "word-pair")
167 (List (Word "the") (Word "cat")))
168
169; Social:
170(EvaluationLink (Predicate "room-pair")
171 (List (Room "The Garage") (Room "Hypercore Dev")))
172```
173
174**Step 3: MST parsing**
175
176For each user's room neighborhood, connect rooms with the highest-MI edges. The tree reveals: which rooms are tightly coupled (same community), which are loosely connected (bridges), which are leaves (niche rooms off a hub).
177
178Critical parallel: you don't MST the entire vocabulary. You parse sentences — local contexts. Similarly, you parse local neighborhoods, not the entire global graph. Each user sees a local parse. The global structure emerges from consistent local parses.
179
180**That's the sheaf condition.** Local parses must agree on overlaps.
181
182**Step 4: Clustering**
183
184Words with similar connector profiles belong to the same grammatical category ("cat", "dog", "table" all take determiners → they're nouns).
185
186Rooms with similar membership profiles belong to the same group type:
187
188- Rooms where everyone is also in the Hypercore room → "ecosystem dev rooms"
189- Rooms where everyone is also in The Garage → "project rooms"
190- Rooms with no overlap → "isolated communities"
191
192The clusters ARE the groups. Not curated — emerged from the data.
193
194**Step 5: Grammar = Social Structure**
195
196Structural patterns extracted from clusters:
197
198- "Hub room → satellite rooms" (a community center with topic offshoots)
199- "Bridge room connects two communities" (shared between clusters)
200- "Governance room governs a set of topic rooms" (the recursive meta-room)
201
202These are the **social grammar** — the rules by which communities self-organize.
203
204### Compartments = Communities
205
206In the language pipeline, compartments are regions where grammar is locally consistent. English in one compartment, French in another. The boundary is where rules change.
207
208In social, compartments are communities with locally consistent norms. The dev community has one set of norms, the gaming community has another. Detecting compartment boundaries = detecting community boundaries. Same algorithm.
209
210### Connector Theory
211
212In Linas' framework, a word's meaning IS its connector set — what it can link to and how.
213
214A room's "meaning" IS its connector set — its pattern of MI with other rooms. The same room in a different graph neighborhood would be a different room, just like the same word in a different sentence has a different parse.
215
216Rooms have typed connectors (affordances):
217- A dev room: `[code+, discussion+, help-seeking+]`
218- A project room: `[team+, output+, planning+]`
219- A social room: `[casual+, off-topic+, bonding+]`
220
221When connectors match, links form. Groups emerge from the parse — from successful connector matching. Multiple valid parses = multiple valid groupings. Ambiguity is natural.
222
223## The Dual Navigation Model
224
225Two views of the same underlying sheaf:
226
227### 1D Sidebar (Local Section)
228
229The traditional sidebar — but computed, not fixed. It's the section of the sheaf at your current position:
230
231```
2321. Current room (where you are)
2332. Rooms with highest MI to current room (most connected)
2343. Rooms where your contacts are active right now
2354. Rooms at the boundary of your current cluster (doors out)
2365. Rooms in other clusters you belong to (distant but yours)
237```
238
239Move to a different room, the slice recomputes. Join a new room, MI shifts, slices everywhere update. It's alive.
240
241Different orderings of the same slice:
242- **By group** — pick a registry, see its rooms (what godot-keet has now)
243- **By recency** — most active rooms float up, regardless of group
244- **By social** — rooms where your contacts are active right now
245- **By topology** — rooms that bridge groups are highlighted as connectors
246
247### Graph View (Base Space)
248
249The topology around you. Rooms as nodes, connections as edges, clusters as groups:
250
251- Force-directed layout (nodes repel, edges attract, clusters emerge)
252- Nodes = rooms (sized by activity, colored by cluster)
253- Edges = connections (weighted by MI)
254- Your position = highlighted node
255- Contacts = dots on their current nodes
256
257Navigate by:
258- Clicking nodes (enter a room → 1D sidebar updates)
259- Zooming into clusters (see individual rooms)
260- Zooming out (clusters collapse into single nodes → macro structure)
261- Following edges (traverse connections)
262
263### How They Work Together
264
265```
2661D Sidebar ←→ Graph View
267Street view ←→ Map view
268Reading ←→ Navigating
269Section ←→ Base space
270Local ←→ Global
271```
272
273You're in the graph view. Three clusters visible — dev rooms, social rooms, project rooms. A room bridges two clusters. Click it. The 1D sidebar updates to show that bridge room's neighborhood. Enter the chat. The graph highlights your position. Your contacts' positions are visible.
274
275Toggle between views. Or split-screen: graph on left, chat on right. Click a node in the graph, chat loads, sidebar updates.
276
277## The Dynamic Graph
278
279The graph isn't static. As people join rooms, send messages, create rooms:
280
281- MI values shift
282- MST re-parses
283- Clusters reorganize
284- The graph layout animates — communities drift apart or merge
285- New bridges form when someone joins rooms in two separate clusters
286
287Communities form and dissolve in real time. The same way conversation flows in chat, structure flows in the graph.
288
289## Implementation Path
290
291### Data Source
292
293keet-daemon API already provides:
294- Room lists (`getRecentRooms`)
295- Member lists (`getMemberList`)
296- Activity data (message events)
297- Discovery registries (existing group structure)
298
299### Processing Pipeline
300
301```
302keet-daemon API
303 → build co-occurrence matrix (rooms × rooms, weighted by shared members)
304 → compute MI (same math as opencog-kb/language-learning/mvp/compute-mi.scm)
305 → MST parse per-user neighborhoods
306 → cluster rooms by connector profile
307 → output: discovered groups, bridge rooms, community boundaries
308```
309
310Could run:
311- In AtomSpace/CogServer (existing infrastructure)
312- Periodically as membership changes
313- Per-user (each user's view parses differently — different sections of the sheaf)
314
315### Godot UI
316
317Current scene tree:
318```
319Main → IdentityBar + ContentHBox[GroupBar | RoomList | Chat | Members]
320```
321
322Extended with graph view:
323```
324Main → IdentityBar + ContentHBox[
325 GraphView (2D force-directed, rooms as nodes)
326 OR
327 GroupBar + RoomList + Chat + Members (1D slice)
328]
329```
330
331Force-directed layout in Godot: nodes as Node2D with physics, edges as Line2D, clusters colored by computed group. Click node → enter room → sidebar updates.
332
333### Three Layers of Groups
334
3351. **Curated groups** (discovery registries) — explicit, admin-managed
3362. **Emergent groups** (MI/MST/clustering) — computed from membership data
3373. **Personal groups** (client-side folders) — local-only, per-user
338
339All three coexist. Curated groups seed the structure. Emergent groups surface hidden patterns. Personal groups let users organize their own view.
340
341## Connection to Hyper-Social Architecture
342
343Bridges are **sheaf morphisms** — they map local data from one space (IRC) to another (Keet) while preserving compatibility. Messages map to messages, users map to users, ordering is preserved.
344
345The bridge graph from [ARCHITECTURE.md](ARCHITECTURE.md) is the base space of a sheaf:
346
347```
348 ┌─────────┐
349 ┌────│ Keet │────┐
350 │ └─────────┘ │
351 │ │ │
352┌───┴───┐ ┌──┴──┐ ┌────┴────┐
353│ IRC │ │Disc.│ │Mastodon │
354│ plane │ │plane│ │ plane │
355└───┬───┘ └──┬──┘ └────┬────┘
356```
357
358Each "plane" is an open set. The overlaps (bridged rooms) must have compatible data. The sheaf condition IS the bridge protocol — messages must be faithfully translated across the bridge. Cohomological obstructions (things that can't be bridged faithfully — Discord reactions, Matrix threads, IRC actions) are real structural features, not bugs.
359
360The fracture principle from the architecture doc is a topological statement: the communication graph has non-trivial topology, and any attempt to flatten it into a single universal bridge will fail because the cohomology is non-trivial. Many small bridges, each covering a local region, with compatibility on overlaps — that's a sheaf.
361
362## What Makes This Novel
363
364No existing chat client:
365
3661. **Discovers** group structure (instead of requiring manual creation)
3672. **Navigates** topologically (instead of hierarchically)
3683. **Computes** the sidebar as a local slice (instead of a fixed tree)
3694. **Uses the same math** that parses language to parse social structure
3705. **Surfaces cohomological tensions** (conflicting governance, incompatible bridges) as features rather than hiding them
371
372The technology stack already supports this:
373- Godot for spatial rendering
374- keet-daemon for data
375- AtomSpace for hypergraph reasoning
376- The language learning pipeline for MI/MST/clustering
377- Autobases for graph-native data
378- Sheaf theory for the mathematical framework
379
380The social graph and the linguistic graph are the same kind of object. The tools to navigate one are the tools to navigate the other.
docs/UDX-GAMING.md +318 -0
……@@ -0,0 +1,318 @@
1# UDX for Real-Time Gaming
2
3Deep dive into libudx — Holepunch's custom reliable UDP transport — and how it maps to Godot's multiplayer system for real-time P2P gaming.
4
5## The Holepunch Networking Stack
6
7```
8Application (Keet, games, bridges)
9 |
10 v
11Protomux (protocol multiplexing over a single stream)
12 |
13 v
14Secret-Stream (Noise XX handshake, XChaCha20-Poly1305 encryption)
15 |
16 v
17UDX Stream (reliable, congestion-controlled streams over UDP)
18 |
19 v
20UDX Socket (raw UDP datagrams)
21 |
22 v
23UDP (kernel)
24```
25
26For gaming, we use the lower layers directly (UDX Stream + UDX Socket), bypassing Protomux and potentially Secret-Stream for maximum performance.
27
28## UDX: Three Levels of Sending
29
30UDX provides three distinct ways to send data, each with different guarantees:
31
32### 1. `stream.write(buffer)` — Reliable, Ordered
33- Like TCP but over UDP
34- Guaranteed delivery in order
35- Congestion-controlled
36- Use for: game events, chat, commands, file transfers
37
38### 2. `stream.send(buffer)` — Unreliable, In-Stream
39- Fire within an established stream
40- No delivery guarantee, no ordering guarantee
41- Encrypted (shares the stream's Noise session)
42- Use for: position updates, velocity, rotation, state snapshots
43
44### 3. `socket.send(buffer, port, host)` — Raw UDP
45- Bare UDP datagram, no stream, no encryption
46- Lowest possible overhead
47- Use for: voice chat, video, bulk transfers, custom protocols
48
49```javascript
50const UDX = require('udx-native')
51const udx = new UDX()
52
53// Create socket and stream
54const socket = udx.createSocket()
55socket.bind(0)
56const stream = udx.createStream(streamId)
57stream.connect(socket, remoteStreamId, remotePort, remoteHost)
58
59// Reliable (game events)
60stream.write(Buffer.from('player_died:42'))
61
62// Unreliable (position)
63stream.send(Buffer.from(positionBuffer))
64
65// Fire-and-forget (voice)
66stream.trySend(Buffer.from(voiceChunk))
67
68// Raw UDP (custom protocol)
69socket.send(Buffer.from(rawData), remotePort, remoteHost)
70```
71
72## Mapping to Godot TransferModes
73
74| Godot TransferMode | UDX Method | Use Case |
75|---------------------|------------|----------|
76| TRANSFER_MODE_RELIABLE | `stream.write()` | Commands, chat, game events, spawns |
77| TRANSFER_MODE_UNRELIABLE | `stream.send()` / `stream.trySend()` | Position, rotation, velocity |
78| TRANSFER_MODE_UNRELIABLE_ORDERED | `stream.send()` + client-side seq filter | Animation state, health bars |
79
80For UNRELIABLE_ORDERED, UDX doesn't provide native ordering on unreliable sends. The GDExtension must add a sequence number and drop stale packets client-side. This is a standard game networking pattern.
81
82## Interactive Mode
83
84```javascript
85stream.setInteractive(true) // mode 0 = interactive (low latency)
86 // mode 1 = batch (high throughput, default)
87```
88
89Interactive mode changes UDX's behavior:
90- Reduces batching delay
91- Prioritizes latency over throughput
92- Essential for real-time games where every millisecond matters
93
94## Congestion Control: BBR
95
96UDX uses **BBR** (Bottleneck Bandwidth and Round-trip propagation time) congestion control.
97
98**Note:** The udx-native skill file incorrectly states LEDBAT. The actual libudx C source code implements BBR.
99
100BBR characteristics relevant to gaming:
101- Probes for available bandwidth (doesn't just react to loss)
102- Better utilization of available bandwidth than loss-based algorithms
103- More stable throughput
104- Less latency inflation than TCP
105
106## Packet Overhead
107
108Understanding exact byte budgets is critical for game networking.
109
110### Header Sizes
111
112| Layer | Overhead |
113|-------|----------|
114| UDX header | 20 bytes |
115| IP header | 20 bytes |
116| UDP header | 8 bytes |
117| **Reliable total** | **67 bytes** (20 UDX + 20 IP + 8 UDP + 19 encryption) |
118| **Unreliable msg total** | **72 bytes** (20 UDX + 20 IP + 8 UDP + 24 encryption) |
119
120### Encryption Overhead (Secret-Stream)
121
122| Mode | Overhead |
123|------|----------|
124| Ordered (stream.write) | 19 bytes |
125| Unordered (stream.send) | 24 bytes |
126
127### Usable Payload
128
129At base MTU of 1200 bytes:
130
131| Mode | Payload |
132|------|---------|
133| Reliable | ~1133 bytes |
134| Unreliable message | ~1128 bytes |
135| Raw UDP (no encryption) | ~1152 bytes |
136
137MAX_PACKET = 2048 bytes per UDP datagram (but MTU usually limits to ~1200).
138
139### What Fits in a Packet
140
141Typical game state per player: ~100-200 bytes (position, rotation, velocity, animation state, health).
142
143At 1128 bytes payload: **5-11 players' state per packet.** For most games, a single unreliable send per tick is enough.
144
145## NAT Holepunching
146
147UDX inherits NAT traversal from Hyperswarm/HyperDHT. No port forwarding needed.
148
149### How It Works
150
1511. Peers register on the DHT (Distributed Hash Table)
1522. When joining a game topic, the DHT provides peer addresses
1533. HyperDHT coordinates NAT holepunching between peers
1544. Once holepunched, UDX streams flow directly peer-to-peer
155
156### Timing by NAT Type
157
158| NAT Combination | Holepunch Time |
159|----------------|----------------|
160| CONSISTENT + CONSISTENT | ~1-10 seconds |
161| CONSISTENT + RANDOM | ~5-20 seconds |
162| RANDOM + RANDOM | ~10-35 seconds |
163| Symmetric + Symmetric | Relay required |
164
165### Relay Fallback
166
167For the hardest NAT scenarios (symmetric NATs), UDX provides:
168
169```javascript
170stream.relayTo(relayStream) // Route through a relay peer
171```
172
173The relay peer forwards packets between peers that can't holepunch directly. Adds latency but ensures connectivity.
174
175### IP Migration
176
177```javascript
178stream.changeRemote(newPort, newHost) // Seamless handoff
179```
180
181If a player switches from WiFi to cellular (or vice versa), the stream migrates without disconnection. Critical for mobile gaming.
182
183## Performance Metrics
184
185UDX exposes real-time network metrics useful for game UI and adaptive networking:
186
187```javascript
188stream.rtt // Smoothed round-trip time (ms)
189stream.cwnd // Congestion window (bytes)
190stream.mtu // Maximum transmission unit
191stream.inflight // Packets currently in flight
192stream.retransmits // Total retransmissions
193stream.fastRecoveries // Fast recovery events
194stream.bytesTransmitted // Total bytes sent
195stream.packetsTransmitted // Total packets sent
196```
197
198### Using Metrics in Games
199
200```gdscript
201# Adaptive quality based on network conditions
202func _adjust_quality():
203 var rtt = peer.get_rtt()
204 if rtt > 100:
205 # High latency: reduce sync rate, increase prediction
206 sync_rate = 10 # 10 Hz
207 prediction_enabled = true
208 elif rtt > 50:
209 sync_rate = 20 # 20 Hz
210 prediction_enabled = true
211 else:
212 sync_rate = 60 # 60 Hz
213 prediction_enabled = false
214```
215
216## Hyperswarm for Game Discovery
217
218Instead of IP:port, games use a **topic** (32-byte hash) for matchmaking:
219
220```javascript
221const Hyperswarm = require('hyperswarm')
222const crypto = require('hypercore-crypto')
223
224// Create a game lobby topic
225const topic = crypto.hash(Buffer.from('my-awesome-game-v1-lobby'))
226
227const swarm = new Hyperswarm()
228swarm.join(topic) // Announce + lookup
229
230swarm.on('connection', (stream, peerInfo) => {
231 // UDX stream arrives, encrypted, holepunched
232 // Hand off to game networking...
233})
234```
235
236### Advantages Over Traditional Game Networking
237
238| Feature | Traditional (ENet/Steam) | Hyperswarm + UDX |
239|---------|--------------------------|------------------|
240| NAT traversal | Port forwarding or relay | DHT holepunching (automatic) |
241| Encryption | Optional, add-on | Always-on (Noise protocol) |
242| Server discovery | Lobby server required | Topic-based (no server) |
243| Relay | Dedicated relay servers | Any peer can relay |
244| Mobile handoff | Connection drops | `changeRemote()` (seamless) |
245| Identity | Platform-specific (Steam ID) | Cryptographic keypair |
246
247## Building the GDExtension
248
249### Dependencies
250
251- **godot-cpp** — C++ bindings for GDExtension API
252- **libudx** — Holepunch's UDP transport (C library)
253- **libsodium** — Noise encryption (C library)
254- **libhyperdht** — DHT discovery (if available as C library; may need to extract from JS)
255
256### Build System
257
258```bash
259# Clone godot-cpp
260git clone https://github.com/godotengine/godot-cpp
261cd godot-cpp && scons platform=linux
262
263# Build the extension
264cd addons/hyperswarm_peer/
265scons platform=linux target=template_release
266```
267
268### Challenge: libhyperdht
269
270libudx is a clean C library and wraps easily. The challenge is libhyperdht — the DHT discovery layer is currently JavaScript only. Options:
271
2721. **Extract C core** — libhyperdht uses libudx internally; extract the holepunching logic
2732. **Reimplement in C** — The DHT protocol is documented; reimplement the essential subset
2743. **Sidecar for discovery only** — Use a small Node.js process just for DHT lookup, hand off the socket to the GDExtension
2754. **Use existing C DHT** — If a C implementation of the Kademlia DHT exists
276
277Option 3 is the pragmatic first step: a tiny discovery sidecar that finds peers, then hands off the raw UDP socket to the GDExtension for all game traffic.
278
279## Game Networking Patterns
280
281### Authoritative Server (FPS, competitive)
282
283```
284Server Peer (authority)
285 |-- validates all inputs
286 |-- sends authoritative state
287 |
288Client Peers
289 |-- send inputs (reliable)
290 |-- receive state (unreliable)
291 |-- predict locally
292 |-- reconcile on authoritative update
293```
294
295### P2P Mesh (co-op, casual)
296
297```
298All Peers equal
299 |-- each owns their player state
300 |-- broadcast position/state (unreliable)
301 |-- broadcast events (reliable)
302 |-- no central authority
303```
304
305Using Godot's `create_mesh()` + Hyperswarm topics, this is the simplest pattern. Good for 2-8 player games.
306
307### Relay for Large Lobbies
308
309For games with many players (>8), use a relay topology:
310
311```
312Relay Peer (optional dedicated)
313 |-- forwards packets between peers
314 |-- reduces connection count from O(n^2) to O(n)
315 |-- adds latency but scales better
316
317UDX: stream.relayTo(relayStream)
318```
hyper-board/index.js +4 -0
……@@ -0,0 +1,4 @@
1import { Board } from './lib/board.js'
2import { BoardStore, DEFAULT_LANES } from './lib/board-store.js'
3
4export { Board, BoardStore, DEFAULT_LANES }
hyper-board/lib/board-store.js +199 -0
……@@ -0,0 +1,199 @@
1import { randomBytes } from 'crypto'
2
3const DEFAULT_LANES = ['backlog', 'todo', 'in-progress', 'done']
4
5class BoardStore {
6 constructor (bee) {
7 this.bee = bee
8 }
9
10 _cardKey (id) {
11 return `card/${id}`
12 }
13
14 _laneKey (name) {
15 return `lane/${name}`
16 }
17
18 _genId () {
19 return randomBytes(8).toString('hex')
20 }
21
22 async ready () {
23 await this.bee.ready()
24
25 // Initialize default lanes if none exist
26 const lanes = await this.listLanes()
27 if (lanes.length === 0) {
28 for (let i = 0; i < DEFAULT_LANES.length; i++) {
29 await this.bee.put(this._laneKey(DEFAULT_LANES[i]), {
30 name: DEFAULT_LANES[i],
31 order: i,
32 created: Date.now()
33 })
34 }
35 }
36 }
37
38 async createCard (opts = {}) {
39 const id = opts.id || this._genId()
40 const now = Date.now()
41
42 const card = {
43 id,
44 title: opts.title || 'Untitled',
45 description: opts.description || '',
46 lane: opts.lane || 'backlog',
47 assignee: opts.assignee || '',
48 tags: opts.tags || [],
49 priority: opts.priority || 'normal', // low, normal, high, urgent
50 created: now,
51 updated: now
52 }
53
54 await this.bee.put(this._cardKey(id), card)
55 return card
56 }
57
58 async getCard (id) {
59 const node = await this.bee.get(this._cardKey(id))
60 return node ? node.value : null
61 }
62
63 async updateCard (id, updates = {}) {
64 const existing = await this.getCard(id)
65 if (!existing) throw new Error(`Card not found: ${id}`)
66
67 const card = {
68 ...existing,
69 ...updates,
70 id, // never overwrite id
71 created: existing.created, // never overwrite created
72 updated: Date.now()
73 }
74
75 await this.bee.put(this._cardKey(id), card)
76 return card
77 }
78
79 async deleteCard (id) {
80 const existing = await this.getCard(id)
81 if (!existing) return false
82
83 await this.bee.del(this._cardKey(id))
84 return true
85 }
86
87 async moveCard (id, lane) {
88 return this.updateCard(id, { lane })
89 }
90
91 async listCards (opts = {}) {
92 const cards = []
93
94 for await (const node of this.bee.createReadStream({
95 gte: 'card/',
96 lte: 'card/~'
97 })) {
98 const card = node.value
99 if (!card) continue
100
101 // Filter by lane
102 if (opts.lane && card.lane !== opts.lane) continue
103
104 // Filter by assignee
105 if (opts.assignee && card.assignee !== opts.assignee) continue
106
107 // Filter by tag
108 if (opts.tag && !card.tags?.includes(opts.tag)) continue
109
110 // Filter by priority
111 if (opts.priority && card.priority !== opts.priority) continue
112
113 cards.push(card)
114 }
115
116 // Sort by priority then created date
117 const priorityOrder = { urgent: 0, high: 1, normal: 2, low: 3 }
118 cards.sort((a, b) => {
119 const pa = priorityOrder[a.priority] ?? 2
120 const pb = priorityOrder[b.priority] ?? 2
121 if (pa !== pb) return pa - pb
122 return a.created - b.created
123 })
124
125 return cards
126 }
127
128 async addLane (name, order) {
129 const existing = await this.bee.get(this._laneKey(name))
130 if (existing) throw new Error(`Lane already exists: ${name}`)
131
132 if (order === undefined) {
133 const lanes = await this.listLanes()
134 order = lanes.length
135 }
136
137 const lane = { name, order, created: Date.now() }
138 await this.bee.put(this._laneKey(name), lane)
139 return lane
140 }
141
142 async removeLane (name) {
143 // Don't allow removing lanes that have cards
144 const cards = await this.listCards({ lane: name })
145 if (cards.length > 0) {
146 throw new Error(`Cannot remove lane "${name}": ${cards.length} cards still in it`)
147 }
148
149 await this.bee.del(this._laneKey(name))
150 return true
151 }
152
153 async listLanes () {
154 const lanes = []
155
156 for await (const node of this.bee.createReadStream({
157 gte: 'lane/',
158 lte: 'lane/~'
159 })) {
160 if (node.value) lanes.push(node.value)
161 }
162
163 lanes.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
164 return lanes
165 }
166
167 async search (query) {
168 const q = query.toLowerCase()
169 const cards = await this.listCards()
170
171 return cards.filter(card => {
172 if (card.title.toLowerCase().includes(q)) return true
173 if (card.description.toLowerCase().includes(q)) return true
174 if (card.tags?.some(t => t.toLowerCase().includes(q))) return true
175 return false
176 })
177 }
178
179 async stats () {
180 const cards = await this.listCards()
181 const lanes = await this.listLanes()
182
183 const byLane = {}
184 for (const lane of lanes) {
185 byLane[lane.name] = 0
186 }
187 for (const card of cards) {
188 byLane[card.lane] = (byLane[card.lane] || 0) + 1
189 }
190
191 return {
192 totalCards: cards.length,
193 totalLanes: lanes.length,
194 byLane
195 }
196 }
197}
198
199export { BoardStore, DEFAULT_LANES }
hyper-board/lib/board.js +85 -0
……@@ -0,0 +1,85 @@
1import Corestore from 'corestore'
2import Hyperbee from 'hyperbee'
3import { BoardStore } from './board-store.js'
4
5class Board {
6 constructor (storage, opts = {}) {
7 this.store = new Corestore(storage)
8 this.bee = new Hyperbee(this.store.get({ name: 'board' }), {
9 keyEncoding: 'utf-8',
10 valueEncoding: 'json'
11 })
12 this.boardStore = new BoardStore(this.bee)
13 this.name = opts.name || 'Board'
14 }
15
16 async ready () {
17 await this.store.ready()
18 await this.boardStore.ready()
19 }
20
21 // Card CRUD
22 async createCard (opts) {
23 return this.boardStore.createCard(opts)
24 }
25
26 async getCard (id) {
27 return this.boardStore.getCard(id)
28 }
29
30 async updateCard (id, updates) {
31 return this.boardStore.updateCard(id, updates)
32 }
33
34 async deleteCard (id) {
35 return this.boardStore.deleteCard(id)
36 }
37
38 async moveCard (id, lane) {
39 return this.boardStore.moveCard(id, lane)
40 }
41
42 // Lane management
43 async addLane (name, order) {
44 return this.boardStore.addLane(name, order)
45 }
46
47 async removeLane (name) {
48 return this.boardStore.removeLane(name)
49 }
50
51 async listLanes () {
52 return this.boardStore.listLanes()
53 }
54
55 // Queries
56 async listCards (opts) {
57 return this.boardStore.listCards(opts)
58 }
59
60 async search (query) {
61 return this.boardStore.search(query)
62 }
63
64 async stats () {
65 return this.boardStore.stats()
66 }
67
68 // Get board view — all lanes with their cards
69 async view () {
70 const lanes = await this.listLanes()
71 const allCards = await this.listCards()
72
73 return lanes.map(lane => ({
74 name: lane.name,
75 order: lane.order,
76 cards: allCards.filter(c => c.lane === lane.name)
77 }))
78 }
79
80 async close () {
81 await this.store.close()
82 }
83}
84
85export { Board }
hyper-board/package-lock.json +1443 -0
……@@ -0,0 +1,1443 @@
1{
2 "name": "hyper-board",
3 "version": "1.0.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "hyper-board",
9 "version": "1.0.0",
10 "license": "MIT",
11 "dependencies": {
12 "b4a": "^1.0.0",
13 "compact-encoding": "^2.0.0",
14 "corestore": "^6.0.0",
15 "hyperbee": "^2.0.0"
16 },
17 "devDependencies": {
18 "brittle": "^3.0.0"
19 }
20 },
21 "node_modules/@hyperswarm/secret-stream": {
22 "version": "6.9.1",
23 "resolved": "https://registry.npmjs.org/@hyperswarm/secret-stream/-/secret-stream-6.9.1.tgz",
24 "integrity": "sha512-xb0S5y3YJwBakD77JOGBHlBxdp63mHClZoXBYoLv+9wH8e054ESKlmQptWqjJK5dv5VMUIVYOJB4MaOpB0JdGw==",
25 "license": "Apache-2.0",
26 "dependencies": {
27 "b4a": "^1.1.0",
28 "hypercore-crypto": "^3.3.1",
29 "noise-curve-ed": "^2.0.1",
30 "noise-handshake": "^4.0.0",
31 "sodium-secretstream": "^1.1.0",
32 "sodium-universal": "^5.0.0",
33 "streamx": "^2.14.0",
34 "timeout-refresh": "^2.0.0",
35 "unslab": "^1.3.0"
36 }
37 },
38 "node_modules/@hyperswarm/secret-stream/node_modules/sodium-native": {
39 "version": "5.0.10",
40 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
41 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
42 "license": "MIT",
43 "dependencies": {
44 "require-addon": "^1.1.0",
45 "which-runtime": "^1.2.1"
46 },
47 "engines": {
48 "bare": ">=1.16.0"
49 }
50 },
51 "node_modules/@hyperswarm/secret-stream/node_modules/sodium-universal": {
52 "version": "5.0.1",
53 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
54 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
55 "license": "MIT",
56 "dependencies": {
57 "sodium-native": "^5.0.1"
58 },
59 "peerDependencies": {
60 "sodium-javascript": "~0.8.0"
61 },
62 "peerDependenciesMeta": {
63 "sodium-javascript": {
64 "optional": true
65 }
66 }
67 },
68 "node_modules/@jridgewell/resolve-uri": {
69 "version": "3.1.2",
70 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
71 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
72 "dev": true,
73 "license": "MIT",
74 "engines": {
75 "node": ">=6.0.0"
76 }
77 },
78 "node_modules/@jridgewell/sourcemap-codec": {
79 "version": "1.5.5",
80 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
81 "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
82 "dev": true,
83 "license": "MIT"
84 },
85 "node_modules/@jridgewell/trace-mapping": {
86 "version": "0.3.31",
87 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
88 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
89 "dev": true,
90 "license": "MIT",
91 "dependencies": {
92 "@jridgewell/resolve-uri": "^3.1.0",
93 "@jridgewell/sourcemap-codec": "^1.4.14"
94 }
95 },
96 "node_modules/@types/istanbul-lib-coverage": {
97 "version": "2.0.6",
98 "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
99 "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
100 "dev": true,
101 "license": "MIT"
102 },
103 "node_modules/b4a": {
104 "version": "1.7.4",
105 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.4.tgz",
106 "integrity": "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug==",
107 "license": "Apache-2.0",
108 "peerDependencies": {
109 "react-native-b4a": "*"
110 },
111 "peerDependenciesMeta": {
112 "react-native-b4a": {
113 "optional": true
114 }
115 }
116 },
117 "node_modules/bare-addon-resolve": {
118 "version": "1.10.0",
119 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
120 "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
121 "license": "Apache-2.0",
122 "dependencies": {
123 "bare-module-resolve": "^1.10.0",
124 "bare-semver": "^1.0.0"
125 },
126 "peerDependencies": {
127 "bare-url": "*"
128 },
129 "peerDependenciesMeta": {
130 "bare-url": {
131 "optional": true
132 }
133 }
134 },
135 "node_modules/bare-ansi-escapes": {
136 "version": "2.2.3",
137 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
138 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
139 "dev": true,
140 "license": "Apache-2.0",
141 "dependencies": {
142 "bare-stream": "^2.6.5"
143 },
144 "peerDependencies": {
145 "bare-buffer": "*"
146 },
147 "peerDependenciesMeta": {
148 "bare-buffer": {
149 "optional": true
150 }
151 }
152 },
153 "node_modules/bare-assert": {
154 "version": "1.2.0",
155 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
156 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
157 "dev": true,
158 "license": "Apache-2.0",
159 "dependencies": {
160 "bare-inspect": "^3.1.2"
161 }
162 },
163 "node_modules/bare-bundle": {
164 "version": "1.10.0",
165 "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
166 "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
167 "dev": true,
168 "license": "Apache-2.0",
169 "peerDependencies": {
170 "bare-buffer": "*",
171 "bare-url": "*"
172 },
173 "peerDependenciesMeta": {
174 "bare-buffer": {
175 "optional": true
176 },
177 "bare-url": {
178 "optional": true
179 }
180 }
181 },
182 "node_modules/bare-cov": {
183 "version": "1.2.1",
184 "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz",
185 "integrity": "sha512-SHNqb/WG9udUEj8m5qGSehoUQ9o0Xzv8nrf4HIdmG+KNU/XdhPLhTP5VPrHnSAMAXhAzEB4Q+2IYZ0x9qPzN1g==",
186 "dev": true,
187 "license": "Apache-2.0",
188 "dependencies": {
189 "bare-fs": "^4.1.2",
190 "bare-inspector": "^4.0.1",
191 "bare-path": "^3.0.0",
192 "bare-process": "^4.2.1",
193 "bare-url": "^2.1.5",
194 "bare-v8-to-istanbul": "^1.0.2",
195 "picomatch": "^4.0.2",
196 "which-runtime": "^1.2.1"
197 }
198 },
199 "node_modules/bare-crypto": {
200 "version": "1.13.0",
201 "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.0.tgz",
202 "integrity": "sha512-RQl13yD+YTACWUZHMMck0C6LNjXgGgRyhVC557ag0xIrkxXf4IQwpnc8WOB58f4k5kSzKhIjy05oW3HUBrFpSQ==",
203 "dev": true,
204 "license": "Apache-2.0",
205 "dependencies": {
206 "bare-stream": "^2.6.3"
207 },
208 "peerDependencies": {
209 "bare-buffer": "*"
210 },
211 "peerDependenciesMeta": {
212 "bare-buffer": {
213 "optional": true
214 }
215 }
216 },
217 "node_modules/bare-debug-log": {
218 "version": "2.0.0",
219 "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
220 "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
221 "dev": true,
222 "license": "Apache-2.0",
223 "dependencies": {
224 "bare-os": "^3.0.1"
225 }
226 },
227 "node_modules/bare-dns": {
228 "version": "2.1.4",
229 "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
230 "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==",
231 "dev": true,
232 "license": "Apache-2.0",
233 "engines": {
234 "bare": ">=1.7.0"
235 }
236 },
237 "node_modules/bare-encoding": {
238 "version": "1.0.3",
239 "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
240 "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
241 "dev": true,
242 "license": "Apache-2.0",
243 "peerDependencies": {
244 "bare-buffer": "*"
245 },
246 "peerDependenciesMeta": {
247 "bare-buffer": {
248 "optional": true
249 }
250 }
251 },
252 "node_modules/bare-env": {
253 "version": "3.0.0",
254 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
255 "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
256 "dev": true,
257 "license": "Apache-2.0",
258 "dependencies": {
259 "bare-os": "^3.0.1"
260 }
261 },
262 "node_modules/bare-events": {
263 "version": "2.8.2",
264 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
265 "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
266 "license": "Apache-2.0",
267 "peerDependencies": {
268 "bare-abort-controller": "*"
269 },
270 "peerDependenciesMeta": {
271 "bare-abort-controller": {
272 "optional": true
273 }
274 }
275 },
276 "node_modules/bare-format": {
277 "version": "1.0.1",
278 "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.1.tgz",
279 "integrity": "sha512-1oS+LZrWK6tnYnvNSHDGljc2MPunRxwhpFriuCgzNF+oklrnwmBKD91tS0yt+jpl2j3UgcSDzBIMiVTvLs9A8w==",
280 "dev": true,
281 "license": "Apache-2.0",
282 "dependencies": {
283 "bare-inspect": "^3.0.0"
284 }
285 },
286 "node_modules/bare-fs": {
287 "version": "4.5.4",
288 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz",
289 "integrity": "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==",
290 "license": "Apache-2.0",
291 "dependencies": {
292 "bare-events": "^2.5.4",
293 "bare-path": "^3.0.0",
294 "bare-stream": "^2.6.4",
295 "bare-url": "^2.2.2",
296 "fast-fifo": "^1.3.2"
297 },
298 "engines": {
299 "bare": ">=1.16.0"
300 },
301 "peerDependencies": {
302 "bare-buffer": "*"
303 },
304 "peerDependenciesMeta": {
305 "bare-buffer": {
306 "optional": true
307 }
308 }
309 },
310 "node_modules/bare-hrtime": {
311 "version": "2.1.1",
312 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
313 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
314 "dev": true,
315 "license": "Apache-2.0"
316 },
317 "node_modules/bare-http-parser": {
318 "version": "1.0.1",
319 "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.0.1.tgz",
320 "integrity": "sha512-A3LTDTcELcmNJ3g5liIaS038v/BQxOhA9cjhBESn7eoV7QCuMoIRBKLDadDe08flxyLbxI2f+1l2MZ/5+HnKPA==",
321 "dev": true,
322 "license": "Apache-2.0"
323 },
324 "node_modules/bare-http1": {
325 "version": "4.2.3",
326 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.2.3.tgz",
327 "integrity": "sha512-zyvHF3xzvBJNpuw0QydE9etTn2AyC/TRVDWTo8Ed3LQEEGaFOkv6KHhaJPlf4eWEeTekk41Fowie+fTsXoNxkA==",
328 "dev": true,
329 "license": "Apache-2.0",
330 "dependencies": {
331 "bare-events": "^2.6.0",
332 "bare-http-parser": "^1.0.0",
333 "bare-stream": "^2.3.0",
334 "bare-tcp": "^2.2.0"
335 },
336 "peerDependencies": {
337 "bare-buffer": "*",
338 "bare-url": "*"
339 },
340 "peerDependenciesMeta": {
341 "bare-buffer": {
342 "optional": true
343 },
344 "bare-url": {
345 "optional": true
346 }
347 }
348 },
349 "node_modules/bare-https": {
350 "version": "2.1.2",
351 "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.2.tgz",
352 "integrity": "sha512-Q+TTydUDsuKQJvh8dX2dvOXCR9fM3xR5TBmKaFrs5p7Lj7XbKX7v4vIUJ36H0SXg2xCOQxXKIbjwrLg5tfJNYg==",
353 "dev": true,
354 "license": "Apache-2.0",
355 "dependencies": {
356 "bare-http1": "^4.0.0",
357 "bare-tcp": "^2.2.0",
358 "bare-tls": "^2.0.0"
359 }
360 },
361 "node_modules/bare-inspect": {
362 "version": "3.1.4",
363 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
364 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
365 "dev": true,
366 "license": "Apache-2.0",
367 "dependencies": {
368 "bare-ansi-escapes": "^2.1.0",
369 "bare-type": "^1.0.0"
370 },
371 "engines": {
372 "bare": ">=1.18.0"
373 }
374 },
375 "node_modules/bare-inspector": {
376 "version": "4.0.3",
377 "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-4.0.3.tgz",
378 "integrity": "sha512-yYk6+7tTVKJKHOja5+QBn989ETv8oVsmwoH0nCM+QYC6w5MC03GC4aQNK+lipAZSz1EaAj42WZ8Wewjpy0L9TA==",
379 "dev": true,
380 "license": "Apache-2.0",
381 "dependencies": {
382 "bare-events": "^2.1.0",
383 "bare-http1": "^4.0.0",
384 "bare-stream": "^2.0.0",
385 "bare-url": "^2.0.0",
386 "bare-ws": "^2.0.0"
387 },
388 "engines": {
389 "bare": ">=1.2.0"
390 },
391 "peerDependencies": {
392 "bare-tcp": "*"
393 },
394 "peerDependenciesMeta": {
395 "bare-tcp": {
396 "optional": true
397 }
398 }
399 },
400 "node_modules/bare-module": {
401 "version": "6.1.3",
402 "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz",
403 "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==",
404 "dev": true,
405 "license": "Apache-2.0",
406 "dependencies": {
407 "bare-bundle": "^1.3.0",
408 "bare-module-lexer": "^1.0.0",
409 "bare-module-resolve": "^1.8.0",
410 "bare-path": "^3.0.0",
411 "bare-url": "^2.0.1"
412 },
413 "engines": {
414 "bare": ">=1.23.0"
415 },
416 "peerDependencies": {
417 "bare-buffer": "*"
418 },
419 "peerDependenciesMeta": {
420 "bare-buffer": {
421 "optional": true
422 }
423 }
424 },
425 "node_modules/bare-module-lexer": {
426 "version": "1.4.7",
427 "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
428 "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
429 "dev": true,
430 "license": "Apache-2.0",
431 "dependencies": {
432 "require-addon": "^1.0.2"
433 },
434 "peerDependencies": {
435 "bare-buffer": "*"
436 },
437 "peerDependenciesMeta": {
438 "bare-buffer": {
439 "optional": true
440 }
441 }
442 },
443 "node_modules/bare-module-resolve": {
444 "version": "1.12.1",
445 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz",
446 "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==",
447 "license": "Apache-2.0",
448 "dependencies": {
449 "bare-semver": "^1.0.0"
450 },
451 "peerDependencies": {
452 "bare-url": "*"
453 },
454 "peerDependenciesMeta": {
455 "bare-url": {
456 "optional": true
457 }
458 }
459 },
460 "node_modules/bare-net": {
461 "version": "2.2.0",
462 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.2.0.tgz",
463 "integrity": "sha512-UF7cAbHsGE+H6uEqWF5IULBow1x58chZz4g3ALgHtv7wZsFcCbRDt0JKWEumf5Oma3QWS1Q6aLi0Rpll8RElMg==",
464 "dev": true,
465 "license": "Apache-2.0",
466 "dependencies": {
467 "bare-events": "^2.2.2",
468 "bare-pipe": "^4.0.0",
469 "bare-stream": "^2.0.0",
470 "bare-tcp": "^2.0.0"
471 }
472 },
473 "node_modules/bare-os": {
474 "version": "3.6.2",
475 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
476 "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
477 "license": "Apache-2.0",
478 "engines": {
479 "bare": ">=1.14.0"
480 }
481 },
482 "node_modules/bare-path": {
483 "version": "3.0.0",
484 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
485 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
486 "license": "Apache-2.0",
487 "dependencies": {
488 "bare-os": "^3.0.1"
489 }
490 },
491 "node_modules/bare-pipe": {
492 "version": "4.1.3",
493 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.3.tgz",
494 "integrity": "sha512-DqQsx93rAzre6yJ9T6l/Vgh+X+bntkVMB1X5ggtXjXtqtMmF2Y2RVlCzxxy/09R6yeR9FSWBEUIaMYJL1/5VDA==",
495 "dev": true,
496 "license": "Apache-2.0",
497 "dependencies": {
498 "bare-events": "^2.0.0",
499 "bare-stream": "^2.0.0"
500 },
501 "engines": {
502 "bare": ">=1.16.0"
503 }
504 },
505 "node_modules/bare-process": {
506 "version": "4.2.2",
507 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.2.2.tgz",
508 "integrity": "sha512-ikzEw+HGLB+2lS/WLVEsqi8BXBwzleG3n7DYI0v6/YNklvNabOIGlLd1dof+7Jw5ob4jsBRPtyflweVahY7rFA==",
509 "dev": true,
510 "license": "Apache-2.0",
511 "dependencies": {
512 "bare-env": "^3.0.0",
513 "bare-events": "^2.3.1",
514 "bare-hrtime": "^2.0.0",
515 "bare-os": "^3.5.0",
516 "bare-pipe": "^4.0.0",
517 "bare-signals": "^4.0.0",
518 "bare-tty": "^5.0.0"
519 }
520 },
521 "node_modules/bare-semver": {
522 "version": "1.0.2",
523 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz",
524 "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==",
525 "license": "Apache-2.0"
526 },
527 "node_modules/bare-signals": {
528 "version": "4.2.0",
529 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz",
530 "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==",
531 "dev": true,
532 "license": "Apache-2.0",
533 "dependencies": {
534 "bare-events": "^2.5.3",
535 "bare-os": "^3.3.1"
536 },
537 "engines": {
538 "bare": ">=1.7.0"
539 }
540 },
541 "node_modules/bare-stream": {
542 "version": "2.7.0",
543 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
544 "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
545 "license": "Apache-2.0",
546 "dependencies": {
547 "streamx": "^2.21.0"
548 },
549 "peerDependencies": {
550 "bare-buffer": "*",
551 "bare-events": "*"
552 },
553 "peerDependenciesMeta": {
554 "bare-buffer": {
555 "optional": true
556 },
557 "bare-events": {
558 "optional": true
559 }
560 }
561 },
562 "node_modules/bare-subprocess": {
563 "version": "5.2.2",
564 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.2.tgz",
565 "integrity": "sha512-L6oXgQ1aWs25RtG5Ky0bDD06p3RAcVVrDDMWb1DfXpHtyEWxamcyIWUbSykxMTWrpLlmSj6ytbb6yoKehGFfmw==",
566 "dev": true,
567 "license": "Apache-2.0",
568 "dependencies": {
569 "bare-env": "^3.0.0",
570 "bare-events": "^2.5.4",
571 "bare-os": "^3.0.1",
572 "bare-pipe": "^4.0.0",
573 "bare-url": "^2.2.2"
574 },
575 "engines": {
576 "bare": ">=1.7.0"
577 },
578 "peerDependencies": {
579 "bare-buffer": "*"
580 },
581 "peerDependenciesMeta": {
582 "bare-buffer": {
583 "optional": true
584 }
585 }
586 },
587 "node_modules/bare-tcp": {
588 "version": "2.2.4",
589 "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.4.tgz",
590 "integrity": "sha512-vGjfMkyhS1lxp+Y4+gXRDR1ijZMK2cKx0RBwtYKUuZbD90ARCoM6bAiTa1HBztumCIC+NXb8eXdMFXyuYwlPyw==",
591 "dev": true,
592 "license": "Apache-2.0",
593 "dependencies": {
594 "bare-dns": "^2.0.4",
595 "bare-events": "^2.5.4",
596 "bare-stream": "^2.6.4"
597 },
598 "engines": {
599 "bare": ">=1.16.0"
600 }
601 },
602 "node_modules/bare-tls": {
603 "version": "2.1.7",
604 "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.1.7.tgz",
605 "integrity": "sha512-h6wcNXQdBeTX7fed9tjPp0/9cA/QfcBTv3ItgjnbUk4rWAU8bEFalZCZnUDdCK/t9zrNfJ+yvcPx4D/1Y6biyA==",
606 "dev": true,
607 "license": "Apache-2.0",
608 "dependencies": {
609 "bare-net": "^2.0.1",
610 "bare-stream": "^2.6.4"
611 },
612 "engines": {
613 "bare": ">=1.7.0"
614 }
615 },
616 "node_modules/bare-tty": {
617 "version": "5.0.3",
618 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.0.3.tgz",
619 "integrity": "sha512-jW24RBWRZOMHuSEWC9mh8wUKO5WUNl0UWUTNPn3yZ8qkOqwa8vaV2dR3a+BvblONPk7V35wuvK9P8508+judAg==",
620 "dev": true,
621 "license": "Apache-2.0",
622 "dependencies": {
623 "bare-events": "^2.2.0",
624 "bare-signals": "^4.0.0",
625 "bare-stream": "^2.0.0"
626 },
627 "engines": {
628 "bare": ">=1.16.0"
629 }
630 },
631 "node_modules/bare-type": {
632 "version": "1.1.0",
633 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
634 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
635 "dev": true,
636 "license": "Apache-2.0",
637 "engines": {
638 "bare": ">=1.2.0"
639 }
640 },
641 "node_modules/bare-url": {
642 "version": "2.3.2",
643 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
644 "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
645 "license": "Apache-2.0",
646 "peer": true,
647 "dependencies": {
648 "bare-path": "^3.0.0"
649 }
650 },
651 "node_modules/bare-utils": {
652 "version": "1.5.1",
653 "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.5.1.tgz",
654 "integrity": "sha512-mxCkFvmDU3mlD/mb+pT64kKXOsx2KMsWLQbngN1LB+NOXfhfnRnyvpy3VZc6m7gzQxe57Bsi+aTCBqA4/S3elQ==",
655 "dev": true,
656 "license": "Apache-2.0",
657 "dependencies": {
658 "bare-debug-log": "^2.0.0",
659 "bare-encoding": "^1.0.0",
660 "bare-format": "^1.0.0",
661 "bare-inspect": "^3.0.0",
662 "bare-type": "^1.0.6"
663 }
664 },
665 "node_modules/bare-v8-to-istanbul": {
666 "version": "1.0.2",
667 "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
668 "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==",
669 "dev": true,
670 "license": "Apache-2.0",
671 "dependencies": {
672 "bare-assert": "^1.0.2",
673 "bare-fs": "^4.1.2",
674 "bare-module": "^6.1.2",
675 "bare-path": "^3.0.0",
676 "bare-process": "^4.2.0",
677 "bare-url": "^2.1.5",
678 "bare-utils": "^1.2.0",
679 "v8-to-istanbul": "^9.3.0",
680 "which-runtime": "^1.2.1"
681 }
682 },
683 "node_modules/bare-ws": {
684 "version": "2.0.4",
685 "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.0.4.tgz",
686 "integrity": "sha512-SQMXzBYna9dRj57Dz1/ag+VWHCRXbfCjMHgyfM2F2lhkVLzMjnVSZP72aVeFWPFqe494Rd70Kzhe2JElGwFlJQ==",
687 "dev": true,
688 "license": "Apache-2.0",
689 "dependencies": {
690 "bare-crypto": "^1.2.0",
691 "bare-events": "^2.3.1",
692 "bare-http1": "^4.0.0",
693 "bare-https": "^2.0.0",
694 "bare-stream": "^2.1.2"
695 },
696 "peerDependencies": {
697 "bare-buffer": "*",
698 "bare-url": "*"
699 },
700 "peerDependenciesMeta": {
701 "bare-buffer": {
702 "optional": true
703 },
704 "bare-url": {
705 "optional": true
706 }
707 }
708 },
709 "node_modules/big-sparse-array": {
710 "version": "1.0.3",
711 "resolved": "https://registry.npmjs.org/big-sparse-array/-/big-sparse-array-1.0.3.tgz",
712 "integrity": "sha512-6RjV/3mSZORlMdpUaQ6rUSpG637cZm0//E54YYGtQg1c1O+AbZP8UTdJ/TchsDZcTVLmyWZcseBfp2HBeXUXOQ==",
713 "license": "MIT"
714 },
715 "node_modules/brittle": {
716 "version": "3.19.1",
717 "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz",
718 "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==",
719 "dev": true,
720 "license": "Apache-2.0",
721 "dependencies": {
722 "b4a": "^1.6.0",
723 "bare-assert": "^1.0.2",
724 "bare-cov": "^1.1.0",
725 "bare-fs": "^4.1.6",
726 "bare-os": "^3.6.1",
727 "bare-path": "^3.0.0",
728 "bare-process": "^4.2.1",
729 "bare-subprocess": "^5.0.0",
730 "bare-url": "^2.1.6",
731 "error-stack-parser": "^2.1.4",
732 "globbie": "^1.0.2",
733 "paparam": "^1.6.2",
734 "same-object": "^1.0.2",
735 "test-tmp": "^1.4.0",
736 "tmatch": "^5.0.0"
737 },
738 "bin": {
739 "brittle": "bin/node.js",
740 "brittle-bare": "bin/bare.js",
741 "brittle-node": "bin/node.js",
742 "brittle-pear": "bin/pear.js"
743 }
744 },
745 "node_modules/codecs": {
746 "version": "3.1.0",
747 "resolved": "https://registry.npmjs.org/codecs/-/codecs-3.1.0.tgz",
748 "integrity": "sha512-Dqx8NwvBvnMeuPQdVKy/XEF71igjR5apxBvCGeV0pP1tXadOiaLvDTXt7xh+/5wI1ASB195mXQGJbw3Ml4YDWQ==",
749 "license": "MIT",
750 "dependencies": {
751 "b4a": "^1.6.3"
752 }
753 },
754 "node_modules/compact-encoding": {
755 "version": "2.18.0",
756 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-2.18.0.tgz",
757 "integrity": "sha512-goACAOlhMI2xo5jGOMUDfOLnGdRE1jGfyZ+zie8N5114nHrbPIqf6GLUtzbLof6DSyrERlYRm3EcBplte5LcQw==",
758 "license": "Apache-2.0",
759 "dependencies": {
760 "b4a": "^1.3.0"
761 }
762 },
763 "node_modules/convert-source-map": {
764 "version": "2.0.0",
765 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
766 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
767 "dev": true,
768 "license": "MIT"
769 },
770 "node_modules/corestore": {
771 "version": "6.18.4",
772 "resolved": "https://registry.npmjs.org/corestore/-/corestore-6.18.4.tgz",
773 "integrity": "sha512-KL1cOUyj3DFRNzeVRekGPRiGN4sIfGo7BsncSJQQBT3Rh2y1nQ32MOR9X5DlOMlWIT4txwqBzhzam6+JXtwFVw==",
774 "license": "MIT",
775 "dependencies": {
776 "b4a": "^1.6.4",
777 "hypercore": "^10.37.10",
778 "hypercore-crypto": "^3.4.0",
779 "hypercore-id-encoding": "^1.2.0",
780 "read-write-mutexify": "^2.1.0",
781 "ready-resource": "^1.0.0",
782 "safety-catch": "^1.0.1",
783 "sodium-universal": "^4.0.0",
784 "xache": "^1.1.0"
785 }
786 },
787 "node_modules/crc-native": {
788 "version": "1.1.8",
789 "resolved": "https://registry.npmjs.org/crc-native/-/crc-native-1.1.8.tgz",
790 "integrity": "sha512-R/RYbD8lhG5M1k0iLX49tJdwHM0ipevJPpZw2Wk7b4EShyvx2P0uaq79xyRpcknjiVGiUfjFA8Gyy0mOlhABvw==",
791 "license": "Apache-2.0",
792 "optional": true,
793 "dependencies": {
794 "require-addon": "^1.1.0"
795 }
796 },
797 "node_modules/crc-universal": {
798 "version": "1.0.4",
799 "resolved": "https://registry.npmjs.org/crc-universal/-/crc-universal-1.0.4.tgz",
800 "integrity": "sha512-CE9xWEI6Gd5V0Bdmj5NoWH3d7+EIe4zUpy1sv6uvKYznzsDP1vhiWsTBmL6q9IrH2P6RRshp+8AkhR4CMtY5Hg==",
801 "license": "ISC",
802 "optionalDependencies": {
803 "crc-native": "^1.0.3"
804 }
805 },
806 "node_modules/debounceify": {
807 "version": "1.1.0",
808 "resolved": "https://registry.npmjs.org/debounceify/-/debounceify-1.1.0.tgz",
809 "integrity": "sha512-eKuHDVfJVg+u/0nPy8P+fhnLgbyuTgVxuCRrS/R7EpDSMMkBDgSes41MJtSAY1F1hcqfHz3Zy/qpqHHIp/EhdA==",
810 "license": "MIT"
811 },
812 "node_modules/error-stack-parser": {
813 "version": "2.1.4",
814 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
815 "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
816 "dev": true,
817 "license": "MIT",
818 "dependencies": {
819 "stackframe": "^1.3.4"
820 }
821 },
822 "node_modules/events-universal": {
823 "version": "1.0.1",
824 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
825 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
826 "license": "Apache-2.0",
827 "dependencies": {
828 "bare-events": "^2.7.0"
829 }
830 },
831 "node_modules/fast-fifo": {
832 "version": "1.3.2",
833 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
834 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
835 "license": "MIT"
836 },
837 "node_modules/flat-tree": {
838 "version": "1.13.0",
839 "resolved": "https://registry.npmjs.org/flat-tree/-/flat-tree-1.13.0.tgz",
840 "integrity": "sha512-fT3HIuCPwHhFgJ20QYzDHgUG0zMmFg5cHvFiFo5h+QMSJ28TihsEVY0f8HGliuO+pOzmvjMx1odToeaEWkTnyQ==",
841 "license": "MIT"
842 },
843 "node_modules/fs-native-extensions": {
844 "version": "1.4.5",
845 "resolved": "https://registry.npmjs.org/fs-native-extensions/-/fs-native-extensions-1.4.5.tgz",
846 "integrity": "sha512-ekV0T//iDm4AvhOcuPaHpxub4DI7HvY5ucLJVDvi7T2J+NZkQ9S6MuvgP0yeQvoqNUaAGyLjVYb1905BF9bpmg==",
847 "license": "Apache-2.0",
848 "optional": true,
849 "dependencies": {
850 "require-addon": "^1.1.0",
851 "which-runtime": "^1.2.0"
852 }
853 },
854 "node_modules/globbie": {
855 "version": "1.0.3",
856 "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz",
857 "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==",
858 "dev": true,
859 "license": "Apache-2.0",
860 "dependencies": {
861 "bare-fs": "^4.1.2",
862 "bare-path": "^3.0.0",
863 "bare-process": "^4.2.1",
864 "picomatch": "^4.0.2"
865 }
866 },
867 "node_modules/hyperbee": {
868 "version": "2.27.3",
869 "resolved": "https://registry.npmjs.org/hyperbee/-/hyperbee-2.27.3.tgz",
870 "integrity": "sha512-PXURH2U4juUZyJRKHTrY5z1zX851pmI1Q0jfv5F/hCIErDt/ND8jOZuxc3hfOLM9f0W3qJEDTMlV5AJBkVPy8w==",
871 "license": "MIT",
872 "dependencies": {
873 "b4a": "^1.6.0",
874 "codecs": "^3.0.0",
875 "debounceify": "^1.0.0",
876 "hypercore-errors": "^1.0.0",
877 "mutexify": "^1.4.0",
878 "protocol-buffers-encodings": "^1.2.0",
879 "rache": "^1.0.0",
880 "ready-resource": "^1.0.0",
881 "resolve-reject-promise": "^1.1.0",
882 "safety-catch": "^1.0.2",
883 "streamx": "^2.12.4",
884 "unslab": "^1.2.0"
885 }
886 },
887 "node_modules/hypercore": {
888 "version": "10.38.2",
889 "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-10.38.2.tgz",
890 "integrity": "sha512-2vx42g2RKdP/o2XzlxKfvgEaZ3+h/B855gCR8iscUDnxUoOZej7vXZ2buex0sweDUEW5aEfMxFR6o5hncio3Nw==",
891 "license": "MIT",
892 "dependencies": {
893 "@hyperswarm/secret-stream": "^6.0.0",
894 "b4a": "^1.1.0",
895 "bare-events": "^2.2.0",
896 "big-sparse-array": "^1.0.3",
897 "compact-encoding": "^2.11.0",
898 "crc-universal": "^1.0.2",
899 "fast-fifo": "^1.3.0",
900 "flat-tree": "^1.9.0",
901 "hypercore-crypto": "^3.2.1",
902 "hypercore-errors": "^1.2.0",
903 "hypercore-id-encoding": "^1.2.0",
904 "is-options": "^1.0.1",
905 "protomux": "^3.5.0",
906 "quickbit-universal": "^2.2.0",
907 "random-access-file": "^4.0.0",
908 "random-array-iterator": "^1.0.0",
909 "safety-catch": "^1.0.1",
910 "sodium-universal": "^4.0.0",
911 "streamx": "^2.12.4",
912 "unslab": "^1.3.0",
913 "xache": "^1.1.0",
914 "z32": "^1.0.0"
915 }
916 },
917 "node_modules/hypercore-crypto": {
918 "version": "3.6.1",
919 "resolved": "https://registry.npmjs.org/hypercore-crypto/-/hypercore-crypto-3.6.1.tgz",
920 "integrity": "sha512-ltIz2uDwy9pO/ZGTvqcjzyBkvt6O4cVm4r/nNxh0GFs/RbQtqP/i4wCvLEdmU7ptgtnw7fI67WYD1aHPuv4OVA==",
921 "license": "MIT",
922 "dependencies": {
923 "b4a": "^1.6.6",
924 "compact-encoding": "^2.15.0",
925 "sodium-universal": "^5.0.0"
926 }
927 },
928 "node_modules/hypercore-crypto/node_modules/sodium-native": {
929 "version": "5.0.10",
930 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
931 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
932 "license": "MIT",
933 "dependencies": {
934 "require-addon": "^1.1.0",
935 "which-runtime": "^1.2.1"
936 },
937 "engines": {
938 "bare": ">=1.16.0"
939 }
940 },
941 "node_modules/hypercore-crypto/node_modules/sodium-universal": {
942 "version": "5.0.1",
943 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
944 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
945 "license": "MIT",
946 "dependencies": {
947 "sodium-native": "^5.0.1"
948 },
949 "peerDependencies": {
950 "sodium-javascript": "~0.8.0"
951 },
952 "peerDependenciesMeta": {
953 "sodium-javascript": {
954 "optional": true
955 }
956 }
957 },
958 "node_modules/hypercore-errors": {
959 "version": "1.5.0",
960 "resolved": "https://registry.npmjs.org/hypercore-errors/-/hypercore-errors-1.5.0.tgz",
961 "integrity": "sha512-5KQ/SuDxsvet+7qWA35Ay6zdD9WyAHQoyWHGcPUTbmJBd300gvNIJoi3oma7kp4TTCSzii6qYumNZe/s0j/saQ==",
962 "license": "Apache-2.0",
963 "dependencies": {
964 "hypercore-id-encoding": "^1.3.0"
965 }
966 },
967 "node_modules/hypercore-id-encoding": {
968 "version": "1.3.0",
969 "resolved": "https://registry.npmjs.org/hypercore-id-encoding/-/hypercore-id-encoding-1.3.0.tgz",
970 "integrity": "sha512-W6sHdGo5h7LXEsoWfKf/KfuROZmZRQDlGqJF2EPHW+noCK66Vvr0+zE6cL0vqQi18s0kQPeN7Sq3QyR0Ytc2VQ==",
971 "license": "Apache-2.0",
972 "dependencies": {
973 "b4a": "^1.5.3",
974 "z32": "^1.0.0"
975 }
976 },
977 "node_modules/is-options": {
978 "version": "1.0.2",
979 "resolved": "https://registry.npmjs.org/is-options/-/is-options-1.0.2.tgz",
980 "integrity": "sha512-u+Ai74c8Q74aS8BuHwPdI1jptGOT1FQXgCq8/zv0xRuE+wRgSMEJLj8lVO8Zp9BeGb29BXY6AsNPinfqjkr7Fg==",
981 "license": "MIT",
982 "dependencies": {
983 "b4a": "^1.1.1"
984 }
985 },
986 "node_modules/mutexify": {
987 "version": "1.4.0",
988 "resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz",
989 "integrity": "sha512-pbYSsOrSB/AKN5h/WzzLRMFgZhClWccf2XIB4RSMC8JbquiB0e0/SH5AIfdQMdyHmYtv4seU7yV/TvAwPLJ1Yg==",
990 "license": "MIT",
991 "dependencies": {
992 "queue-tick": "^1.0.0"
993 }
994 },
995 "node_modules/nanoassert": {
996 "version": "2.0.0",
997 "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz",
998 "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==",
999 "license": "ISC"
1000 },
1001 "node_modules/noise-curve-ed": {
1002 "version": "2.1.0",
1003 "resolved": "https://registry.npmjs.org/noise-curve-ed/-/noise-curve-ed-2.1.0.tgz",
1004 "integrity": "sha512-zAzJx+VwZM3w6EA1hTmDhJfvAnCeBQn/1FAeZ0LtGxCcCtlAK/uJXQVF/eDVUOaAZ286lHlx77WJ+qj9SmsRRg==",
1005 "license": "ISC",
1006 "dependencies": {
1007 "b4a": "^1.1.0",
1008 "nanoassert": "^2.0.0",
1009 "sodium-universal": "^5.0.0"
1010 }
1011 },
1012 "node_modules/noise-curve-ed/node_modules/sodium-native": {
1013 "version": "5.0.10",
1014 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
1015 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
1016 "license": "MIT",
1017 "dependencies": {
1018 "require-addon": "^1.1.0",
1019 "which-runtime": "^1.2.1"
1020 },
1021 "engines": {
1022 "bare": ">=1.16.0"
1023 }
1024 },
1025 "node_modules/noise-curve-ed/node_modules/sodium-universal": {
1026 "version": "5.0.1",
1027 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1028 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1029 "license": "MIT",
1030 "dependencies": {
1031 "sodium-native": "^5.0.1"
1032 },
1033 "peerDependencies": {
1034 "sodium-javascript": "~0.8.0"
1035 },
1036 "peerDependenciesMeta": {
1037 "sodium-javascript": {
1038 "optional": true
1039 }
1040 }
1041 },
1042 "node_modules/noise-handshake": {
1043 "version": "4.2.0",
1044 "resolved": "https://registry.npmjs.org/noise-handshake/-/noise-handshake-4.2.0.tgz",
1045 "integrity": "sha512-9O/VTNX/E2/AToyMTTDU0J/4WhaXMTdqc2DHs9vf+snoZ0cenSBq0dNYTVV1snYYEkmo6QeRrYMxtqtoYnY+LA==",
1046 "license": "Apache-2.0",
1047 "dependencies": {
1048 "b4a": "^1.1.0",
1049 "nanoassert": "^2.0.0",
1050 "sodium-universal": "^5.0.0"
1051 }
1052 },
1053 "node_modules/noise-handshake/node_modules/sodium-native": {
1054 "version": "5.0.10",
1055 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
1056 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
1057 "license": "MIT",
1058 "dependencies": {
1059 "require-addon": "^1.1.0",
1060 "which-runtime": "^1.2.1"
1061 },
1062 "engines": {
1063 "bare": ">=1.16.0"
1064 }
1065 },
1066 "node_modules/noise-handshake/node_modules/sodium-universal": {
1067 "version": "5.0.1",
1068 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1069 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1070 "license": "MIT",
1071 "dependencies": {
1072 "sodium-native": "^5.0.1"
1073 },
1074 "peerDependencies": {
1075 "sodium-javascript": "~0.8.0"
1076 },
1077 "peerDependenciesMeta": {
1078 "sodium-javascript": {
1079 "optional": true
1080 }
1081 }
1082 },
1083 "node_modules/paparam": {
1084 "version": "1.10.0",
1085 "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.0.tgz",
1086 "integrity": "sha512-p36QQrwU3X6fj5d2JN20B4lcZI/O3fxMVUHc1xD7MNe5bcOf3jIgE0CIK5Zv/s5YAUGlobTJ/kKXrh1FJVrs3w==",
1087 "dev": true,
1088 "license": "Apache-2.0"
1089 },
1090 "node_modules/picomatch": {
1091 "version": "4.0.3",
1092 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
1093 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
1094 "dev": true,
1095 "license": "MIT",
1096 "engines": {
1097 "node": ">=12"
1098 },
1099 "funding": {
1100 "url": "https://github.com/sponsors/jonschlinkert"
1101 }
1102 },
1103 "node_modules/protocol-buffers-encodings": {
1104 "version": "1.2.0",
1105 "resolved": "https://registry.npmjs.org/protocol-buffers-encodings/-/protocol-buffers-encodings-1.2.0.tgz",
1106 "integrity": "sha512-daeNPuKh1NlLD1uDfbLpD+xyUTc07nEtfHwmBZmt/vH0B7VOM+JOCOpDcx9ZRpqHjAiIkGqyTDi+wfGSl17R9w==",
1107 "license": "MIT",
1108 "dependencies": {
1109 "b4a": "^1.6.0",
1110 "signed-varint": "^2.0.1",
1111 "varint": "5.0.0"
1112 }
1113 },
1114 "node_modules/protomux": {
1115 "version": "3.10.1",
1116 "resolved": "https://registry.npmjs.org/protomux/-/protomux-3.10.1.tgz",
1117 "integrity": "sha512-jgBqx8ZyaBWea/DFG4eOu1scOaeBwcnagiRC1XFVrjeGt7oAb0Pk5udPpBUpJ4DJBRjra50jD6YcZiQQTRqaaA==",
1118 "license": "MIT",
1119 "dependencies": {
1120 "b4a": "^1.3.1",
1121 "compact-encoding": "^2.5.1",
1122 "queue-tick": "^1.0.0",
1123 "safety-catch": "^1.0.1",
1124 "unslab": "^1.3.0"
1125 }
1126 },
1127 "node_modules/queue-tick": {
1128 "version": "1.0.1",
1129 "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
1130 "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
1131 "license": "MIT"
1132 },
1133 "node_modules/quickbit-native": {
1134 "version": "2.4.8",
1135 "resolved": "https://registry.npmjs.org/quickbit-native/-/quickbit-native-2.4.8.tgz",
1136 "integrity": "sha512-FcCcqI+nIAWGknqhtrYT5TSD7t/N+Xd8ctM+2PrIIBuwOi5hx0SxAvuPtzLIEMfT/2h9+fhBakUe2uALOHX6yw==",
1137 "license": "Apache-2.0",
1138 "optional": true,
1139 "dependencies": {
1140 "require-addon": "^1.1.0"
1141 }
1142 },
1143 "node_modules/quickbit-universal": {
1144 "version": "2.2.0",
1145 "resolved": "https://registry.npmjs.org/quickbit-universal/-/quickbit-universal-2.2.0.tgz",
1146 "integrity": "sha512-w02i1R8n7+6pEKTud8DfF8zbFY9o7RtPlUc3jWbtCkDKvhbx/AvV7oNnz4/TcmsPGpSJS+fq5Ud6RH6+YPvSGg==",
1147 "license": "ISC",
1148 "dependencies": {
1149 "b4a": "^1.6.0",
1150 "simdle-universal": "^1.1.0"
1151 },
1152 "optionalDependencies": {
1153 "quickbit-native": "^2.2.0"
1154 }
1155 },
1156 "node_modules/rache": {
1157 "version": "1.0.0",
1158 "resolved": "https://registry.npmjs.org/rache/-/rache-1.0.0.tgz",
1159 "integrity": "sha512-e0k0g0w/8jOCB+7YqCIlOa+OJ38k0wrYS4x18pMSmqOvLKoyhmMhmQyCcvfY6VaP8D75cqkEnlakXs+RYYLqNg==",
1160 "license": "Apache-2.0"
1161 },
1162 "node_modules/random-access-file": {
1163 "version": "4.1.2",
1164 "resolved": "https://registry.npmjs.org/random-access-file/-/random-access-file-4.1.2.tgz",
1165 "integrity": "sha512-GQM6R78DceZDcQod8KxlDFwXIiUvlvuy1EOzxTDsjuDjW5NlnlZi0MOk6iI4itAj/2vcvdqcEExYbVpC/dJcEw==",
1166 "license": "MIT",
1167 "dependencies": {
1168 "bare-fs": "^4.0.1",
1169 "bare-path": "^3.0.0",
1170 "random-access-storage": "^3.0.0"
1171 },
1172 "optionalDependencies": {
1173 "fs-native-extensions": "^1.3.1"
1174 }
1175 },
1176 "node_modules/random-access-storage": {
1177 "version": "3.0.2",
1178 "resolved": "https://registry.npmjs.org/random-access-storage/-/random-access-storage-3.0.2.tgz",
1179 "integrity": "sha512-Es9maUyWdJXWKckKy9s1+vT+DEgAt+PBb9lxPaake/0EDUsHehloKGv9v1zimS2V3gpFAcQXubvc1Rgci2sDPQ==",
1180 "license": "MIT",
1181 "dependencies": {
1182 "bare-events": "^2.2.0",
1183 "queue-tick": "^1.0.0"
1184 }
1185 },
1186 "node_modules/random-array-iterator": {
1187 "version": "1.0.0",
1188 "resolved": "https://registry.npmjs.org/random-array-iterator/-/random-array-iterator-1.0.0.tgz",
1189 "integrity": "sha512-u7xCM93XqKEvPTP6xZp2ehttcAemKnh73oKNf1FvzuVCfpt6dILDt1Kxl1LeBjm2iNIeR49VGFhy4Iz3yOun+Q==",
1190 "license": "MIT"
1191 },
1192 "node_modules/read-write-mutexify": {
1193 "version": "2.1.0",
1194 "resolved": "https://registry.npmjs.org/read-write-mutexify/-/read-write-mutexify-2.1.0.tgz",
1195 "integrity": "sha512-fDw/p5/acI1ytVY1UbxEDma/ej1yJH/n9NcjS9YNzcE6sPBPWdlru3ydRa/UBowUg4zqOvNMD5SOGYJrlQ6MzQ==",
1196 "license": "MIT"
1197 },
1198 "node_modules/ready-resource": {
1199 "version": "1.2.0",
1200 "resolved": "https://registry.npmjs.org/ready-resource/-/ready-resource-1.2.0.tgz",
1201 "integrity": "sha512-nfcco/8iAFV0M+2PYnmIc+/xY0iRb35d42HFHQ7AfjulbGEAFa+XWpByfwSyeVeiBoMLLFVMv1HixxNCqzSQ1g==",
1202 "license": "MIT",
1203 "dependencies": {
1204 "bare-events": "^2.2.0"
1205 }
1206 },
1207 "node_modules/require-addon": {
1208 "version": "1.2.0",
1209 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
1210 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
1211 "license": "Apache-2.0",
1212 "dependencies": {
1213 "bare-addon-resolve": "^1.3.0"
1214 },
1215 "engines": {
1216 "bare": ">=1.10.0"
1217 }
1218 },
1219 "node_modules/resolve-reject-promise": {
1220 "version": "1.1.0",
1221 "resolved": "https://registry.npmjs.org/resolve-reject-promise/-/resolve-reject-promise-1.1.0.tgz",
1222 "integrity": "sha512-LWsTOA91AqzBTjSGgX79Tc130pwcBK6xjpJEO+qRT5IKZ6bGnHKcc8QL3upUBcWuU8OTIDzKK2VNSwmmlqvAVg==",
1223 "license": "MIT"
1224 },
1225 "node_modules/safety-catch": {
1226 "version": "1.0.2",
1227 "resolved": "https://registry.npmjs.org/safety-catch/-/safety-catch-1.0.2.tgz",
1228 "integrity": "sha512-C1UYVZ4dtbBxEtvOcpjBaaD27nP8MlvyAQEp2fOTOEe6pfUpk1cDUxij6BR1jZup6rSyUTaBBplK7LanskrULA==",
1229 "license": "MIT"
1230 },
1231 "node_modules/same-object": {
1232 "version": "1.0.2",
1233 "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz",
1234 "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==",
1235 "dev": true,
1236 "license": "MIT"
1237 },
1238 "node_modules/signed-varint": {
1239 "version": "2.0.1",
1240 "resolved": "https://registry.npmjs.org/signed-varint/-/signed-varint-2.0.1.tgz",
1241 "integrity": "sha512-abgDPg1106vuZZOvw7cFwdCABddfJRz5akcCcchzTbhyhYnsG31y4AlZEgp315T7W3nQq5P4xeOm186ZiPVFzw==",
1242 "license": "MIT",
1243 "dependencies": {
1244 "varint": "~5.0.0"
1245 }
1246 },
1247 "node_modules/simdle-native": {
1248 "version": "1.3.9",
1249 "resolved": "https://registry.npmjs.org/simdle-native/-/simdle-native-1.3.9.tgz",
1250 "integrity": "sha512-Isc8sP4OiiIU0mpslD4GHEnR0VQWvR/54WN7YtwEDkNdTJVWtpmvsSvsgRlw5BNGxdYXlVRegdnrSu10H/PhvA==",
1251 "license": "Apache-2.0",
1252 "optional": true,
1253 "dependencies": {
1254 "b4a": "^1.6.0",
1255 "require-addon": "^1.1.0"
1256 }
1257 },
1258 "node_modules/simdle-universal": {
1259 "version": "1.1.2",
1260 "resolved": "https://registry.npmjs.org/simdle-universal/-/simdle-universal-1.1.2.tgz",
1261 "integrity": "sha512-3n3w1bs+uwgHKQjt6arez83EywNlhZzYvNOhvAASTl/8KqNIcqr6aHyGt3JRlfuUC7iB0tomJRPlJ2cRGIpBzA==",
1262 "license": "ISC",
1263 "dependencies": {
1264 "b4a": "^1.6.0"
1265 },
1266 "optionalDependencies": {
1267 "simdle-native": "^1.1.1"
1268 }
1269 },
1270 "node_modules/sodium-native": {
1271 "version": "4.3.3",
1272 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz",
1273 "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==",
1274 "license": "MIT",
1275 "dependencies": {
1276 "require-addon": "^1.1.0"
1277 }
1278 },
1279 "node_modules/sodium-secretstream": {
1280 "version": "1.2.0",
1281 "resolved": "https://registry.npmjs.org/sodium-secretstream/-/sodium-secretstream-1.2.0.tgz",
1282 "integrity": "sha512-q/DbraNFXm1KfCiiZvapmz5UC3OlpirYFIvBK2MhGaOFSb3gRyk8OXTi17UI9SGfshQNCpsVvlopogbzZNyW6Q==",
1283 "license": "MIT",
1284 "dependencies": {
1285 "b4a": "^1.1.1",
1286 "sodium-universal": "^5.0.0"
1287 }
1288 },
1289 "node_modules/sodium-secretstream/node_modules/sodium-native": {
1290 "version": "5.0.10",
1291 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
1292 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
1293 "license": "MIT",
1294 "dependencies": {
1295 "require-addon": "^1.1.0",
1296 "which-runtime": "^1.2.1"
1297 },
1298 "engines": {
1299 "bare": ">=1.16.0"
1300 }
1301 },
1302 "node_modules/sodium-secretstream/node_modules/sodium-universal": {
1303 "version": "5.0.1",
1304 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1305 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1306 "license": "MIT",
1307 "dependencies": {
1308 "sodium-native": "^5.0.1"
1309 },
1310 "peerDependencies": {
1311 "sodium-javascript": "~0.8.0"
1312 },
1313 "peerDependenciesMeta": {
1314 "sodium-javascript": {
1315 "optional": true
1316 }
1317 }
1318 },
1319 "node_modules/sodium-universal": {
1320 "version": "4.0.1",
1321 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-4.0.1.tgz",
1322 "integrity": "sha512-sNp13PrxYLaUFHTGoDKkSDFvoEu51bfzE12RwGlqU1fcrkpAOK0NvizaJzOWV0Omtk9me2+Pnbjcf/l0efxuGQ==",
1323 "license": "MIT",
1324 "dependencies": {
1325 "sodium-native": "^4.0.0"
1326 },
1327 "peerDependencies": {
1328 "sodium-javascript": "~0.8.0"
1329 },
1330 "peerDependenciesMeta": {
1331 "sodium-javascript": {
1332 "optional": true
1333 }
1334 }
1335 },
1336 "node_modules/stackframe": {
1337 "version": "1.3.4",
1338 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
1339 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
1340 "dev": true,
1341 "license": "MIT"
1342 },
1343 "node_modules/streamx": {
1344 "version": "2.23.0",
1345 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
1346 "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
1347 "license": "MIT",
1348 "dependencies": {
1349 "events-universal": "^1.0.0",
1350 "fast-fifo": "^1.3.2",
1351 "text-decoder": "^1.1.0"
1352 }
1353 },
1354 "node_modules/test-tmp": {
1355 "version": "1.4.0",
1356 "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz",
1357 "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==",
1358 "dev": true,
1359 "license": "MIT",
1360 "dependencies": {
1361 "bare-fs": "^4.0.1",
1362 "bare-os": "^3.3.0",
1363 "bare-path": "^3.0.0"
1364 }
1365 },
1366 "node_modules/text-decoder": {
1367 "version": "1.2.6",
1368 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.6.tgz",
1369 "integrity": "sha512-27FeW5GQFDfw0FpwMQhMagB7BztOOlmjcSRi97t2oplhKVTZtp0DZbSegSaXS5IIC6mxMvBG4AR1Sgc6BX3CQg==",
1370 "license": "Apache-2.0",
1371 "dependencies": {
1372 "b4a": "^1.6.4"
1373 }
1374 },
1375 "node_modules/timeout-refresh": {
1376 "version": "2.0.1",
1377 "resolved": "https://registry.npmjs.org/timeout-refresh/-/timeout-refresh-2.0.1.tgz",
1378 "integrity": "sha512-SVqEcMZBsZF9mA78rjzCrYrUs37LMJk3ShZ851ygZYW1cMeIjs9mL57KO6Iv5mmjSQnOe/29/VAfGXo+oRCiVw==",
1379 "license": "MIT"
1380 },
1381 "node_modules/tmatch": {
1382 "version": "5.0.0",
1383 "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz",
1384 "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==",
1385 "dev": true,
1386 "license": "ISC",
1387 "engines": {
1388 "node": ">=8"
1389 }
1390 },
1391 "node_modules/unslab": {
1392 "version": "1.3.0",
1393 "resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz",
1394 "integrity": "sha512-YATkfKAFj47kTzmiQrWXMyRvaVrHsW6MEALa4bm+FhiA2YG4oira+Z3DXN6LrYOYn2Y8eO94Lwl9DOHjs1FpoQ==",
1395 "license": "Apache-2.0",
1396 "dependencies": {
1397 "b4a": "^1.6.6"
1398 }
1399 },
1400 "node_modules/v8-to-istanbul": {
1401 "version": "9.3.0",
1402 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
1403 "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
1404 "dev": true,
1405 "license": "ISC",
1406 "dependencies": {
1407 "@jridgewell/trace-mapping": "^0.3.12",
1408 "@types/istanbul-lib-coverage": "^2.0.1",
1409 "convert-source-map": "^2.0.0"
1410 },
1411 "engines": {
1412 "node": ">=10.12.0"
1413 }
1414 },
1415 "node_modules/varint": {
1416 "version": "5.0.0",
1417 "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz",
1418 "integrity": "sha512-gC13b/bWrqQoKY2EmROCZ+AR0jitc6DnDGaQ6Ls9QpKmuSgJB1eQ7H3KETtQm7qSdMWMKCmsshyCmUwMLh3OAA==",
1419 "license": "MIT"
1420 },
1421 "node_modules/which-runtime": {
1422 "version": "1.3.2",
1423 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
1424 "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
1425 "license": "Apache-2.0"
1426 },
1427 "node_modules/xache": {
1428 "version": "1.2.1",
1429 "resolved": "https://registry.npmjs.org/xache/-/xache-1.2.1.tgz",
1430 "integrity": "sha512-igRS6jPreJ54ABdzhh4mCDXcz+XMaWO2q1ABRV2yWYuk29jlp8VT7UBdCqNkX7rpYBbXsebVVKkwIuYZjyZNqA==",
1431 "license": "MIT"
1432 },
1433 "node_modules/z32": {
1434 "version": "1.1.0",
1435 "resolved": "https://registry.npmjs.org/z32/-/z32-1.1.0.tgz",
1436 "integrity": "sha512-1WUHy+VS6d0HPNspDxvLssBbeQjXMjSnpv0vH82vRAUfg847NmX3OXozp/hRP5jPhxBbrVzrgvAt+UsGNzRFQQ==",
1437 "license": "MIT",
1438 "dependencies": {
1439 "b4a": "^1.5.3"
1440 }
1441 }
1442 }
1443}
hyper-board/package.json +24 -0
……@@ -0,0 +1,24 @@
1{
2 "name": "hyper-board",
3 "version": "1.0.0",
4 "description": "P2P kanban board backed by Hyperbee",
5 "type": "module",
6 "main": "index.js",
7 "pear": {
8 "name": "hyper-board",
9 "type": "terminal"
10 },
11 "scripts": {
12 "test": "brittle test/*.test.js"
13 },
14 "dependencies": {
15 "hyperbee": "^2.0.0",
16 "corestore": "^6.0.0",
17 "b4a": "^1.0.0",
18 "compact-encoding": "^2.0.0"
19 },
20 "devDependencies": {
21 "brittle": "^3.0.0"
22 },
23 "license": "MIT"
24}
hyper-board/test/board.test.js +328 -0
……@@ -0,0 +1,328 @@
1import test from 'brittle'
2import { Board } from '../lib/board.js'
3import { tmpdir } from 'os'
4import { join } from 'path'
5import { randomBytes } from 'crypto'
6
7function tmpStorage () {
8 return join(tmpdir(), 'hyper-board-test-' + randomBytes(8).toString('hex'))
9}
10
11test('Board - ready creates default lanes', async (t) => {
12 const board = new Board(tmpStorage())
13 await board.ready()
14
15 const lanes = await board.listLanes()
16 t.is(lanes.length, 4)
17 t.is(lanes[0].name, 'backlog')
18 t.is(lanes[1].name, 'todo')
19 t.is(lanes[2].name, 'in-progress')
20 t.is(lanes[3].name, 'done')
21
22 await board.close()
23})
24
25test('Board - create and get card', async (t) => {
26 const board = new Board(tmpStorage())
27 await board.ready()
28
29 const card = await board.createCard({
30 title: 'Fix bug',
31 description: 'Critical production bug',
32 lane: 'todo',
33 priority: 'high',
34 tags: ['bug', 'production']
35 })
36
37 t.ok(card.id)
38 t.is(card.title, 'Fix bug')
39 t.is(card.lane, 'todo')
40 t.is(card.priority, 'high')
41 t.alike(card.tags, ['bug', 'production'])
42 t.ok(card.created > 0)
43
44 const fetched = await board.getCard(card.id)
45 t.is(fetched.title, 'Fix bug')
46
47 await board.close()
48})
49
50test('Board - update card', async (t) => {
51 const board = new Board(tmpStorage())
52 await board.ready()
53
54 const card = await board.createCard({ title: 'Task A' })
55 const updated = await board.updateCard(card.id, {
56 title: 'Task A (updated)',
57 description: 'Now with details',
58 assignee: 'fleeky'
59 })
60
61 t.is(updated.title, 'Task A (updated)')
62 t.is(updated.assignee, 'fleeky')
63 t.is(updated.id, card.id)
64 t.is(updated.created, card.created)
65 t.ok(updated.updated >= card.updated)
66
67 await board.close()
68})
69
70test('Board - delete card', async (t) => {
71 const board = new Board(tmpStorage())
72 await board.ready()
73
74 const card = await board.createCard({ title: 'Delete me' })
75 t.ok(await board.getCard(card.id))
76
77 const ok = await board.deleteCard(card.id)
78 t.ok(ok)
79 t.is(await board.getCard(card.id), null)
80
81 // Deleting non-existent returns false
82 const nope = await board.deleteCard('nonexistent')
83 t.is(nope, false)
84
85 await board.close()
86})
87
88test('Board - move card between lanes', async (t) => {
89 const board = new Board(tmpStorage())
90 await board.ready()
91
92 const card = await board.createCard({ title: 'Move me', lane: 'backlog' })
93 t.is(card.lane, 'backlog')
94
95 const moved = await board.moveCard(card.id, 'in-progress')
96 t.is(moved.lane, 'in-progress')
97
98 const fetched = await board.getCard(card.id)
99 t.is(fetched.lane, 'in-progress')
100
101 await board.close()
102})
103
104test('Board - list cards with lane filter', async (t) => {
105 const board = new Board(tmpStorage())
106 await board.ready()
107
108 await board.createCard({ title: 'A', lane: 'todo' })
109 await board.createCard({ title: 'B', lane: 'todo' })
110 await board.createCard({ title: 'C', lane: 'done' })
111
112 const todo = await board.listCards({ lane: 'todo' })
113 t.is(todo.length, 2)
114
115 const done = await board.listCards({ lane: 'done' })
116 t.is(done.length, 1)
117
118 const all = await board.listCards()
119 t.is(all.length, 3)
120
121 await board.close()
122})
123
124test('Board - list cards with assignee filter', async (t) => {
125 const board = new Board(tmpStorage())
126 await board.ready()
127
128 await board.createCard({ title: 'A', assignee: 'alice' })
129 await board.createCard({ title: 'B', assignee: 'bob' })
130 await board.createCard({ title: 'C', assignee: 'alice' })
131
132 const alice = await board.listCards({ assignee: 'alice' })
133 t.is(alice.length, 2)
134
135 await board.close()
136})
137
138test('Board - list cards with tag filter', async (t) => {
139 const board = new Board(tmpStorage())
140 await board.ready()
141
142 await board.createCard({ title: 'A', tags: ['bug'] })
143 await board.createCard({ title: 'B', tags: ['feature'] })
144 await board.createCard({ title: 'C', tags: ['bug', 'urgent'] })
145
146 const bugs = await board.listCards({ tag: 'bug' })
147 t.is(bugs.length, 2)
148
149 await board.close()
150})
151
152test('Board - priority sorting', async (t) => {
153 const board = new Board(tmpStorage())
154 await board.ready()
155
156 await board.createCard({ title: 'Normal', priority: 'normal' })
157 await board.createCard({ title: 'Urgent', priority: 'urgent' })
158 await board.createCard({ title: 'Low', priority: 'low' })
159 await board.createCard({ title: 'High', priority: 'high' })
160
161 const cards = await board.listCards()
162 t.is(cards[0].title, 'Urgent')
163 t.is(cards[1].title, 'High')
164 t.is(cards[2].title, 'Normal')
165 t.is(cards[3].title, 'Low')
166
167 await board.close()
168})
169
170test('Board - add and remove lane', async (t) => {
171 const board = new Board(tmpStorage())
172 await board.ready()
173
174 await board.addLane('review', 2)
175 const lanes = await board.listLanes()
176 t.ok(lanes.find(l => l.name === 'review'))
177
178 await board.removeLane('review')
179 const lanes2 = await board.listLanes()
180 t.absent(lanes2.find(l => l.name === 'review'))
181
182 await board.close()
183})
184
185test('Board - cannot remove lane with cards', async (t) => {
186 const board = new Board(tmpStorage())
187 await board.ready()
188
189 await board.createCard({ title: 'Blocker', lane: 'todo' })
190
191 try {
192 await board.removeLane('todo')
193 t.fail('Should have thrown')
194 } catch (err) {
195 t.ok(err.message.includes('Cannot remove'))
196 t.ok(err.message.includes('1 cards'))
197 }
198
199 await board.close()
200})
201
202test('Board - duplicate lane name rejected', async (t) => {
203 const board = new Board(tmpStorage())
204 await board.ready()
205
206 try {
207 await board.addLane('todo')
208 t.fail('Should have thrown')
209 } catch (err) {
210 t.ok(err.message.includes('already exists'))
211 }
212
213 await board.close()
214})
215
216test('Board - search by title', async (t) => {
217 const board = new Board(tmpStorage())
218 await board.ready()
219
220 await board.createCard({ title: 'Fix authentication bug' })
221 await board.createCard({ title: 'Add new feature' })
222 await board.createCard({ title: 'Bug in login page' })
223
224 const results = await board.search('bug')
225 t.is(results.length, 2)
226
227 await board.close()
228})
229
230test('Board - search by description', async (t) => {
231 const board = new Board(tmpStorage())
232 await board.ready()
233
234 await board.createCard({ title: 'Task', description: 'needs performance optimization' })
235 await board.createCard({ title: 'Other' })
236
237 const results = await board.search('performance')
238 t.is(results.length, 1)
239 t.is(results[0].title, 'Task')
240
241 await board.close()
242})
243
244test('Board - search by tag', async (t) => {
245 const board = new Board(tmpStorage())
246 await board.ready()
247
248 await board.createCard({ title: 'A', tags: ['frontend'] })
249 await board.createCard({ title: 'B', tags: ['backend'] })
250
251 const results = await board.search('frontend')
252 t.is(results.length, 1)
253
254 await board.close()
255})
256
257test('Board - stats', async (t) => {
258 const board = new Board(tmpStorage())
259 await board.ready()
260
261 await board.createCard({ title: 'A', lane: 'todo' })
262 await board.createCard({ title: 'B', lane: 'todo' })
263 await board.createCard({ title: 'C', lane: 'done' })
264
265 const stats = await board.stats()
266 t.is(stats.totalCards, 3)
267 t.is(stats.totalLanes, 4)
268 t.is(stats.byLane.todo, 2)
269 t.is(stats.byLane.done, 1)
270 t.is(stats.byLane.backlog, 0)
271
272 await board.close()
273})
274
275test('Board - view returns lanes with cards', async (t) => {
276 const board = new Board(tmpStorage())
277 await board.ready()
278
279 await board.createCard({ title: 'A', lane: 'todo' })
280 await board.createCard({ title: 'B', lane: 'done' })
281
282 const view = await board.view()
283 t.is(view.length, 4) // all 4 default lanes
284
285 const todoView = view.find(l => l.name === 'todo')
286 t.is(todoView.cards.length, 1)
287 t.is(todoView.cards[0].title, 'A')
288
289 const doneView = view.find(l => l.name === 'done')
290 t.is(doneView.cards.length, 1)
291
292 const backlogView = view.find(l => l.name === 'backlog')
293 t.is(backlogView.cards.length, 0)
294
295 await board.close()
296})
297
298test('Board - persistence', async (t) => {
299 const storage = tmpStorage()
300
301 // Create and populate
302 const b1 = new Board(storage)
303 await b1.ready()
304 await b1.createCard({ title: 'Persistent', lane: 'todo' })
305 await b1.close()
306
307 // Reopen
308 const b2 = new Board(storage)
309 await b2.ready()
310 const cards = await b2.listCards()
311 t.is(cards.length, 1)
312 t.is(cards[0].title, 'Persistent')
313 await b2.close()
314})
315
316test('Board - update nonexistent card throws', async (t) => {
317 const board = new Board(tmpStorage())
318 await board.ready()
319
320 try {
321 await board.updateCard('nonexistent', { title: 'X' })
322 t.fail('Should have thrown')
323 } catch (err) {
324 t.ok(err.message.includes('not found'))
325 }
326
327 await board.close()
328})
hyper-daemon/daemons/crosspost.js +180 -0
……@@ -0,0 +1,180 @@
1// Crosspost daemon — selective content syndication
2// Routes messages between platforms based on configurable rules
3
4const PLATFORM_LIMITS = {
5 mastodon: 500,
6 keet: 0, // no limit
7 irc: 450,
8 discord: 2000,
9 matrix: 0 // no limit
10}
11
12const DEFAULT_RULES = [
13 // keet messages tagged #public → mastodon
14 { from: 'keet', to: 'mastodon', filter: { tags: ['public'] } },
15 // all mastodon posts → keet
16 { from: 'mastodon', to: 'keet', filter: {} }
17]
18
19// Check if a message matches a filter
20function matchFilter (msg, filter) {
21 if (!filter || Object.keys(filter).length === 0) return true
22
23 // Tag filter — at least one tag must appear in the text
24 if (filter.tags && filter.tags.length > 0) {
25 const text = (msg.text || '').toLowerCase()
26 const hasTag = filter.tags.some(tag => {
27 const normalized = tag.toLowerCase()
28 // Match #tag or plain tag
29 return text.includes('#' + normalized) || text.includes(normalized)
30 })
31 if (!hasTag) return false
32 }
33
34 // Author filter — exact match
35 if (filter.author && msg.author !== filter.author) return false
36
37 // Type filter — message type (text, image, link, etc.)
38 if (filter.type && msg.type !== filter.type) return false
39
40 // Text regex pattern
41 if (filter.pattern) {
42 try {
43 const regex = new RegExp(filter.pattern, 'i')
44 if (!regex.test(msg.text || '')) return false
45 } catch {
46 return false
47 }
48 }
49
50 // Contains — simple substring
51 if (filter.contains) {
52 if (!(msg.text || '').includes(filter.contains)) return false
53 }
54
55 return true
56}
57
58// Strip hashtags used for routing (e.g., #public) to keep output clean
59function stripRoutingTags (text, tags) {
60 if (!tags || !tags.length) return text
61 let result = text
62 for (const tag of tags) {
63 // Remove #tag variants (case-insensitive)
64 result = result.replace(new RegExp('#' + tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'), '')
65 }
66 return result.replace(/\s{2,}/g, ' ').trim()
67}
68
69// Adapt message text for target platform
70function formatForTarget (msg, target, rule) {
71 const author = msg.author || 'unknown'
72 const source = msg.source || 'unknown'
73 let text = msg.text || ''
74
75 // Strip routing tags from output
76 if (rule && rule.filter && rule.filter.tags) {
77 text = stripRoutingTags(text, rule.filter.tags)
78 }
79
80 const attribution = `[via ${source}/${author}]`
81
82 switch (target) {
83 case 'mastodon': {
84 const limit = PLATFORM_LIMITS.mastodon
85 const prefix = attribution + ' '
86 const available = limit - prefix.length
87 if (text.length > available) {
88 text = text.slice(0, available - 3) + '...'
89 }
90 return prefix + text
91 }
92
93 case 'keet': {
94 return `${attribution} ${text}`
95 }
96
97 case 'irc': {
98 const limit = PLATFORM_LIMITS.irc
99 const prefix = `<${author}@${source}> `
100 const available = limit - prefix.length
101 if (text.length > available) {
102 text = text.slice(0, available - 3) + '...'
103 }
104 return prefix + text
105 }
106
107 case 'discord': {
108 return `**${author}** (from ${source}):\n${text}`
109 }
110
111 case 'matrix': {
112 return `[${source}] <${author}> ${text}`
113 }
114
115 default: {
116 return `${attribution} ${text}`
117 }
118 }
119}
120
121const crosspost = {
122 name: 'crosspost',
123 description: 'Selective syndication -- route content to specific platforms',
124
125 defaultConfig: {
126 rules: DEFAULT_RULES
127 },
128
129 // Process a message through all rules
130 // msg: { source, text, tags, author, type, timestamp }
131 // config: { rules: [...] }
132 // Returns: [{ target, text, metadata }]
133 process (msg, config) {
134 const rules = (config && config.rules) || DEFAULT_RULES
135 const actions = []
136
137 for (const rule of rules) {
138 // Source platform must match
139 if (msg.source !== rule.from) continue
140
141 // Filter must match
142 if (!matchFilter(msg, rule.filter)) continue
143
144 actions.push({
145 target: rule.to,
146 text: formatForTarget(msg, rule.to, rule),
147 metadata: {
148 crosspostedFrom: msg.source,
149 originalAuthor: msg.author,
150 originalTimestamp: msg.timestamp || Date.now(),
151 rule: { from: rule.from, to: rule.to }
152 }
153 })
154 }
155
156 return actions
157 },
158
159 // Generate a daemon script for the DaemonRuntime
160 // Wraps process() into the trigger/action format the runtime expects
161 toDaemon (config, opts = {}) {
162 const targetRoom = opts.targetRoom
163 const sourceRoom = opts.sourceRoom
164
165 return {
166 trigger: {
167 type: 'message',
168 filter: {}
169 },
170 actions: [{
171 type: 'send-message',
172 roomId: targetRoom,
173 text: `[crosspost from ${(sourceRoom || '').slice(0, 8) || 'source'}] {author}: {text}`
174 }],
175 enabled: true
176 }
177 }
178}
179
180export { crosspost, matchFilter, formatForTarget, stripRoutingTags, PLATFORM_LIMITS }
hyper-daemon/daemons/notify.js +23 -0
……@@ -0,0 +1,23 @@
1// Notify daemon — alert on keyword mentions
2// Sends a notification message when a keyword is mentioned
3
4function notify (opts = {}) {
5 const keywords = opts.keywords || []
6 const notifyRoom = opts.notifyRoom
7 const pattern = keywords.map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')
8
9 return {
10 trigger: {
11 type: 'message',
12 filter: { pattern }
13 },
14 actions: [{
15 type: 'send-message',
16 roomId: notifyRoom,
17 text: `🔔 Keyword match from {author}: {text}`
18 }],
19 enabled: true
20 }
21}
22
23export { notify }
hyper-daemon/daemons/summarize.js +300 -0
……@@ -0,0 +1,300 @@
1// Summarize daemon — extractive conversation summarizer
2// Collects chat messages and produces summaries without LLM
3
4// Common English stop words to filter from topic extraction
5const STOP_WORDS = new Set([
6 'the', 'be', 'to', 'of', 'and', 'a', 'in', 'that', 'have', 'i',
7 'it', 'for', 'not', 'on', 'with', 'he', 'as', 'you', 'do', 'at',
8 'this', 'but', 'his', 'by', 'from', 'they', 'we', 'say', 'her',
9 'she', 'or', 'an', 'will', 'my', 'one', 'all', 'would', 'there',
10 'their', 'what', 'so', 'up', 'out', 'if', 'about', 'who', 'get',
11 'which', 'go', 'me', 'when', 'make', 'can', 'like', 'time', 'no',
12 'just', 'him', 'know', 'take', 'people', 'into', 'year', 'your',
13 'good', 'some', 'could', 'them', 'see', 'other', 'than', 'then',
14 'now', 'look', 'only', 'come', 'its', 'over', 'think', 'also',
15 'back', 'after', 'use', 'two', 'how', 'our', 'work', 'first',
16 'well', 'way', 'even', 'new', 'want', 'because', 'any', 'these',
17 'give', 'day', 'most', 'us', 'is', 'am', 'are', 'was', 'were',
18 'been', 'being', 'has', 'had', 'having', 'does', 'did', 'doing',
19 'should', 'more', 'very', 'too', 'here', 'where', 'why', 'yes',
20 'yeah', 'ok', 'okay', 'sure', 'right', 'oh', 'hi', 'hey', 'lol',
21 'haha', 'thanks', 'thank', 'im', 'dont', 'thats', 'gonna', 'got',
22 'really', 'thing', 'things', 'much', 'still', 'let', 'need',
23 'been', 'going', 'did', 'made', 'said', 'each', 'tell', 'does',
24 'set', 'three', 'own', 'point', 'end', 'while', 'same'
25])
26
27// Tokenize text into lowercase words, stripping punctuation
28function tokenize (text) {
29 if (!text) return []
30 return text
31 .toLowerCase()
32 .replace(/https?:\/\/\S+/g, '') // strip URLs
33 .replace(/[^a-z0-9\s'-]/g, ' ')
34 .split(/\s+/)
35 .filter(w => w.length > 2 && !STOP_WORDS.has(w))
36}
37
38// Extract most frequent non-stopword terms (3+ occurrences by default)
39function extractTopics (messages, opts = {}) {
40 const minCount = opts.minCount || 3
41 const maxTopics = opts.maxTopics || 10
42 const freq = new Map()
43
44 for (const msg of messages) {
45 const words = tokenize(msg.text)
46 // Count unique words per message to avoid spam-inflation
47 const unique = new Set(words)
48 for (const word of unique) {
49 freq.set(word, (freq.get(word) || 0) + 1)
50 }
51 }
52
53 // Filter by minimum count, sort by frequency descending
54 return Array.from(freq.entries())
55 .filter(([, count]) => count >= minCount)
56 .sort((a, b) => b[1] - a[1])
57 .slice(0, maxTopics)
58 .map(([word, count]) => ({ word, count }))
59}
60
61// Identify key messages: questions, links, or high-engagement content
62function extractKeyMessages (messages, opts = {}) {
63 const maxKey = opts.maxKeyMessages || 5
64 const scored = []
65
66 for (const msg of messages) {
67 const text = msg.text || ''
68 let score = 0
69
70 // Questions get high priority
71 if (text.includes('?')) score += 3
72
73 // Links are informational
74 if (/https?:\/\/\S+/.test(text)) score += 2
75
76 // Code blocks indicate technical discussion
77 if (text.includes('```') || text.includes('`')) score += 1
78
79 // Longer messages tend to be more substantive
80 if (text.length > 100) score += 1
81 if (text.length > 200) score += 1
82
83 // Exclamation marks indicate emphasis
84 if (text.includes('!')) score += 1
85
86 if (score > 0) {
87 scored.push({ msg, score })
88 }
89 }
90
91 return scored
92 .sort((a, b) => b.score - a.score)
93 .slice(0, maxKey)
94 .map(s => s.msg)
95}
96
97// Get unique participants
98function extractParticipants (messages) {
99 const participants = new Map()
100
101 for (const msg of messages) {
102 const author = msg.author || 'unknown'
103 participants.set(author, (participants.get(author) || 0) + 1)
104 }
105
106 return Array.from(participants.entries())
107 .sort((a, b) => b[1] - a[1])
108 .map(([name, count]) => ({ name, count }))
109}
110
111// Calculate time range of the conversation
112function getTimeRange (messages) {
113 if (!messages.length) return { start: 0, end: 0, durationMs: 0 }
114
115 const timestamps = messages
116 .map(m => m.timestamp || 0)
117 .filter(t => t > 0)
118
119 if (!timestamps.length) return { start: 0, end: 0, durationMs: 0 }
120
121 const start = Math.min(...timestamps)
122 const end = Math.max(...timestamps)
123
124 return {
125 start,
126 end,
127 durationMs: end - start
128 }
129}
130
131// Format duration in human-readable form
132function formatDuration (ms) {
133 if (ms < 60000) return 'less than a minute'
134 const minutes = Math.floor(ms / 60000)
135 if (minutes < 60) return `${minutes} minute${minutes !== 1 ? 's' : ''}`
136 const hours = Math.floor(minutes / 60)
137 const remaining = minutes % 60
138 if (hours < 24) {
139 if (remaining === 0) return `${hours} hour${hours !== 1 ? 's' : ''}`
140 return `${hours}h ${remaining}m`
141 }
142 const days = Math.floor(hours / 24)
143 return `${days} day${days !== 1 ? 's' : ''}`
144}
145
146// Build the markdown summary
147function formatSummary (participants, topics, keyMessages, timeRange, messageCount) {
148 const lines = []
149
150 lines.push(`## Conversation Summary`)
151 lines.push('')
152
153 // Stats line
154 const parts = [`${messageCount} messages`]
155 if (participants.length > 0) {
156 parts.push(`${participants.length} participant${participants.length !== 1 ? 's' : ''}`)
157 }
158 if (timeRange.durationMs > 0) {
159 parts.push(`over ${formatDuration(timeRange.durationMs)}`)
160 }
161 lines.push(parts.join(', '))
162 lines.push('')
163
164 // Participants
165 if (participants.length > 0) {
166 lines.push('### Participants')
167 for (const p of participants) {
168 lines.push(`- **${p.name}** (${p.count} message${p.count !== 1 ? 's' : ''})`)
169 }
170 lines.push('')
171 }
172
173 // Topics
174 if (topics.length > 0) {
175 lines.push('### Topics')
176 for (const t of topics) {
177 lines.push(`- ${t.word} (${t.count}x)`)
178 }
179 lines.push('')
180 }
181
182 // Key messages
183 if (keyMessages.length > 0) {
184 lines.push('### Key Messages')
185 for (const msg of keyMessages) {
186 const author = msg.author || 'unknown'
187 const text = (msg.text || '').slice(0, 150)
188 const ellipsis = (msg.text || '').length > 150 ? '...' : ''
189 lines.push(`- **${author}**: ${text}${ellipsis}`)
190 }
191 lines.push('')
192 }
193
194 return lines.join('\n')
195}
196
197const summarize = {
198 name: 'summarize',
199 description: 'Auto-summarize conversations into digestible format',
200
201 defaultConfig: {
202 windowSize: 50,
203 minMessages: 10,
204 includeParticipants: true,
205 includeTopics: true,
206 topicMinCount: 3,
207 maxTopics: 10,
208 maxKeyMessages: 5
209 },
210
211 // Process a batch of messages and produce a summary
212 // messages: [{ text, author, timestamp, type }]
213 // config: { windowSize, minMessages, ... }
214 // Returns: { summary, participants, topics, keyMessages, messageCount, timeRange }
215 process (messages, config) {
216 const cfg = { ...summarize.defaultConfig, ...(config || {}) }
217
218 if (!messages || !messages.length) {
219 return {
220 summary: '',
221 participants: [],
222 topics: [],
223 keyMessages: [],
224 messageCount: 0,
225 timeRange: { start: 0, end: 0, durationMs: 0 }
226 }
227 }
228
229 // Enforce minimum threshold
230 if (messages.length < cfg.minMessages) {
231 return {
232 summary: '',
233 participants: extractParticipants(messages),
234 topics: [],
235 keyMessages: [],
236 messageCount: messages.length,
237 timeRange: getTimeRange(messages),
238 belowThreshold: true
239 }
240 }
241
242 // Window: take only the last N messages
243 const window = messages.length > cfg.windowSize
244 ? messages.slice(messages.length - cfg.windowSize)
245 : messages
246
247 const participants = cfg.includeParticipants
248 ? extractParticipants(window)
249 : []
250
251 const topics = cfg.includeTopics
252 ? extractTopics(window, { minCount: cfg.topicMinCount, maxTopics: cfg.maxTopics })
253 : []
254
255 const keyMessages = extractKeyMessages(window, { maxKeyMessages: cfg.maxKeyMessages })
256 const timeRange = getTimeRange(window)
257 const summary = formatSummary(participants, topics, keyMessages, timeRange, window.length)
258
259 return {
260 summary,
261 participants,
262 topics,
263 keyMessages,
264 messageCount: window.length,
265 timeRange
266 }
267 },
268
269 // Generate a daemon script for the DaemonRuntime
270 // Collects messages in a buffer and summarizes at threshold
271 toDaemon (config, opts = {}) {
272 const targetRoom = opts.targetRoom
273
274 return {
275 trigger: {
276 type: 'message',
277 filter: {}
278 },
279 actions: [{
280 type: 'log',
281 message: 'Message buffered for summary'
282 }],
283 enabled: true,
284 _targetRoom: targetRoom,
285 _threshold: (config && config.windowSize) || 50
286 }
287 }
288}
289
290export {
291 summarize,
292 extractTopics,
293 extractKeyMessages,
294 extractParticipants,
295 getTimeRange,
296 formatSummary,
297 formatDuration,
298 tokenize,
299 STOP_WORDS
300}
hyper-daemon/index.js +5 -0
……@@ -0,0 +1,5 @@
1import { DaemonRuntime } from './lib/runtime.js'
2import { TriggerEngine } from './lib/triggers.js'
3import { ActionRunner } from './lib/actions.js'
4
5export { DaemonRuntime, TriggerEngine, ActionRunner }
hyper-daemon/lib/actions.js +103 -0
……@@ -0,0 +1,103 @@
1import http from 'http'
2import net from 'net'
3
4class ActionRunner {
5 constructor (opts = {}) {
6 this.keetHost = opts.keetHost || 'localhost'
7 this.keetPort = opts.keetPort || 3117
8 this._log = opts.onLog || console.log
9 }
10
11 async run (action, data) {
12 switch (action.type) {
13 case 'send-message':
14 return this._sendMessage(action, data)
15 case 'http-call':
16 return this._httpCall(action, data)
17 case 'log':
18 return this._log(action, data)
19 default:
20 throw new Error(`Unknown action type: ${action.type}`)
21 }
22 }
23
24 async _sendMessage (action, data) {
25 const roomId = action.roomId || data.roomId
26 let text = action.text || ''
27
28 // Template substitution
29 text = text
30 .replace(/\{author\}/g, data.author || data.name || '')
31 .replace(/\{text\}/g, data.text || '')
32 .replace(/\{channel\}/g, data.channel || '')
33 .replace(/\{timestamp\}/g, new Date().toISOString())
34
35 if (!roomId || !text) return { sent: false, reason: 'missing roomId or text' }
36
37 await this._keetRpc('send', { roomId, text })
38 return { sent: true, roomId, text }
39 }
40
41 async _httpCall (action) {
42 const url = new URL(action.url)
43 return new Promise((resolve, reject) => {
44 const req = http.request({
45 hostname: url.hostname,
46 port: url.port || 80,
47 path: url.pathname + url.search,
48 method: action.method || 'GET',
49 headers: action.headers || {}
50 }, (res) => {
51 let body = ''
52 res.on('data', (c) => { body += c.toString() })
53 res.on('end', () => resolve({ status: res.statusCode, body }))
54 })
55 req.on('error', reject)
56 req.setTimeout(10000, () => req.destroy(new Error('timeout')))
57 if (action.body) req.write(action.body)
58 req.end()
59 })
60 }
61
62 _logAction (action, data) {
63 const msg = action.message || JSON.stringify(data)
64 this._log(`[daemon] ${msg}`)
65 return { logged: true }
66 }
67
68 // Raw TCP for keet-daemon (duplicate Content-Length workaround)
69 _keetRpc (method, params) {
70 return new Promise((resolve, reject) => {
71 const socket = net.connect(this.keetPort, this.keetHost)
72 const payload = JSON.stringify({ method, params })
73
74 socket.on('connect', () => {
75 let req = `POST /rpc HTTP/1.1\r\n`
76 req += `Host: ${this.keetHost}:${this.keetPort}\r\n`
77 req += `Content-Type: application/json\r\n`
78 req += `Content-Length: ${Buffer.byteLength(payload)}\r\n`
79 req += `Connection: close\r\n\r\n`
80 req += payload
81 socket.write(req)
82 })
83
84 let data = ''
85 socket.on('data', (chunk) => { data += chunk.toString() })
86 socket.on('end', () => {
87 const idx = data.indexOf('\r\n\r\n')
88 if (idx === -1) return reject(new Error('No body'))
89 try {
90 const parsed = JSON.parse(data.slice(idx + 4))
91 if (!parsed.ok) reject(new Error(parsed.error || 'RPC error'))
92 else resolve(parsed.data)
93 } catch {
94 reject(new Error('Invalid response'))
95 }
96 })
97 socket.on('error', reject)
98 socket.setTimeout(10000, () => socket.destroy(new Error('timeout')))
99 })
100 }
101}
102
103export { ActionRunner }
hyper-daemon/lib/runtime.js +95 -0
……@@ -0,0 +1,95 @@
1import { TriggerEngine } from './triggers.js'
2import { ActionRunner } from './actions.js'
3import { DaemonStore } from './store.js'
4
5class DaemonRuntime {
6 constructor (storage, opts = {}) {
7 this._storage = storage
8 this._triggers = new TriggerEngine()
9 this._actions = new ActionRunner({
10 keetHost: opts.keetHost || 'localhost',
11 keetPort: opts.keetPort || 3117,
12 onLog: opts.onLog || console.log
13 })
14 this._store = new DaemonStore(storage)
15 this._ready = false
16 this._onLog = opts.onLog || console.log
17 }
18
19 async ready () {
20 if (this._ready) return
21 await this._store.ready()
22 this._ready = true
23 }
24
25 async load (id, script) {
26 await this.ready()
27
28 // Register trigger
29 this._triggers.register(id, {
30 type: script.trigger.type,
31 filter: script.trigger.filter || {},
32 enabled: script.enabled ?? true,
33 action: async (data) => {
34 const results = []
35 for (const action of script.actions) {
36 const result = await this._actions.run(action, data)
37 results.push(result)
38 }
39
40 // Log execution
41 await this._store.logEvent(id, {
42 timestamp: Date.now(),
43 trigger: script.trigger.type,
44 results
45 })
46
47 return results
48 }
49 })
50
51 // Save to persistent store
52 await this._store.saveScript(id, script)
53
54 this._onLog(`[daemon] Loaded script: ${id}`)
55 }
56
57 async unload (id) {
58 this._triggers.unregister(id)
59 await this._store.deleteScript(id)
60 this._onLog(`[daemon] Unloaded script: ${id}`)
61 }
62
63 async loadAll () {
64 await this.ready()
65 const scripts = await this._store.listScripts()
66 for (const script of scripts) {
67 await this.load(script.id, script)
68 }
69 return scripts.length
70 }
71
72 enable (id) { this._triggers.enable(id) }
73 disable (id) { this._triggers.disable(id) }
74
75 // Fire an event through the trigger engine
76 async fire (eventType, data) {
77 return this._triggers.fire(eventType, data)
78 }
79
80 list () {
81 return this._triggers.list()
82 }
83
84 async getLog (id, limit = 20) {
85 return this._store.getLog(id, limit)
86 }
87
88 async close () {
89 this._triggers.destroy()
90 await this._store.close()
91 this._ready = false
92 }
93}
94
95export { DaemonRuntime }
hyper-daemon/lib/store.js +89 -0
……@@ -0,0 +1,89 @@
1import Corestore from 'corestore'
2import Hyperbee from 'hyperbee'
3
4class DaemonStore {
5 constructor (storage) {
6 this._storage = storage
7 this._store = null
8 this._db = null
9 this._ready = false
10 }
11
12 async ready () {
13 if (this._ready) return
14
15 this._store = new Corestore(this._storage)
16 const core = this._store.get({ name: 'hyper-daemon' })
17 this._db = new Hyperbee(core, {
18 keyEncoding: 'utf-8',
19 valueEncoding: 'utf-8'
20 })
21 await this._db.ready()
22 this._ready = true
23 }
24
25 async saveScript (id, script) {
26 await this.ready()
27 await this._db.put(`script/${id}`, JSON.stringify(script))
28 }
29
30 async getScript (id) {
31 await this.ready()
32 const node = await this._db.get(`script/${id}`)
33 if (!node) return null
34 try {
35 return JSON.parse(node.value)
36 } catch {
37 return null
38 }
39 }
40
41 async deleteScript (id) {
42 await this.ready()
43 await this._db.del(`script/${id}`)
44 }
45
46 async listScripts () {
47 await this.ready()
48 const scripts = []
49 for await (const node of this._db.createReadStream({
50 gte: 'script/',
51 lt: 'script/\xff'
52 })) {
53 const id = node.key.slice('script/'.length)
54 try {
55 scripts.push({ id, ...JSON.parse(node.value) })
56 } catch {}
57 }
58 return scripts
59 }
60
61 async logEvent (id, event) {
62 await this.ready()
63 const key = `log/${id}/${Date.now()}`
64 await this._db.put(key, JSON.stringify(event))
65 }
66
67 async getLog (id, limit = 20) {
68 await this.ready()
69 const events = []
70 for await (const node of this._db.createReadStream({
71 gte: `log/${id}/`,
72 lt: `log/${id}/\xff`,
73 reverse: true,
74 limit
75 })) {
76 try {
77 events.push(JSON.parse(node.value))
78 } catch {}
79 }
80 return events
81 }
82
83 async close () {
84 if (this._store) await this._store.close()
85 this._ready = false
86 }
87}
88
89export { DaemonStore }
hyper-daemon/lib/triggers.js +128 -0
……@@ -0,0 +1,128 @@
1class TriggerEngine {
2 constructor () {
3 this._triggers = new Map()
4 this._timers = new Map()
5 }
6
7 register (id, trigger) {
8 this._triggers.set(id, {
9 id,
10 type: trigger.type,
11 filter: trigger.filter || {},
12 action: trigger.action,
13 enabled: trigger.enabled ?? true
14 })
15 }
16
17 unregister (id) {
18 this._triggers.delete(id)
19 if (this._timers.has(id)) {
20 clearInterval(this._timers.get(id))
21 this._timers.delete(id)
22 }
23 }
24
25 enable (id) {
26 const t = this._triggers.get(id)
27 if (t) t.enabled = true
28 }
29
30 disable (id) {
31 const t = this._triggers.get(id)
32 if (t) t.enabled = false
33 }
34
35 // Fire event and run matching trigger actions
36 async fire (eventType, data) {
37 const results = []
38
39 for (const [id, trigger] of this._triggers) {
40 if (!trigger.enabled) continue
41 if (trigger.type !== eventType) continue
42
43 if (this._matchesFilter(trigger.filter, data)) {
44 try {
45 const result = await trigger.action(data)
46 results.push({ id, ok: true, result })
47 } catch (err) {
48 results.push({ id, ok: false, error: err.message })
49 }
50 }
51 }
52
53 return results
54 }
55
56 _matchesFilter (filter, data) {
57 if (!filter || Object.keys(filter).length === 0) return true
58
59 // Channel filter
60 if (filter.channel && data.channel !== filter.channel) return false
61
62 // Author filter
63 if (filter.author && data.author !== filter.author) return false
64
65 // Content contains filter
66 if (filter.contains && !data.text?.includes(filter.contains)) return false
67
68 // Regex pattern filter
69 if (filter.pattern) {
70 const regex = new RegExp(filter.pattern)
71 if (!regex.test(data.text || '')) return false
72 }
73
74 // Tag filter
75 if (filter.tags) {
76 const tags = filter.tags
77 const text = (data.text || '').toLowerCase()
78 if (!tags.some(tag => text.includes(tag.toLowerCase()))) return false
79 }
80
81 return true
82 }
83
84 // Start a scheduled trigger (cron-like)
85 startSchedule (id, intervalMs, action) {
86 this.register(id, {
87 type: 'schedule',
88 action
89 })
90
91 const timer = setInterval(async () => {
92 const trigger = this._triggers.get(id)
93 if (!trigger || !trigger.enabled) return
94 try {
95 await trigger.action({ timestamp: Date.now(), triggerId: id })
96 } catch {}
97 }, intervalMs)
98
99 this._timers.set(id, timer)
100 }
101
102 list () {
103 const result = []
104 for (const [id, t] of this._triggers) {
105 result.push({
106 id,
107 type: t.type,
108 enabled: t.enabled,
109 hasFilter: Object.keys(t.filter).length > 0
110 })
111 }
112 return result
113 }
114
115 get count () {
116 return this._triggers.size
117 }
118
119 destroy () {
120 for (const timer of this._timers.values()) {
121 clearInterval(timer)
122 }
123 this._timers.clear()
124 this._triggers.clear()
125 }
126}
127
128export { TriggerEngine }
hyper-daemon/package-lock.json +1443 -0
……@@ -0,0 +1,1443 @@
1{
2 "name": "hyper-daemon",
3 "version": "1.0.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "hyper-daemon",
9 "version": "1.0.0",
10 "license": "MIT",
11 "dependencies": {
12 "b4a": "^1.0.0",
13 "corestore": "^6.0.0",
14 "hyperbee": "^2.0.0",
15 "hypercore": "^10.0.0"
16 },
17 "devDependencies": {
18 "brittle": "^3.0.0"
19 }
20 },
21 "node_modules/@hyperswarm/secret-stream": {
22 "version": "6.9.1",
23 "resolved": "https://registry.npmjs.org/@hyperswarm/secret-stream/-/secret-stream-6.9.1.tgz",
24 "integrity": "sha512-xb0S5y3YJwBakD77JOGBHlBxdp63mHClZoXBYoLv+9wH8e054ESKlmQptWqjJK5dv5VMUIVYOJB4MaOpB0JdGw==",
25 "license": "Apache-2.0",
26 "dependencies": {
27 "b4a": "^1.1.0",
28 "hypercore-crypto": "^3.3.1",
29 "noise-curve-ed": "^2.0.1",
30 "noise-handshake": "^4.0.0",
31 "sodium-secretstream": "^1.1.0",
32 "sodium-universal": "^5.0.0",
33 "streamx": "^2.14.0",
34 "timeout-refresh": "^2.0.0",
35 "unslab": "^1.3.0"
36 }
37 },
38 "node_modules/@hyperswarm/secret-stream/node_modules/sodium-native": {
39 "version": "5.0.10",
40 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
41 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
42 "license": "MIT",
43 "dependencies": {
44 "require-addon": "^1.1.0",
45 "which-runtime": "^1.2.1"
46 },
47 "engines": {
48 "bare": ">=1.16.0"
49 }
50 },
51 "node_modules/@hyperswarm/secret-stream/node_modules/sodium-universal": {
52 "version": "5.0.1",
53 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
54 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
55 "license": "MIT",
56 "dependencies": {
57 "sodium-native": "^5.0.1"
58 },
59 "peerDependencies": {
60 "sodium-javascript": "~0.8.0"
61 },
62 "peerDependenciesMeta": {
63 "sodium-javascript": {
64 "optional": true
65 }
66 }
67 },
68 "node_modules/@jridgewell/resolve-uri": {
69 "version": "3.1.2",
70 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
71 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
72 "dev": true,
73 "license": "MIT",
74 "engines": {
75 "node": ">=6.0.0"
76 }
77 },
78 "node_modules/@jridgewell/sourcemap-codec": {
79 "version": "1.5.5",
80 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
81 "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
82 "dev": true,
83 "license": "MIT"
84 },
85 "node_modules/@jridgewell/trace-mapping": {
86 "version": "0.3.31",
87 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
88 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
89 "dev": true,
90 "license": "MIT",
91 "dependencies": {
92 "@jridgewell/resolve-uri": "^3.1.0",
93 "@jridgewell/sourcemap-codec": "^1.4.14"
94 }
95 },
96 "node_modules/@types/istanbul-lib-coverage": {
97 "version": "2.0.6",
98 "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
99 "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
100 "dev": true,
101 "license": "MIT"
102 },
103 "node_modules/b4a": {
104 "version": "1.7.4",
105 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.4.tgz",
106 "integrity": "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug==",
107 "license": "Apache-2.0",
108 "peerDependencies": {
109 "react-native-b4a": "*"
110 },
111 "peerDependenciesMeta": {
112 "react-native-b4a": {
113 "optional": true
114 }
115 }
116 },
117 "node_modules/bare-addon-resolve": {
118 "version": "1.10.0",
119 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
120 "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
121 "license": "Apache-2.0",
122 "dependencies": {
123 "bare-module-resolve": "^1.10.0",
124 "bare-semver": "^1.0.0"
125 },
126 "peerDependencies": {
127 "bare-url": "*"
128 },
129 "peerDependenciesMeta": {
130 "bare-url": {
131 "optional": true
132 }
133 }
134 },
135 "node_modules/bare-ansi-escapes": {
136 "version": "2.2.3",
137 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
138 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
139 "dev": true,
140 "license": "Apache-2.0",
141 "dependencies": {
142 "bare-stream": "^2.6.5"
143 },
144 "peerDependencies": {
145 "bare-buffer": "*"
146 },
147 "peerDependenciesMeta": {
148 "bare-buffer": {
149 "optional": true
150 }
151 }
152 },
153 "node_modules/bare-assert": {
154 "version": "1.2.0",
155 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
156 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
157 "dev": true,
158 "license": "Apache-2.0",
159 "dependencies": {
160 "bare-inspect": "^3.1.2"
161 }
162 },
163 "node_modules/bare-bundle": {
164 "version": "1.10.0",
165 "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
166 "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
167 "dev": true,
168 "license": "Apache-2.0",
169 "peerDependencies": {
170 "bare-buffer": "*",
171 "bare-url": "*"
172 },
173 "peerDependenciesMeta": {
174 "bare-buffer": {
175 "optional": true
176 },
177 "bare-url": {
178 "optional": true
179 }
180 }
181 },
182 "node_modules/bare-cov": {
183 "version": "1.2.1",
184 "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz",
185 "integrity": "sha512-SHNqb/WG9udUEj8m5qGSehoUQ9o0Xzv8nrf4HIdmG+KNU/XdhPLhTP5VPrHnSAMAXhAzEB4Q+2IYZ0x9qPzN1g==",
186 "dev": true,
187 "license": "Apache-2.0",
188 "dependencies": {
189 "bare-fs": "^4.1.2",
190 "bare-inspector": "^4.0.1",
191 "bare-path": "^3.0.0",
192 "bare-process": "^4.2.1",
193 "bare-url": "^2.1.5",
194 "bare-v8-to-istanbul": "^1.0.2",
195 "picomatch": "^4.0.2",
196 "which-runtime": "^1.2.1"
197 }
198 },
199 "node_modules/bare-crypto": {
200 "version": "1.13.0",
201 "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.0.tgz",
202 "integrity": "sha512-RQl13yD+YTACWUZHMMck0C6LNjXgGgRyhVC557ag0xIrkxXf4IQwpnc8WOB58f4k5kSzKhIjy05oW3HUBrFpSQ==",
203 "dev": true,
204 "license": "Apache-2.0",
205 "dependencies": {
206 "bare-stream": "^2.6.3"
207 },
208 "peerDependencies": {
209 "bare-buffer": "*"
210 },
211 "peerDependenciesMeta": {
212 "bare-buffer": {
213 "optional": true
214 }
215 }
216 },
217 "node_modules/bare-debug-log": {
218 "version": "2.0.0",
219 "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
220 "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
221 "dev": true,
222 "license": "Apache-2.0",
223 "dependencies": {
224 "bare-os": "^3.0.1"
225 }
226 },
227 "node_modules/bare-dns": {
228 "version": "2.1.4",
229 "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
230 "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==",
231 "dev": true,
232 "license": "Apache-2.0",
233 "engines": {
234 "bare": ">=1.7.0"
235 }
236 },
237 "node_modules/bare-encoding": {
238 "version": "1.0.3",
239 "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
240 "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
241 "dev": true,
242 "license": "Apache-2.0",
243 "peerDependencies": {
244 "bare-buffer": "*"
245 },
246 "peerDependenciesMeta": {
247 "bare-buffer": {
248 "optional": true
249 }
250 }
251 },
252 "node_modules/bare-env": {
253 "version": "3.0.0",
254 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
255 "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
256 "dev": true,
257 "license": "Apache-2.0",
258 "dependencies": {
259 "bare-os": "^3.0.1"
260 }
261 },
262 "node_modules/bare-events": {
263 "version": "2.8.2",
264 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
265 "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
266 "license": "Apache-2.0",
267 "peerDependencies": {
268 "bare-abort-controller": "*"
269 },
270 "peerDependenciesMeta": {
271 "bare-abort-controller": {
272 "optional": true
273 }
274 }
275 },
276 "node_modules/bare-format": {
277 "version": "1.0.1",
278 "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.1.tgz",
279 "integrity": "sha512-1oS+LZrWK6tnYnvNSHDGljc2MPunRxwhpFriuCgzNF+oklrnwmBKD91tS0yt+jpl2j3UgcSDzBIMiVTvLs9A8w==",
280 "dev": true,
281 "license": "Apache-2.0",
282 "dependencies": {
283 "bare-inspect": "^3.0.0"
284 }
285 },
286 "node_modules/bare-fs": {
287 "version": "4.5.4",
288 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz",
289 "integrity": "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==",
290 "license": "Apache-2.0",
291 "dependencies": {
292 "bare-events": "^2.5.4",
293 "bare-path": "^3.0.0",
294 "bare-stream": "^2.6.4",
295 "bare-url": "^2.2.2",
296 "fast-fifo": "^1.3.2"
297 },
298 "engines": {
299 "bare": ">=1.16.0"
300 },
301 "peerDependencies": {
302 "bare-buffer": "*"
303 },
304 "peerDependenciesMeta": {
305 "bare-buffer": {
306 "optional": true
307 }
308 }
309 },
310 "node_modules/bare-hrtime": {
311 "version": "2.1.1",
312 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
313 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
314 "dev": true,
315 "license": "Apache-2.0"
316 },
317 "node_modules/bare-http-parser": {
318 "version": "1.0.1",
319 "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.0.1.tgz",
320 "integrity": "sha512-A3LTDTcELcmNJ3g5liIaS038v/BQxOhA9cjhBESn7eoV7QCuMoIRBKLDadDe08flxyLbxI2f+1l2MZ/5+HnKPA==",
321 "dev": true,
322 "license": "Apache-2.0"
323 },
324 "node_modules/bare-http1": {
325 "version": "4.2.3",
326 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.2.3.tgz",
327 "integrity": "sha512-zyvHF3xzvBJNpuw0QydE9etTn2AyC/TRVDWTo8Ed3LQEEGaFOkv6KHhaJPlf4eWEeTekk41Fowie+fTsXoNxkA==",
328 "dev": true,
329 "license": "Apache-2.0",
330 "dependencies": {
331 "bare-events": "^2.6.0",
332 "bare-http-parser": "^1.0.0",
333 "bare-stream": "^2.3.0",
334 "bare-tcp": "^2.2.0"
335 },
336 "peerDependencies": {
337 "bare-buffer": "*",
338 "bare-url": "*"
339 },
340 "peerDependenciesMeta": {
341 "bare-buffer": {
342 "optional": true
343 },
344 "bare-url": {
345 "optional": true
346 }
347 }
348 },
349 "node_modules/bare-https": {
350 "version": "2.1.2",
351 "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.2.tgz",
352 "integrity": "sha512-Q+TTydUDsuKQJvh8dX2dvOXCR9fM3xR5TBmKaFrs5p7Lj7XbKX7v4vIUJ36H0SXg2xCOQxXKIbjwrLg5tfJNYg==",
353 "dev": true,
354 "license": "Apache-2.0",
355 "dependencies": {
356 "bare-http1": "^4.0.0",
357 "bare-tcp": "^2.2.0",
358 "bare-tls": "^2.0.0"
359 }
360 },
361 "node_modules/bare-inspect": {
362 "version": "3.1.4",
363 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
364 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
365 "dev": true,
366 "license": "Apache-2.0",
367 "dependencies": {
368 "bare-ansi-escapes": "^2.1.0",
369 "bare-type": "^1.0.0"
370 },
371 "engines": {
372 "bare": ">=1.18.0"
373 }
374 },
375 "node_modules/bare-inspector": {
376 "version": "4.0.3",
377 "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-4.0.3.tgz",
378 "integrity": "sha512-yYk6+7tTVKJKHOja5+QBn989ETv8oVsmwoH0nCM+QYC6w5MC03GC4aQNK+lipAZSz1EaAj42WZ8Wewjpy0L9TA==",
379 "dev": true,
380 "license": "Apache-2.0",
381 "dependencies": {
382 "bare-events": "^2.1.0",
383 "bare-http1": "^4.0.0",
384 "bare-stream": "^2.0.0",
385 "bare-url": "^2.0.0",
386 "bare-ws": "^2.0.0"
387 },
388 "engines": {
389 "bare": ">=1.2.0"
390 },
391 "peerDependencies": {
392 "bare-tcp": "*"
393 },
394 "peerDependenciesMeta": {
395 "bare-tcp": {
396 "optional": true
397 }
398 }
399 },
400 "node_modules/bare-module": {
401 "version": "6.1.3",
402 "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz",
403 "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==",
404 "dev": true,
405 "license": "Apache-2.0",
406 "dependencies": {
407 "bare-bundle": "^1.3.0",
408 "bare-module-lexer": "^1.0.0",
409 "bare-module-resolve": "^1.8.0",
410 "bare-path": "^3.0.0",
411 "bare-url": "^2.0.1"
412 },
413 "engines": {
414 "bare": ">=1.23.0"
415 },
416 "peerDependencies": {
417 "bare-buffer": "*"
418 },
419 "peerDependenciesMeta": {
420 "bare-buffer": {
421 "optional": true
422 }
423 }
424 },
425 "node_modules/bare-module-lexer": {
426 "version": "1.4.7",
427 "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
428 "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
429 "dev": true,
430 "license": "Apache-2.0",
431 "dependencies": {
432 "require-addon": "^1.0.2"
433 },
434 "peerDependencies": {
435 "bare-buffer": "*"
436 },
437 "peerDependenciesMeta": {
438 "bare-buffer": {
439 "optional": true
440 }
441 }
442 },
443 "node_modules/bare-module-resolve": {
444 "version": "1.12.1",
445 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz",
446 "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==",
447 "license": "Apache-2.0",
448 "dependencies": {
449 "bare-semver": "^1.0.0"
450 },
451 "peerDependencies": {
452 "bare-url": "*"
453 },
454 "peerDependenciesMeta": {
455 "bare-url": {
456 "optional": true
457 }
458 }
459 },
460 "node_modules/bare-net": {
461 "version": "2.2.0",
462 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.2.0.tgz",
463 "integrity": "sha512-UF7cAbHsGE+H6uEqWF5IULBow1x58chZz4g3ALgHtv7wZsFcCbRDt0JKWEumf5Oma3QWS1Q6aLi0Rpll8RElMg==",
464 "dev": true,
465 "license": "Apache-2.0",
466 "dependencies": {
467 "bare-events": "^2.2.2",
468 "bare-pipe": "^4.0.0",
469 "bare-stream": "^2.0.0",
470 "bare-tcp": "^2.0.0"
471 }
472 },
473 "node_modules/bare-os": {
474 "version": "3.6.2",
475 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
476 "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
477 "license": "Apache-2.0",
478 "engines": {
479 "bare": ">=1.14.0"
480 }
481 },
482 "node_modules/bare-path": {
483 "version": "3.0.0",
484 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
485 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
486 "license": "Apache-2.0",
487 "dependencies": {
488 "bare-os": "^3.0.1"
489 }
490 },
491 "node_modules/bare-pipe": {
492 "version": "4.1.3",
493 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.3.tgz",
494 "integrity": "sha512-DqQsx93rAzre6yJ9T6l/Vgh+X+bntkVMB1X5ggtXjXtqtMmF2Y2RVlCzxxy/09R6yeR9FSWBEUIaMYJL1/5VDA==",
495 "dev": true,
496 "license": "Apache-2.0",
497 "dependencies": {
498 "bare-events": "^2.0.0",
499 "bare-stream": "^2.0.0"
500 },
501 "engines": {
502 "bare": ">=1.16.0"
503 }
504 },
505 "node_modules/bare-process": {
506 "version": "4.2.2",
507 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.2.2.tgz",
508 "integrity": "sha512-ikzEw+HGLB+2lS/WLVEsqi8BXBwzleG3n7DYI0v6/YNklvNabOIGlLd1dof+7Jw5ob4jsBRPtyflweVahY7rFA==",
509 "dev": true,
510 "license": "Apache-2.0",
511 "dependencies": {
512 "bare-env": "^3.0.0",
513 "bare-events": "^2.3.1",
514 "bare-hrtime": "^2.0.0",
515 "bare-os": "^3.5.0",
516 "bare-pipe": "^4.0.0",
517 "bare-signals": "^4.0.0",
518 "bare-tty": "^5.0.0"
519 }
520 },
521 "node_modules/bare-semver": {
522 "version": "1.0.2",
523 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz",
524 "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==",
525 "license": "Apache-2.0"
526 },
527 "node_modules/bare-signals": {
528 "version": "4.2.0",
529 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz",
530 "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==",
531 "dev": true,
532 "license": "Apache-2.0",
533 "dependencies": {
534 "bare-events": "^2.5.3",
535 "bare-os": "^3.3.1"
536 },
537 "engines": {
538 "bare": ">=1.7.0"
539 }
540 },
541 "node_modules/bare-stream": {
542 "version": "2.7.0",
543 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
544 "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
545 "license": "Apache-2.0",
546 "dependencies": {
547 "streamx": "^2.21.0"
548 },
549 "peerDependencies": {
550 "bare-buffer": "*",
551 "bare-events": "*"
552 },
553 "peerDependenciesMeta": {
554 "bare-buffer": {
555 "optional": true
556 },
557 "bare-events": {
558 "optional": true
559 }
560 }
561 },
562 "node_modules/bare-subprocess": {
563 "version": "5.2.2",
564 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.2.tgz",
565 "integrity": "sha512-L6oXgQ1aWs25RtG5Ky0bDD06p3RAcVVrDDMWb1DfXpHtyEWxamcyIWUbSykxMTWrpLlmSj6ytbb6yoKehGFfmw==",
566 "dev": true,
567 "license": "Apache-2.0",
568 "dependencies": {
569 "bare-env": "^3.0.0",
570 "bare-events": "^2.5.4",
571 "bare-os": "^3.0.1",
572 "bare-pipe": "^4.0.0",
573 "bare-url": "^2.2.2"
574 },
575 "engines": {
576 "bare": ">=1.7.0"
577 },
578 "peerDependencies": {
579 "bare-buffer": "*"
580 },
581 "peerDependenciesMeta": {
582 "bare-buffer": {
583 "optional": true
584 }
585 }
586 },
587 "node_modules/bare-tcp": {
588 "version": "2.2.4",
589 "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.4.tgz",
590 "integrity": "sha512-vGjfMkyhS1lxp+Y4+gXRDR1ijZMK2cKx0RBwtYKUuZbD90ARCoM6bAiTa1HBztumCIC+NXb8eXdMFXyuYwlPyw==",
591 "dev": true,
592 "license": "Apache-2.0",
593 "dependencies": {
594 "bare-dns": "^2.0.4",
595 "bare-events": "^2.5.4",
596 "bare-stream": "^2.6.4"
597 },
598 "engines": {
599 "bare": ">=1.16.0"
600 }
601 },
602 "node_modules/bare-tls": {
603 "version": "2.1.7",
604 "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.1.7.tgz",
605 "integrity": "sha512-h6wcNXQdBeTX7fed9tjPp0/9cA/QfcBTv3ItgjnbUk4rWAU8bEFalZCZnUDdCK/t9zrNfJ+yvcPx4D/1Y6biyA==",
606 "dev": true,
607 "license": "Apache-2.0",
608 "dependencies": {
609 "bare-net": "^2.0.1",
610 "bare-stream": "^2.6.4"
611 },
612 "engines": {
613 "bare": ">=1.7.0"
614 }
615 },
616 "node_modules/bare-tty": {
617 "version": "5.0.3",
618 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.0.3.tgz",
619 "integrity": "sha512-jW24RBWRZOMHuSEWC9mh8wUKO5WUNl0UWUTNPn3yZ8qkOqwa8vaV2dR3a+BvblONPk7V35wuvK9P8508+judAg==",
620 "dev": true,
621 "license": "Apache-2.0",
622 "dependencies": {
623 "bare-events": "^2.2.0",
624 "bare-signals": "^4.0.0",
625 "bare-stream": "^2.0.0"
626 },
627 "engines": {
628 "bare": ">=1.16.0"
629 }
630 },
631 "node_modules/bare-type": {
632 "version": "1.1.0",
633 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
634 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
635 "dev": true,
636 "license": "Apache-2.0",
637 "engines": {
638 "bare": ">=1.2.0"
639 }
640 },
641 "node_modules/bare-url": {
642 "version": "2.3.2",
643 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
644 "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
645 "license": "Apache-2.0",
646 "peer": true,
647 "dependencies": {
648 "bare-path": "^3.0.0"
649 }
650 },
651 "node_modules/bare-utils": {
652 "version": "1.5.1",
653 "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.5.1.tgz",
654 "integrity": "sha512-mxCkFvmDU3mlD/mb+pT64kKXOsx2KMsWLQbngN1LB+NOXfhfnRnyvpy3VZc6m7gzQxe57Bsi+aTCBqA4/S3elQ==",
655 "dev": true,
656 "license": "Apache-2.0",
657 "dependencies": {
658 "bare-debug-log": "^2.0.0",
659 "bare-encoding": "^1.0.0",
660 "bare-format": "^1.0.0",
661 "bare-inspect": "^3.0.0",
662 "bare-type": "^1.0.6"
663 }
664 },
665 "node_modules/bare-v8-to-istanbul": {
666 "version": "1.0.2",
667 "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
668 "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==",
669 "dev": true,
670 "license": "Apache-2.0",
671 "dependencies": {
672 "bare-assert": "^1.0.2",
673 "bare-fs": "^4.1.2",
674 "bare-module": "^6.1.2",
675 "bare-path": "^3.0.0",
676 "bare-process": "^4.2.0",
677 "bare-url": "^2.1.5",
678 "bare-utils": "^1.2.0",
679 "v8-to-istanbul": "^9.3.0",
680 "which-runtime": "^1.2.1"
681 }
682 },
683 "node_modules/bare-ws": {
684 "version": "2.0.4",
685 "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.0.4.tgz",
686 "integrity": "sha512-SQMXzBYna9dRj57Dz1/ag+VWHCRXbfCjMHgyfM2F2lhkVLzMjnVSZP72aVeFWPFqe494Rd70Kzhe2JElGwFlJQ==",
687 "dev": true,
688 "license": "Apache-2.0",
689 "dependencies": {
690 "bare-crypto": "^1.2.0",
691 "bare-events": "^2.3.1",
692 "bare-http1": "^4.0.0",
693 "bare-https": "^2.0.0",
694 "bare-stream": "^2.1.2"
695 },
696 "peerDependencies": {
697 "bare-buffer": "*",
698 "bare-url": "*"
699 },
700 "peerDependenciesMeta": {
701 "bare-buffer": {
702 "optional": true
703 },
704 "bare-url": {
705 "optional": true
706 }
707 }
708 },
709 "node_modules/big-sparse-array": {
710 "version": "1.0.3",
711 "resolved": "https://registry.npmjs.org/big-sparse-array/-/big-sparse-array-1.0.3.tgz",
712 "integrity": "sha512-6RjV/3mSZORlMdpUaQ6rUSpG637cZm0//E54YYGtQg1c1O+AbZP8UTdJ/TchsDZcTVLmyWZcseBfp2HBeXUXOQ==",
713 "license": "MIT"
714 },
715 "node_modules/brittle": {
716 "version": "3.19.1",
717 "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz",
718 "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==",
719 "dev": true,
720 "license": "Apache-2.0",
721 "dependencies": {
722 "b4a": "^1.6.0",
723 "bare-assert": "^1.0.2",
724 "bare-cov": "^1.1.0",
725 "bare-fs": "^4.1.6",
726 "bare-os": "^3.6.1",
727 "bare-path": "^3.0.0",
728 "bare-process": "^4.2.1",
729 "bare-subprocess": "^5.0.0",
730 "bare-url": "^2.1.6",
731 "error-stack-parser": "^2.1.4",
732 "globbie": "^1.0.2",
733 "paparam": "^1.6.2",
734 "same-object": "^1.0.2",
735 "test-tmp": "^1.4.0",
736 "tmatch": "^5.0.0"
737 },
738 "bin": {
739 "brittle": "bin/node.js",
740 "brittle-bare": "bin/bare.js",
741 "brittle-node": "bin/node.js",
742 "brittle-pear": "bin/pear.js"
743 }
744 },
745 "node_modules/codecs": {
746 "version": "3.1.0",
747 "resolved": "https://registry.npmjs.org/codecs/-/codecs-3.1.0.tgz",
748 "integrity": "sha512-Dqx8NwvBvnMeuPQdVKy/XEF71igjR5apxBvCGeV0pP1tXadOiaLvDTXt7xh+/5wI1ASB195mXQGJbw3Ml4YDWQ==",
749 "license": "MIT",
750 "dependencies": {
751 "b4a": "^1.6.3"
752 }
753 },
754 "node_modules/compact-encoding": {
755 "version": "2.18.0",
756 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-2.18.0.tgz",
757 "integrity": "sha512-goACAOlhMI2xo5jGOMUDfOLnGdRE1jGfyZ+zie8N5114nHrbPIqf6GLUtzbLof6DSyrERlYRm3EcBplte5LcQw==",
758 "license": "Apache-2.0",
759 "dependencies": {
760 "b4a": "^1.3.0"
761 }
762 },
763 "node_modules/convert-source-map": {
764 "version": "2.0.0",
765 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
766 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
767 "dev": true,
768 "license": "MIT"
769 },
770 "node_modules/corestore": {
771 "version": "6.18.4",
772 "resolved": "https://registry.npmjs.org/corestore/-/corestore-6.18.4.tgz",
773 "integrity": "sha512-KL1cOUyj3DFRNzeVRekGPRiGN4sIfGo7BsncSJQQBT3Rh2y1nQ32MOR9X5DlOMlWIT4txwqBzhzam6+JXtwFVw==",
774 "license": "MIT",
775 "dependencies": {
776 "b4a": "^1.6.4",
777 "hypercore": "^10.37.10",
778 "hypercore-crypto": "^3.4.0",
779 "hypercore-id-encoding": "^1.2.0",
780 "read-write-mutexify": "^2.1.0",
781 "ready-resource": "^1.0.0",
782 "safety-catch": "^1.0.1",
783 "sodium-universal": "^4.0.0",
784 "xache": "^1.1.0"
785 }
786 },
787 "node_modules/crc-native": {
788 "version": "1.1.8",
789 "resolved": "https://registry.npmjs.org/crc-native/-/crc-native-1.1.8.tgz",
790 "integrity": "sha512-R/RYbD8lhG5M1k0iLX49tJdwHM0ipevJPpZw2Wk7b4EShyvx2P0uaq79xyRpcknjiVGiUfjFA8Gyy0mOlhABvw==",
791 "license": "Apache-2.0",
792 "optional": true,
793 "dependencies": {
794 "require-addon": "^1.1.0"
795 }
796 },
797 "node_modules/crc-universal": {
798 "version": "1.0.4",
799 "resolved": "https://registry.npmjs.org/crc-universal/-/crc-universal-1.0.4.tgz",
800 "integrity": "sha512-CE9xWEI6Gd5V0Bdmj5NoWH3d7+EIe4zUpy1sv6uvKYznzsDP1vhiWsTBmL6q9IrH2P6RRshp+8AkhR4CMtY5Hg==",
801 "license": "ISC",
802 "optionalDependencies": {
803 "crc-native": "^1.0.3"
804 }
805 },
806 "node_modules/debounceify": {
807 "version": "1.1.0",
808 "resolved": "https://registry.npmjs.org/debounceify/-/debounceify-1.1.0.tgz",
809 "integrity": "sha512-eKuHDVfJVg+u/0nPy8P+fhnLgbyuTgVxuCRrS/R7EpDSMMkBDgSes41MJtSAY1F1hcqfHz3Zy/qpqHHIp/EhdA==",
810 "license": "MIT"
811 },
812 "node_modules/error-stack-parser": {
813 "version": "2.1.4",
814 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
815 "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
816 "dev": true,
817 "license": "MIT",
818 "dependencies": {
819 "stackframe": "^1.3.4"
820 }
821 },
822 "node_modules/events-universal": {
823 "version": "1.0.1",
824 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
825 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
826 "license": "Apache-2.0",
827 "dependencies": {
828 "bare-events": "^2.7.0"
829 }
830 },
831 "node_modules/fast-fifo": {
832 "version": "1.3.2",
833 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
834 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
835 "license": "MIT"
836 },
837 "node_modules/flat-tree": {
838 "version": "1.13.0",
839 "resolved": "https://registry.npmjs.org/flat-tree/-/flat-tree-1.13.0.tgz",
840 "integrity": "sha512-fT3HIuCPwHhFgJ20QYzDHgUG0zMmFg5cHvFiFo5h+QMSJ28TihsEVY0f8HGliuO+pOzmvjMx1odToeaEWkTnyQ==",
841 "license": "MIT"
842 },
843 "node_modules/fs-native-extensions": {
844 "version": "1.4.5",
845 "resolved": "https://registry.npmjs.org/fs-native-extensions/-/fs-native-extensions-1.4.5.tgz",
846 "integrity": "sha512-ekV0T//iDm4AvhOcuPaHpxub4DI7HvY5ucLJVDvi7T2J+NZkQ9S6MuvgP0yeQvoqNUaAGyLjVYb1905BF9bpmg==",
847 "license": "Apache-2.0",
848 "optional": true,
849 "dependencies": {
850 "require-addon": "^1.1.0",
851 "which-runtime": "^1.2.0"
852 }
853 },
854 "node_modules/globbie": {
855 "version": "1.0.3",
856 "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz",
857 "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==",
858 "dev": true,
859 "license": "Apache-2.0",
860 "dependencies": {
861 "bare-fs": "^4.1.2",
862 "bare-path": "^3.0.0",
863 "bare-process": "^4.2.1",
864 "picomatch": "^4.0.2"
865 }
866 },
867 "node_modules/hyperbee": {
868 "version": "2.27.3",
869 "resolved": "https://registry.npmjs.org/hyperbee/-/hyperbee-2.27.3.tgz",
870 "integrity": "sha512-PXURH2U4juUZyJRKHTrY5z1zX851pmI1Q0jfv5F/hCIErDt/ND8jOZuxc3hfOLM9f0W3qJEDTMlV5AJBkVPy8w==",
871 "license": "MIT",
872 "dependencies": {
873 "b4a": "^1.6.0",
874 "codecs": "^3.0.0",
875 "debounceify": "^1.0.0",
876 "hypercore-errors": "^1.0.0",
877 "mutexify": "^1.4.0",
878 "protocol-buffers-encodings": "^1.2.0",
879 "rache": "^1.0.0",
880 "ready-resource": "^1.0.0",
881 "resolve-reject-promise": "^1.1.0",
882 "safety-catch": "^1.0.2",
883 "streamx": "^2.12.4",
884 "unslab": "^1.2.0"
885 }
886 },
887 "node_modules/hypercore": {
888 "version": "10.38.2",
889 "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-10.38.2.tgz",
890 "integrity": "sha512-2vx42g2RKdP/o2XzlxKfvgEaZ3+h/B855gCR8iscUDnxUoOZej7vXZ2buex0sweDUEW5aEfMxFR6o5hncio3Nw==",
891 "license": "MIT",
892 "dependencies": {
893 "@hyperswarm/secret-stream": "^6.0.0",
894 "b4a": "^1.1.0",
895 "bare-events": "^2.2.0",
896 "big-sparse-array": "^1.0.3",
897 "compact-encoding": "^2.11.0",
898 "crc-universal": "^1.0.2",
899 "fast-fifo": "^1.3.0",
900 "flat-tree": "^1.9.0",
901 "hypercore-crypto": "^3.2.1",
902 "hypercore-errors": "^1.2.0",
903 "hypercore-id-encoding": "^1.2.0",
904 "is-options": "^1.0.1",
905 "protomux": "^3.5.0",
906 "quickbit-universal": "^2.2.0",
907 "random-access-file": "^4.0.0",
908 "random-array-iterator": "^1.0.0",
909 "safety-catch": "^1.0.1",
910 "sodium-universal": "^4.0.0",
911 "streamx": "^2.12.4",
912 "unslab": "^1.3.0",
913 "xache": "^1.1.0",
914 "z32": "^1.0.0"
915 }
916 },
917 "node_modules/hypercore-crypto": {
918 "version": "3.6.1",
919 "resolved": "https://registry.npmjs.org/hypercore-crypto/-/hypercore-crypto-3.6.1.tgz",
920 "integrity": "sha512-ltIz2uDwy9pO/ZGTvqcjzyBkvt6O4cVm4r/nNxh0GFs/RbQtqP/i4wCvLEdmU7ptgtnw7fI67WYD1aHPuv4OVA==",
921 "license": "MIT",
922 "dependencies": {
923 "b4a": "^1.6.6",
924 "compact-encoding": "^2.15.0",
925 "sodium-universal": "^5.0.0"
926 }
927 },
928 "node_modules/hypercore-crypto/node_modules/sodium-native": {
929 "version": "5.0.10",
930 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
931 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
932 "license": "MIT",
933 "dependencies": {
934 "require-addon": "^1.1.0",
935 "which-runtime": "^1.2.1"
936 },
937 "engines": {
938 "bare": ">=1.16.0"
939 }
940 },
941 "node_modules/hypercore-crypto/node_modules/sodium-universal": {
942 "version": "5.0.1",
943 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
944 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
945 "license": "MIT",
946 "dependencies": {
947 "sodium-native": "^5.0.1"
948 },
949 "peerDependencies": {
950 "sodium-javascript": "~0.8.0"
951 },
952 "peerDependenciesMeta": {
953 "sodium-javascript": {
954 "optional": true
955 }
956 }
957 },
958 "node_modules/hypercore-errors": {
959 "version": "1.5.0",
960 "resolved": "https://registry.npmjs.org/hypercore-errors/-/hypercore-errors-1.5.0.tgz",
961 "integrity": "sha512-5KQ/SuDxsvet+7qWA35Ay6zdD9WyAHQoyWHGcPUTbmJBd300gvNIJoi3oma7kp4TTCSzii6qYumNZe/s0j/saQ==",
962 "license": "Apache-2.0",
963 "dependencies": {
964 "hypercore-id-encoding": "^1.3.0"
965 }
966 },
967 "node_modules/hypercore-id-encoding": {
968 "version": "1.3.0",
969 "resolved": "https://registry.npmjs.org/hypercore-id-encoding/-/hypercore-id-encoding-1.3.0.tgz",
970 "integrity": "sha512-W6sHdGo5h7LXEsoWfKf/KfuROZmZRQDlGqJF2EPHW+noCK66Vvr0+zE6cL0vqQi18s0kQPeN7Sq3QyR0Ytc2VQ==",
971 "license": "Apache-2.0",
972 "dependencies": {
973 "b4a": "^1.5.3",
974 "z32": "^1.0.0"
975 }
976 },
977 "node_modules/is-options": {
978 "version": "1.0.2",
979 "resolved": "https://registry.npmjs.org/is-options/-/is-options-1.0.2.tgz",
980 "integrity": "sha512-u+Ai74c8Q74aS8BuHwPdI1jptGOT1FQXgCq8/zv0xRuE+wRgSMEJLj8lVO8Zp9BeGb29BXY6AsNPinfqjkr7Fg==",
981 "license": "MIT",
982 "dependencies": {
983 "b4a": "^1.1.1"
984 }
985 },
986 "node_modules/mutexify": {
987 "version": "1.4.0",
988 "resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz",
989 "integrity": "sha512-pbYSsOrSB/AKN5h/WzzLRMFgZhClWccf2XIB4RSMC8JbquiB0e0/SH5AIfdQMdyHmYtv4seU7yV/TvAwPLJ1Yg==",
990 "license": "MIT",
991 "dependencies": {
992 "queue-tick": "^1.0.0"
993 }
994 },
995 "node_modules/nanoassert": {
996 "version": "2.0.0",
997 "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz",
998 "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==",
999 "license": "ISC"
1000 },
1001 "node_modules/noise-curve-ed": {
1002 "version": "2.1.0",
1003 "resolved": "https://registry.npmjs.org/noise-curve-ed/-/noise-curve-ed-2.1.0.tgz",
1004 "integrity": "sha512-zAzJx+VwZM3w6EA1hTmDhJfvAnCeBQn/1FAeZ0LtGxCcCtlAK/uJXQVF/eDVUOaAZ286lHlx77WJ+qj9SmsRRg==",
1005 "license": "ISC",
1006 "dependencies": {
1007 "b4a": "^1.1.0",
1008 "nanoassert": "^2.0.0",
1009 "sodium-universal": "^5.0.0"
1010 }
1011 },
1012 "node_modules/noise-curve-ed/node_modules/sodium-native": {
1013 "version": "5.0.10",
1014 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
1015 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
1016 "license": "MIT",
1017 "dependencies": {
1018 "require-addon": "^1.1.0",
1019 "which-runtime": "^1.2.1"
1020 },
1021 "engines": {
1022 "bare": ">=1.16.0"
1023 }
1024 },
1025 "node_modules/noise-curve-ed/node_modules/sodium-universal": {
1026 "version": "5.0.1",
1027 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1028 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1029 "license": "MIT",
1030 "dependencies": {
1031 "sodium-native": "^5.0.1"
1032 },
1033 "peerDependencies": {
1034 "sodium-javascript": "~0.8.0"
1035 },
1036 "peerDependenciesMeta": {
1037 "sodium-javascript": {
1038 "optional": true
1039 }
1040 }
1041 },
1042 "node_modules/noise-handshake": {
1043 "version": "4.2.0",
1044 "resolved": "https://registry.npmjs.org/noise-handshake/-/noise-handshake-4.2.0.tgz",
1045 "integrity": "sha512-9O/VTNX/E2/AToyMTTDU0J/4WhaXMTdqc2DHs9vf+snoZ0cenSBq0dNYTVV1snYYEkmo6QeRrYMxtqtoYnY+LA==",
1046 "license": "Apache-2.0",
1047 "dependencies": {
1048 "b4a": "^1.1.0",
1049 "nanoassert": "^2.0.0",
1050 "sodium-universal": "^5.0.0"
1051 }
1052 },
1053 "node_modules/noise-handshake/node_modules/sodium-native": {
1054 "version": "5.0.10",
1055 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
1056 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
1057 "license": "MIT",
1058 "dependencies": {
1059 "require-addon": "^1.1.0",
1060 "which-runtime": "^1.2.1"
1061 },
1062 "engines": {
1063 "bare": ">=1.16.0"
1064 }
1065 },
1066 "node_modules/noise-handshake/node_modules/sodium-universal": {
1067 "version": "5.0.1",
1068 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1069 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1070 "license": "MIT",
1071 "dependencies": {
1072 "sodium-native": "^5.0.1"
1073 },
1074 "peerDependencies": {
1075 "sodium-javascript": "~0.8.0"
1076 },
1077 "peerDependenciesMeta": {
1078 "sodium-javascript": {
1079 "optional": true
1080 }
1081 }
1082 },
1083 "node_modules/paparam": {
1084 "version": "1.10.0",
1085 "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.0.tgz",
1086 "integrity": "sha512-p36QQrwU3X6fj5d2JN20B4lcZI/O3fxMVUHc1xD7MNe5bcOf3jIgE0CIK5Zv/s5YAUGlobTJ/kKXrh1FJVrs3w==",
1087 "dev": true,
1088 "license": "Apache-2.0"
1089 },
1090 "node_modules/picomatch": {
1091 "version": "4.0.3",
1092 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
1093 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
1094 "dev": true,
1095 "license": "MIT",
1096 "engines": {
1097 "node": ">=12"
1098 },
1099 "funding": {
1100 "url": "https://github.com/sponsors/jonschlinkert"
1101 }
1102 },
1103 "node_modules/protocol-buffers-encodings": {
1104 "version": "1.2.0",
1105 "resolved": "https://registry.npmjs.org/protocol-buffers-encodings/-/protocol-buffers-encodings-1.2.0.tgz",
1106 "integrity": "sha512-daeNPuKh1NlLD1uDfbLpD+xyUTc07nEtfHwmBZmt/vH0B7VOM+JOCOpDcx9ZRpqHjAiIkGqyTDi+wfGSl17R9w==",
1107 "license": "MIT",
1108 "dependencies": {
1109 "b4a": "^1.6.0",
1110 "signed-varint": "^2.0.1",
1111 "varint": "5.0.0"
1112 }
1113 },
1114 "node_modules/protomux": {
1115 "version": "3.10.1",
1116 "resolved": "https://registry.npmjs.org/protomux/-/protomux-3.10.1.tgz",
1117 "integrity": "sha512-jgBqx8ZyaBWea/DFG4eOu1scOaeBwcnagiRC1XFVrjeGt7oAb0Pk5udPpBUpJ4DJBRjra50jD6YcZiQQTRqaaA==",
1118 "license": "MIT",
1119 "dependencies": {
1120 "b4a": "^1.3.1",
1121 "compact-encoding": "^2.5.1",
1122 "queue-tick": "^1.0.0",
1123 "safety-catch": "^1.0.1",
1124 "unslab": "^1.3.0"
1125 }
1126 },
1127 "node_modules/queue-tick": {
1128 "version": "1.0.1",
1129 "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
1130 "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
1131 "license": "MIT"
1132 },
1133 "node_modules/quickbit-native": {
1134 "version": "2.4.8",
1135 "resolved": "https://registry.npmjs.org/quickbit-native/-/quickbit-native-2.4.8.tgz",
1136 "integrity": "sha512-FcCcqI+nIAWGknqhtrYT5TSD7t/N+Xd8ctM+2PrIIBuwOi5hx0SxAvuPtzLIEMfT/2h9+fhBakUe2uALOHX6yw==",
1137 "license": "Apache-2.0",
1138 "optional": true,
1139 "dependencies": {
1140 "require-addon": "^1.1.0"
1141 }
1142 },
1143 "node_modules/quickbit-universal": {
1144 "version": "2.2.0",
1145 "resolved": "https://registry.npmjs.org/quickbit-universal/-/quickbit-universal-2.2.0.tgz",
1146 "integrity": "sha512-w02i1R8n7+6pEKTud8DfF8zbFY9o7RtPlUc3jWbtCkDKvhbx/AvV7oNnz4/TcmsPGpSJS+fq5Ud6RH6+YPvSGg==",
1147 "license": "ISC",
1148 "dependencies": {
1149 "b4a": "^1.6.0",
1150 "simdle-universal": "^1.1.0"
1151 },
1152 "optionalDependencies": {
1153 "quickbit-native": "^2.2.0"
1154 }
1155 },
1156 "node_modules/rache": {
1157 "version": "1.0.0",
1158 "resolved": "https://registry.npmjs.org/rache/-/rache-1.0.0.tgz",
1159 "integrity": "sha512-e0k0g0w/8jOCB+7YqCIlOa+OJ38k0wrYS4x18pMSmqOvLKoyhmMhmQyCcvfY6VaP8D75cqkEnlakXs+RYYLqNg==",
1160 "license": "Apache-2.0"
1161 },
1162 "node_modules/random-access-file": {
1163 "version": "4.1.2",
1164 "resolved": "https://registry.npmjs.org/random-access-file/-/random-access-file-4.1.2.tgz",
1165 "integrity": "sha512-GQM6R78DceZDcQod8KxlDFwXIiUvlvuy1EOzxTDsjuDjW5NlnlZi0MOk6iI4itAj/2vcvdqcEExYbVpC/dJcEw==",
1166 "license": "MIT",
1167 "dependencies": {
1168 "bare-fs": "^4.0.1",
1169 "bare-path": "^3.0.0",
1170 "random-access-storage": "^3.0.0"
1171 },
1172 "optionalDependencies": {
1173 "fs-native-extensions": "^1.3.1"
1174 }
1175 },
1176 "node_modules/random-access-storage": {
1177 "version": "3.0.2",
1178 "resolved": "https://registry.npmjs.org/random-access-storage/-/random-access-storage-3.0.2.tgz",
1179 "integrity": "sha512-Es9maUyWdJXWKckKy9s1+vT+DEgAt+PBb9lxPaake/0EDUsHehloKGv9v1zimS2V3gpFAcQXubvc1Rgci2sDPQ==",
1180 "license": "MIT",
1181 "dependencies": {
1182 "bare-events": "^2.2.0",
1183 "queue-tick": "^1.0.0"
1184 }
1185 },
1186 "node_modules/random-array-iterator": {
1187 "version": "1.0.0",
1188 "resolved": "https://registry.npmjs.org/random-array-iterator/-/random-array-iterator-1.0.0.tgz",
1189 "integrity": "sha512-u7xCM93XqKEvPTP6xZp2ehttcAemKnh73oKNf1FvzuVCfpt6dILDt1Kxl1LeBjm2iNIeR49VGFhy4Iz3yOun+Q==",
1190 "license": "MIT"
1191 },
1192 "node_modules/read-write-mutexify": {
1193 "version": "2.1.0",
1194 "resolved": "https://registry.npmjs.org/read-write-mutexify/-/read-write-mutexify-2.1.0.tgz",
1195 "integrity": "sha512-fDw/p5/acI1ytVY1UbxEDma/ej1yJH/n9NcjS9YNzcE6sPBPWdlru3ydRa/UBowUg4zqOvNMD5SOGYJrlQ6MzQ==",
1196 "license": "MIT"
1197 },
1198 "node_modules/ready-resource": {
1199 "version": "1.2.0",
1200 "resolved": "https://registry.npmjs.org/ready-resource/-/ready-resource-1.2.0.tgz",
1201 "integrity": "sha512-nfcco/8iAFV0M+2PYnmIc+/xY0iRb35d42HFHQ7AfjulbGEAFa+XWpByfwSyeVeiBoMLLFVMv1HixxNCqzSQ1g==",
1202 "license": "MIT",
1203 "dependencies": {
1204 "bare-events": "^2.2.0"
1205 }
1206 },
1207 "node_modules/require-addon": {
1208 "version": "1.2.0",
1209 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
1210 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
1211 "license": "Apache-2.0",
1212 "dependencies": {
1213 "bare-addon-resolve": "^1.3.0"
1214 },
1215 "engines": {
1216 "bare": ">=1.10.0"
1217 }
1218 },
1219 "node_modules/resolve-reject-promise": {
1220 "version": "1.1.0",
1221 "resolved": "https://registry.npmjs.org/resolve-reject-promise/-/resolve-reject-promise-1.1.0.tgz",
1222 "integrity": "sha512-LWsTOA91AqzBTjSGgX79Tc130pwcBK6xjpJEO+qRT5IKZ6bGnHKcc8QL3upUBcWuU8OTIDzKK2VNSwmmlqvAVg==",
1223 "license": "MIT"
1224 },
1225 "node_modules/safety-catch": {
1226 "version": "1.0.2",
1227 "resolved": "https://registry.npmjs.org/safety-catch/-/safety-catch-1.0.2.tgz",
1228 "integrity": "sha512-C1UYVZ4dtbBxEtvOcpjBaaD27nP8MlvyAQEp2fOTOEe6pfUpk1cDUxij6BR1jZup6rSyUTaBBplK7LanskrULA==",
1229 "license": "MIT"
1230 },
1231 "node_modules/same-object": {
1232 "version": "1.0.2",
1233 "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz",
1234 "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==",
1235 "dev": true,
1236 "license": "MIT"
1237 },
1238 "node_modules/signed-varint": {
1239 "version": "2.0.1",
1240 "resolved": "https://registry.npmjs.org/signed-varint/-/signed-varint-2.0.1.tgz",
1241 "integrity": "sha512-abgDPg1106vuZZOvw7cFwdCABddfJRz5akcCcchzTbhyhYnsG31y4AlZEgp315T7W3nQq5P4xeOm186ZiPVFzw==",
1242 "license": "MIT",
1243 "dependencies": {
1244 "varint": "~5.0.0"
1245 }
1246 },
1247 "node_modules/simdle-native": {
1248 "version": "1.3.9",
1249 "resolved": "https://registry.npmjs.org/simdle-native/-/simdle-native-1.3.9.tgz",
1250 "integrity": "sha512-Isc8sP4OiiIU0mpslD4GHEnR0VQWvR/54WN7YtwEDkNdTJVWtpmvsSvsgRlw5BNGxdYXlVRegdnrSu10H/PhvA==",
1251 "license": "Apache-2.0",
1252 "optional": true,
1253 "dependencies": {
1254 "b4a": "^1.6.0",
1255 "require-addon": "^1.1.0"
1256 }
1257 },
1258 "node_modules/simdle-universal": {
1259 "version": "1.1.2",
1260 "resolved": "https://registry.npmjs.org/simdle-universal/-/simdle-universal-1.1.2.tgz",
1261 "integrity": "sha512-3n3w1bs+uwgHKQjt6arez83EywNlhZzYvNOhvAASTl/8KqNIcqr6aHyGt3JRlfuUC7iB0tomJRPlJ2cRGIpBzA==",
1262 "license": "ISC",
1263 "dependencies": {
1264 "b4a": "^1.6.0"
1265 },
1266 "optionalDependencies": {
1267 "simdle-native": "^1.1.1"
1268 }
1269 },
1270 "node_modules/sodium-native": {
1271 "version": "4.3.3",
1272 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz",
1273 "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==",
1274 "license": "MIT",
1275 "dependencies": {
1276 "require-addon": "^1.1.0"
1277 }
1278 },
1279 "node_modules/sodium-secretstream": {
1280 "version": "1.2.0",
1281 "resolved": "https://registry.npmjs.org/sodium-secretstream/-/sodium-secretstream-1.2.0.tgz",
1282 "integrity": "sha512-q/DbraNFXm1KfCiiZvapmz5UC3OlpirYFIvBK2MhGaOFSb3gRyk8OXTi17UI9SGfshQNCpsVvlopogbzZNyW6Q==",
1283 "license": "MIT",
1284 "dependencies": {
1285 "b4a": "^1.1.1",
1286 "sodium-universal": "^5.0.0"
1287 }
1288 },
1289 "node_modules/sodium-secretstream/node_modules/sodium-native": {
1290 "version": "5.0.10",
1291 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
1292 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
1293 "license": "MIT",
1294 "dependencies": {
1295 "require-addon": "^1.1.0",
1296 "which-runtime": "^1.2.1"
1297 },
1298 "engines": {
1299 "bare": ">=1.16.0"
1300 }
1301 },
1302 "node_modules/sodium-secretstream/node_modules/sodium-universal": {
1303 "version": "5.0.1",
1304 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1305 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1306 "license": "MIT",
1307 "dependencies": {
1308 "sodium-native": "^5.0.1"
1309 },
1310 "peerDependencies": {
1311 "sodium-javascript": "~0.8.0"
1312 },
1313 "peerDependenciesMeta": {
1314 "sodium-javascript": {
1315 "optional": true
1316 }
1317 }
1318 },
1319 "node_modules/sodium-universal": {
1320 "version": "4.0.1",
1321 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-4.0.1.tgz",
1322 "integrity": "sha512-sNp13PrxYLaUFHTGoDKkSDFvoEu51bfzE12RwGlqU1fcrkpAOK0NvizaJzOWV0Omtk9me2+Pnbjcf/l0efxuGQ==",
1323 "license": "MIT",
1324 "dependencies": {
1325 "sodium-native": "^4.0.0"
1326 },
1327 "peerDependencies": {
1328 "sodium-javascript": "~0.8.0"
1329 },
1330 "peerDependenciesMeta": {
1331 "sodium-javascript": {
1332 "optional": true
1333 }
1334 }
1335 },
1336 "node_modules/stackframe": {
1337 "version": "1.3.4",
1338 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
1339 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
1340 "dev": true,
1341 "license": "MIT"
1342 },
1343 "node_modules/streamx": {
1344 "version": "2.23.0",
1345 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
1346 "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
1347 "license": "MIT",
1348 "dependencies": {
1349 "events-universal": "^1.0.0",
1350 "fast-fifo": "^1.3.2",
1351 "text-decoder": "^1.1.0"
1352 }
1353 },
1354 "node_modules/test-tmp": {
1355 "version": "1.4.0",
1356 "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz",
1357 "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==",
1358 "dev": true,
1359 "license": "MIT",
1360 "dependencies": {
1361 "bare-fs": "^4.0.1",
1362 "bare-os": "^3.3.0",
1363 "bare-path": "^3.0.0"
1364 }
1365 },
1366 "node_modules/text-decoder": {
1367 "version": "1.2.6",
1368 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.6.tgz",
1369 "integrity": "sha512-27FeW5GQFDfw0FpwMQhMagB7BztOOlmjcSRi97t2oplhKVTZtp0DZbSegSaXS5IIC6mxMvBG4AR1Sgc6BX3CQg==",
1370 "license": "Apache-2.0",
1371 "dependencies": {
1372 "b4a": "^1.6.4"
1373 }
1374 },
1375 "node_modules/timeout-refresh": {
1376 "version": "2.0.1",
1377 "resolved": "https://registry.npmjs.org/timeout-refresh/-/timeout-refresh-2.0.1.tgz",
1378 "integrity": "sha512-SVqEcMZBsZF9mA78rjzCrYrUs37LMJk3ShZ851ygZYW1cMeIjs9mL57KO6Iv5mmjSQnOe/29/VAfGXo+oRCiVw==",
1379 "license": "MIT"
1380 },
1381 "node_modules/tmatch": {
1382 "version": "5.0.0",
1383 "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz",
1384 "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==",
1385 "dev": true,
1386 "license": "ISC",
1387 "engines": {
1388 "node": ">=8"
1389 }
1390 },
1391 "node_modules/unslab": {
1392 "version": "1.3.0",
1393 "resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz",
1394 "integrity": "sha512-YATkfKAFj47kTzmiQrWXMyRvaVrHsW6MEALa4bm+FhiA2YG4oira+Z3DXN6LrYOYn2Y8eO94Lwl9DOHjs1FpoQ==",
1395 "license": "Apache-2.0",
1396 "dependencies": {
1397 "b4a": "^1.6.6"
1398 }
1399 },
1400 "node_modules/v8-to-istanbul": {
1401 "version": "9.3.0",
1402 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
1403 "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
1404 "dev": true,
1405 "license": "ISC",
1406 "dependencies": {
1407 "@jridgewell/trace-mapping": "^0.3.12",
1408 "@types/istanbul-lib-coverage": "^2.0.1",
1409 "convert-source-map": "^2.0.0"
1410 },
1411 "engines": {
1412 "node": ">=10.12.0"
1413 }
1414 },
1415 "node_modules/varint": {
1416 "version": "5.0.0",
1417 "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz",
1418 "integrity": "sha512-gC13b/bWrqQoKY2EmROCZ+AR0jitc6DnDGaQ6Ls9QpKmuSgJB1eQ7H3KETtQm7qSdMWMKCmsshyCmUwMLh3OAA==",
1419 "license": "MIT"
1420 },
1421 "node_modules/which-runtime": {
1422 "version": "1.3.2",
1423 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
1424 "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
1425 "license": "Apache-2.0"
1426 },
1427 "node_modules/xache": {
1428 "version": "1.2.1",
1429 "resolved": "https://registry.npmjs.org/xache/-/xache-1.2.1.tgz",
1430 "integrity": "sha512-igRS6jPreJ54ABdzhh4mCDXcz+XMaWO2q1ABRV2yWYuk29jlp8VT7UBdCqNkX7rpYBbXsebVVKkwIuYZjyZNqA==",
1431 "license": "MIT"
1432 },
1433 "node_modules/z32": {
1434 "version": "1.1.0",
1435 "resolved": "https://registry.npmjs.org/z32/-/z32-1.1.0.tgz",
1436 "integrity": "sha512-1WUHy+VS6d0HPNspDxvLssBbeQjXMjSnpv0vH82vRAUfg847NmX3OXozp/hRP5jPhxBbrVzrgvAt+UsGNzRFQQ==",
1437 "license": "MIT",
1438 "dependencies": {
1439 "b4a": "^1.5.3"
1440 }
1441 }
1442 }
1443}
hyper-daemon/package.json +24 -0
……@@ -0,0 +1,24 @@
1{
2 "name": "hyper-daemon",
3 "version": "1.0.0",
4 "description": "P2P automation runtime — trigger-action scripts for Keet rooms",
5 "type": "module",
6 "main": "index.js",
7 "pear": {
8 "name": "hyper-daemon",
9 "type": "terminal"
10 },
11 "scripts": {
12 "test": "brittle test/*.test.js"
13 },
14 "dependencies": {
15 "hyperbee": "^2.0.0",
16 "hypercore": "^10.0.0",
17 "corestore": "^6.0.0",
18 "b4a": "^1.0.0"
19 },
20 "devDependencies": {
21 "brittle": "^3.0.0"
22 },
23 "license": "MIT"
24}
hyper-daemon/test/daemons.test.js +422 -0
……@@ -0,0 +1,422 @@
1import test from 'brittle'
2import { crosspost, matchFilter, formatForTarget, stripRoutingTags, PLATFORM_LIMITS } from '../daemons/crosspost.js'
3import {
4 summarize, extractTopics, extractKeyMessages, extractParticipants,
5 getTimeRange, formatSummary, formatDuration, tokenize
6} from '../daemons/summarize.js'
7
8// ---------------------------------------------------------------------------
9// Crosspost tests
10// ---------------------------------------------------------------------------
11
12test('crosspost - match by source platform', (t) => {
13 const msg = { source: 'keet', text: 'Hello #public world', author: 'alice' }
14 const config = {
15 rules: [
16 { from: 'keet', to: 'mastodon', filter: { tags: ['public'] } }
17 ]
18 }
19
20 const actions = crosspost.process(msg, config)
21 t.is(actions.length, 1)
22 t.is(actions[0].target, 'mastodon')
23})
24
25test('crosspost - no match when source differs', (t) => {
26 const msg = { source: 'discord', text: 'Hello #public world', author: 'alice' }
27 const config = {
28 rules: [
29 { from: 'keet', to: 'mastodon', filter: { tags: ['public'] } }
30 ]
31 }
32
33 const actions = crosspost.process(msg, config)
34 t.is(actions.length, 0)
35})
36
37test('crosspost - match by tag filter', (t) => {
38 const msg = { source: 'keet', text: 'Check this out #public', author: 'bob' }
39 const result = matchFilter(msg, { tags: ['public'] })
40 t.ok(result)
41})
42
43test('crosspost - no match when tag absent', (t) => {
44 const msg = { source: 'keet', text: 'This is private stuff', author: 'bob' }
45 const result = matchFilter(msg, { tags: ['public'] })
46 t.absent(result)
47})
48
49test('crosspost - match by type filter', (t) => {
50 const msg = { source: 'keet', text: 'Image post', type: 'image', author: 'alice' }
51 const result = matchFilter(msg, { type: 'image' })
52 t.ok(result)
53})
54
55test('crosspost - no match when type differs', (t) => {
56 const msg = { source: 'keet', text: 'Text post', type: 'text', author: 'alice' }
57 const result = matchFilter(msg, { type: 'image' })
58 t.absent(result)
59})
60
61test('crosspost - match by author filter', (t) => {
62 const msg = { source: 'keet', text: 'Hello', author: 'alice' }
63 const result = matchFilter(msg, { author: 'alice' })
64 t.ok(result)
65})
66
67test('crosspost - no match when author differs', (t) => {
68 const msg = { source: 'keet', text: 'Hello', author: 'bob' }
69 const result = matchFilter(msg, { author: 'alice' })
70 t.absent(result)
71})
72
73test('crosspost - match by pattern filter', (t) => {
74 const msg = { source: 'keet', text: 'Check https://example.com for details', author: 'alice' }
75 const result = matchFilter(msg, { pattern: 'https?://\\S+' })
76 t.ok(result)
77})
78
79test('crosspost - empty filter matches everything', (t) => {
80 const msg = { source: 'mastodon', text: 'Anything goes', author: 'carol' }
81 const result = matchFilter(msg, {})
82 t.ok(result)
83})
84
85test('crosspost - format for mastodon trims to 500 chars', (t) => {
86 const longText = 'A'.repeat(600)
87 const msg = { source: 'keet', text: longText, author: 'alice' }
88 const formatted = formatForTarget(msg, 'mastodon')
89 t.ok(formatted.length <= PLATFORM_LIMITS.mastodon)
90 t.ok(formatted.includes('...'))
91 t.ok(formatted.includes('[via keet/alice]'))
92})
93
94test('crosspost - format for mastodon with short text', (t) => {
95 const msg = { source: 'keet', text: 'Short post', author: 'alice' }
96 const formatted = formatForTarget(msg, 'mastodon')
97 t.ok(formatted.length <= PLATFORM_LIMITS.mastodon)
98 t.ok(formatted.includes('[via keet/alice]'))
99 t.ok(formatted.includes('Short post'))
100})
101
102test('crosspost - format for keet adds attribution', (t) => {
103 const msg = { source: 'mastodon', text: 'Hello from the fediverse', author: 'bob' }
104 const formatted = formatForTarget(msg, 'keet')
105 t.ok(formatted.includes('[via mastodon/bob]'))
106 t.ok(formatted.includes('Hello from the fediverse'))
107})
108
109test('crosspost - format for irc trims and uses angle brackets', (t) => {
110 const msg = { source: 'keet', text: 'Hello IRC', author: 'alice' }
111 const formatted = formatForTarget(msg, 'irc')
112 t.ok(formatted.includes('<alice@keet>'))
113 t.ok(formatted.includes('Hello IRC'))
114})
115
116test('crosspost - format for discord uses markdown bold', (t) => {
117 const msg = { source: 'keet', text: 'Hey discord', author: 'alice' }
118 const formatted = formatForTarget(msg, 'discord')
119 t.ok(formatted.includes('**alice**'))
120 t.ok(formatted.includes('Hey discord'))
121})
122
123test('crosspost - format for matrix uses brackets', (t) => {
124 const msg = { source: 'keet', text: 'Hello matrix', author: 'alice' }
125 const formatted = formatForTarget(msg, 'matrix')
126 t.ok(formatted.includes('[keet]'))
127 t.ok(formatted.includes('<alice>'))
128})
129
130test('crosspost - multiple rules produce multiple actions', (t) => {
131 const msg = { source: 'keet', text: 'Broadcast #public', author: 'alice' }
132 const config = {
133 rules: [
134 { from: 'keet', to: 'mastodon', filter: { tags: ['public'] } },
135 { from: 'keet', to: 'discord', filter: { tags: ['public'] } },
136 { from: 'keet', to: 'matrix', filter: { tags: ['public'] } }
137 ]
138 }
139
140 const actions = crosspost.process(msg, config)
141 t.is(actions.length, 3)
142 t.is(actions[0].target, 'mastodon')
143 t.is(actions[1].target, 'discord')
144 t.is(actions[2].target, 'matrix')
145})
146
147test('crosspost - empty rules produce no actions', (t) => {
148 const msg = { source: 'keet', text: 'Hello', author: 'alice' }
149 const config = { rules: [] }
150
151 const actions = crosspost.process(msg, config)
152 t.is(actions.length, 0)
153})
154
155test('crosspost - metadata includes crosspost info', (t) => {
156 const msg = { source: 'keet', text: 'Cross me #public', author: 'alice', timestamp: 12345 }
157 const config = {
158 rules: [
159 { from: 'keet', to: 'mastodon', filter: { tags: ['public'] } }
160 ]
161 }
162
163 const actions = crosspost.process(msg, config)
164 t.is(actions[0].metadata.crosspostedFrom, 'keet')
165 t.is(actions[0].metadata.originalAuthor, 'alice')
166 t.is(actions[0].metadata.originalTimestamp, 12345)
167 t.alike(actions[0].metadata.rule, { from: 'keet', to: 'mastodon' })
168})
169
170test('crosspost - stripRoutingTags removes #public from output', (t) => {
171 const result = stripRoutingTags('Hello #public world', ['public'])
172 t.is(result, 'Hello world')
173})
174
175test('crosspost - routing tags stripped in formatted output', (t) => {
176 const msg = { source: 'keet', text: 'Important #public announcement', author: 'alice' }
177 const rule = { from: 'keet', to: 'keet', filter: { tags: ['public'] } }
178 const formatted = formatForTarget(msg, 'keet', rule)
179 t.absent(formatted.includes('#public'))
180 t.ok(formatted.includes('Important'))
181 t.ok(formatted.includes('announcement'))
182})
183
184test('crosspost - contains filter works', (t) => {
185 const msg = { source: 'keet', text: 'secret word: banana', author: 'alice' }
186 t.ok(matchFilter(msg, { contains: 'banana' }))
187 t.absent(matchFilter(msg, { contains: 'orange' }))
188})
189
190// ---------------------------------------------------------------------------
191// Summarize tests
192// ---------------------------------------------------------------------------
193
194function makeMessages (count, opts = {}) {
195 const messages = []
196 const authors = opts.authors || ['alice', 'bob', 'carol']
197 const baseTime = opts.baseTime || 1700000000000
198
199 for (let i = 0; i < count; i++) {
200 messages.push({
201 text: opts.textFn ? opts.textFn(i) : `Message number ${i} about hypercore and distributed systems`,
202 author: authors[i % authors.length],
203 timestamp: baseTime + (i * 60000), // 1 min apart
204 type: 'text'
205 })
206 }
207 return messages
208}
209
210test('summarize - extract topics from messages', (t) => {
211 const messages = [
212 { text: 'hypercore is great for distributed data', author: 'alice' },
213 { text: 'I love hypercore and hyperbee', author: 'bob' },
214 { text: 'hypercore makes P2P easy', author: 'carol' },
215 { text: 'hyperbee is built on hypercore', author: 'alice' },
216 { text: 'distributed systems with hypercore', author: 'bob' }
217 ]
218
219 const topics = extractTopics(messages, { minCount: 2 })
220 t.ok(topics.length > 0)
221
222 // hypercore should be the top topic (appears in all 5)
223 t.is(topics[0].word, 'hypercore')
224 t.is(topics[0].count, 5)
225})
226
227test('summarize - extract key messages with questions', (t) => {
228 const messages = [
229 { text: 'Hello', author: 'alice' },
230 { text: 'How does hypercore replication work?', author: 'bob' },
231 { text: 'ok', author: 'carol' },
232 { text: 'Check https://docs.holepunch.to for details', author: 'alice' },
233 { text: 'cool', author: 'bob' }
234 ]
235
236 const keyMsgs = extractKeyMessages(messages)
237 t.ok(keyMsgs.length >= 2)
238
239 // The question and the link should be picked up
240 const texts = keyMsgs.map(m => m.text)
241 t.ok(texts.some(t => t.includes('?')))
242 t.ok(texts.some(t => t.includes('https://')))
243})
244
245test('summarize - extract key messages with links', (t) => {
246 const messages = [
247 { text: 'hi', author: 'alice' },
248 { text: 'See https://example.com/docs', author: 'bob' },
249 { text: 'thanks', author: 'carol' }
250 ]
251
252 const keyMsgs = extractKeyMessages(messages)
253 t.ok(keyMsgs.length >= 1)
254 t.ok(keyMsgs[0].text.includes('https://'))
255})
256
257test('summarize - participant list sorted by message count', (t) => {
258 const messages = [
259 { text: 'a', author: 'alice' },
260 { text: 'b', author: 'alice' },
261 { text: 'c', author: 'alice' },
262 { text: 'd', author: 'bob' },
263 { text: 'e', author: 'carol' }
264 ]
265
266 const participants = extractParticipants(messages)
267 t.is(participants.length, 3)
268 t.is(participants[0].name, 'alice')
269 t.is(participants[0].count, 3)
270 t.is(participants[1].name, 'bob')
271 t.is(participants[1].count, 1)
272})
273
274test('summarize - time range calculation', (t) => {
275 const messages = [
276 { text: 'first', author: 'alice', timestamp: 1000 },
277 { text: 'second', author: 'bob', timestamp: 2000 },
278 { text: 'third', author: 'carol', timestamp: 5000 }
279 ]
280
281 const range = getTimeRange(messages)
282 t.is(range.start, 1000)
283 t.is(range.end, 5000)
284 t.is(range.durationMs, 4000)
285})
286
287test('summarize - time range with no timestamps', (t) => {
288 const messages = [
289 { text: 'first', author: 'alice' },
290 { text: 'second', author: 'bob' }
291 ]
292
293 const range = getTimeRange(messages)
294 t.is(range.start, 0)
295 t.is(range.end, 0)
296 t.is(range.durationMs, 0)
297})
298
299test('summarize - summary format includes all sections', (t) => {
300 const messages = makeMessages(20, {
301 textFn: (i) => {
302 if (i === 5) return 'How does hypercore replication work?'
303 if (i === 10) return 'Check https://docs.holepunch.to for info'
304 return `Discussing hypercore distributed database systems for message ${i}`
305 }
306 })
307
308 const result = summarize.process(messages)
309 t.ok(result.summary.includes('## Conversation Summary'))
310 t.ok(result.summary.includes('### Participants'))
311 t.ok(result.summary.includes('### Key Messages'))
312 t.is(result.messageCount, 20)
313 t.ok(result.participants.length > 0)
314})
315
316test('summarize - minimum message threshold', (t) => {
317 const messages = makeMessages(5) // below default minMessages of 10
318
319 const result = summarize.process(messages)
320 t.is(result.summary, '')
321 t.ok(result.belowThreshold)
322 t.is(result.messageCount, 5)
323 // Participants still extracted even below threshold
324 t.ok(result.participants.length > 0)
325})
326
327test('summarize - empty input handling', (t) => {
328 const result = summarize.process([])
329 t.is(result.summary, '')
330 t.is(result.messageCount, 0)
331 t.is(result.participants.length, 0)
332 t.is(result.topics.length, 0)
333 t.is(result.keyMessages.length, 0)
334})
335
336test('summarize - null input handling', (t) => {
337 const result = summarize.process(null)
338 t.is(result.summary, '')
339 t.is(result.messageCount, 0)
340})
341
342test('summarize - window size limits messages', (t) => {
343 const messages = makeMessages(100)
344
345 const result = summarize.process(messages, { windowSize: 20 })
346 t.is(result.messageCount, 20)
347})
348
349test('summarize - tokenize strips stop words', (t) => {
350 const tokens = tokenize('the quick brown fox jumps over the lazy dog')
351 t.absent(tokens.includes('the'))
352 t.absent(tokens.includes('over'))
353 t.ok(tokens.includes('quick'))
354 t.ok(tokens.includes('brown'))
355 t.ok(tokens.includes('fox'))
356})
357
358test('summarize - tokenize strips URLs', (t) => {
359 const tokens = tokenize('check https://example.com for details')
360 t.absent(tokens.some(t => t.includes('https')))
361 t.absent(tokens.some(t => t.includes('example')))
362})
363
364test('summarize - formatDuration readable output', (t) => {
365 t.is(formatDuration(30000), 'less than a minute')
366 t.is(formatDuration(60000), '1 minute')
367 t.is(formatDuration(120000), '2 minutes')
368 t.is(formatDuration(3600000), '1 hour')
369 t.is(formatDuration(7200000), '2 hours')
370 t.is(formatDuration(90000000), '1 day')
371})
372
373test('summarize - messages with various content types', (t) => {
374 const messages = makeMessages(15, {
375 textFn: (i) => {
376 switch (i % 5) {
377 case 0: return 'Is hypercore better than IPFS?'
378 case 1: return 'Read https://docs.holepunch.to/hypercore'
379 case 2: return 'The `hypercore.append()` method is key'
380 case 3: return 'I think hypercore is amazing!'
381 case 4: return 'ok'
382 }
383 }
384 })
385
386 const keyMsgs = extractKeyMessages(messages)
387 t.ok(keyMsgs.length > 0)
388
389 // Questions should be highest priority (score 3)
390 t.ok(keyMsgs[0].text.includes('?'))
391})
392
393test('summarize - topics minCount threshold', (t) => {
394 const messages = [
395 { text: 'hypercore is cool', author: 'alice' },
396 { text: 'hypercore rocks', author: 'bob' },
397 { text: 'randomword', author: 'carol' }
398 ]
399
400 // minCount=3 means hypercore (2 occurrences) won't pass
401 const topics = extractTopics(messages, { minCount: 3 })
402 t.is(topics.length, 0)
403
404 // minCount=2 means hypercore passes
405 const topics2 = extractTopics(messages, { minCount: 2 })
406 t.ok(topics2.length > 0)
407 t.is(topics2[0].word, 'hypercore')
408})
409
410test('summarize - config override', (t) => {
411 const messages = makeMessages(20)
412
413 const result = summarize.process(messages, {
414 includeParticipants: false,
415 includeTopics: false
416 })
417
418 t.is(result.participants.length, 0)
419 t.is(result.topics.length, 0)
420 // Key messages still extracted
421 t.ok(result.messageCount > 0)
422})
hyper-daemon/test/runtime.test.js +162 -0
……@@ -0,0 +1,162 @@
1import test from 'brittle'
2import { DaemonRuntime } from '../lib/runtime.js'
3import { tmpdir } from 'os'
4import { join } from 'path'
5import { randomBytes } from 'crypto'
6
7function tmpStorage () {
8 return join(tmpdir(), 'hyper-daemon-test-' + randomBytes(8).toString('hex'))
9}
10
11test('DaemonRuntime - load and fire script', async (t) => {
12 const logs = []
13 const runtime = new DaemonRuntime(tmpStorage(), {
14 onLog: (msg) => logs.push(msg)
15 })
16 await runtime.ready()
17
18 let actionCalled = false
19
20 await runtime.load('greet', {
21 trigger: { type: 'message', filter: { contains: 'hello' } },
22 actions: [{
23 type: 'log',
24 message: 'Greeting detected!'
25 }],
26 enabled: true
27 })
28
29 // Override action for testing — fire triggers the registered action
30 const results = await runtime.fire('message', { text: 'hello world' })
31 t.is(results.length, 1)
32 t.ok(results[0].ok)
33
34 t.ok(logs.some(l => l.includes('Loaded script: greet')))
35
36 await runtime.close()
37})
38
39test('DaemonRuntime - list scripts', async (t) => {
40 const runtime = new DaemonRuntime(tmpStorage(), { onLog: () => {} })
41 await runtime.ready()
42
43 await runtime.load('script-a', {
44 trigger: { type: 'message' },
45 actions: [{ type: 'log', message: 'a' }]
46 })
47 await runtime.load('script-b', {
48 trigger: { type: 'file-change' },
49 actions: [{ type: 'log', message: 'b' }]
50 })
51
52 const list = runtime.list()
53 t.is(list.length, 2)
54 t.ok(list.find(s => s.id === 'script-a'))
55 t.ok(list.find(s => s.id === 'script-b'))
56
57 await runtime.close()
58})
59
60test('DaemonRuntime - unload script', async (t) => {
61 const runtime = new DaemonRuntime(tmpStorage(), { onLog: () => {} })
62 await runtime.ready()
63
64 await runtime.load('temp', {
65 trigger: { type: 'message' },
66 actions: [{ type: 'log', message: 'temp' }]
67 })
68
69 t.is(runtime.list().length, 1)
70
71 await runtime.unload('temp')
72 t.is(runtime.list().length, 0)
73
74 // Verify no triggers fire
75 const results = await runtime.fire('message', {})
76 t.is(results.length, 0)
77
78 await runtime.close()
79})
80
81test('DaemonRuntime - enable/disable', async (t) => {
82 const runtime = new DaemonRuntime(tmpStorage(), { onLog: () => {} })
83 await runtime.ready()
84
85 await runtime.load('toggle', {
86 trigger: { type: 'message' },
87 actions: [{ type: 'log', message: 'toggled' }]
88 })
89
90 runtime.disable('toggle')
91 const r1 = await runtime.fire('message', { text: 'test' })
92 t.is(r1.length, 0)
93
94 runtime.enable('toggle')
95 const r2 = await runtime.fire('message', { text: 'test' })
96 t.is(r2.length, 1)
97
98 await runtime.close()
99})
100
101test('DaemonRuntime - filter matching', async (t) => {
102 const runtime = new DaemonRuntime(tmpStorage(), { onLog: () => {} })
103 await runtime.ready()
104
105 await runtime.load('filtered', {
106 trigger: { type: 'message', filter: { author: 'alice' } },
107 actions: [{ type: 'log', message: 'alice said something' }]
108 })
109
110 const r1 = await runtime.fire('message', { author: 'alice', text: 'hi' })
111 t.is(r1.length, 1)
112
113 const r2 = await runtime.fire('message', { author: 'bob', text: 'hi' })
114 t.is(r2.length, 0)
115
116 await runtime.close()
117})
118
119test('DaemonRuntime - event logging', async (t) => {
120 const runtime = new DaemonRuntime(tmpStorage(), { onLog: () => {} })
121 await runtime.ready()
122
123 await runtime.load('logged', {
124 trigger: { type: 'message' },
125 actions: [{ type: 'log', message: 'event' }]
126 })
127
128 await runtime.fire('message', { text: 'first' })
129 await runtime.fire('message', { text: 'second' })
130
131 const log = await runtime.getLog('logged')
132 t.is(log.length, 2)
133 t.ok(log[0].timestamp > 0)
134 t.is(log[0].trigger, 'message')
135
136 await runtime.close()
137})
138
139test('DaemonRuntime - persistence (loadAll)', async (t) => {
140 const storage = tmpStorage()
141
142 // Save scripts
143 const rt1 = new DaemonRuntime(storage, { onLog: () => {} })
144 await rt1.ready()
145 await rt1.load('persistent', {
146 trigger: { type: 'message' },
147 actions: [{ type: 'log', message: 'persistent' }]
148 })
149 await rt1.close()
150
151 // Reload
152 const rt2 = new DaemonRuntime(storage, { onLog: () => {} })
153 await rt2.ready()
154 const loaded = await rt2.loadAll()
155 t.is(loaded, 1)
156
157 const list = rt2.list()
158 t.is(list.length, 1)
159 t.is(list[0].id, 'persistent')
160
161 await rt2.close()
162})
hyper-daemon/test/triggers.test.js +228 -0
……@@ -0,0 +1,228 @@
1import test from 'brittle'
2import { TriggerEngine } from '../lib/triggers.js'
3
4test('TriggerEngine - register and fire', async (t) => {
5 const engine = new TriggerEngine()
6 let called = false
7
8 engine.register('test1', {
9 type: 'message',
10 action: async (data) => {
11 called = true
12 return data.text
13 }
14 })
15
16 const results = await engine.fire('message', { text: 'hello' })
17 t.ok(called)
18 t.is(results.length, 1)
19 t.is(results[0].id, 'test1')
20 t.ok(results[0].ok)
21 t.is(results[0].result, 'hello')
22
23 engine.destroy()
24})
25
26test('TriggerEngine - no match for different event type', async (t) => {
27 const engine = new TriggerEngine()
28
29 engine.register('msg-handler', {
30 type: 'message',
31 action: async () => 'handled'
32 })
33
34 const results = await engine.fire('file-change', {})
35 t.is(results.length, 0)
36
37 engine.destroy()
38})
39
40test('TriggerEngine - filter by channel', async (t) => {
41 const engine = new TriggerEngine()
42
43 engine.register('chan-filter', {
44 type: 'message',
45 filter: { channel: '#important' },
46 action: async () => 'matched'
47 })
48
49 const r1 = await engine.fire('message', { channel: '#important', text: 'hi' })
50 t.is(r1.length, 1)
51
52 const r2 = await engine.fire('message', { channel: '#general', text: 'hi' })
53 t.is(r2.length, 0)
54
55 engine.destroy()
56})
57
58test('TriggerEngine - filter by contains', async (t) => {
59 const engine = new TriggerEngine()
60
61 engine.register('keyword', {
62 type: 'message',
63 filter: { contains: '#public' },
64 action: async () => 'matched'
65 })
66
67 const r1 = await engine.fire('message', { text: 'Hey everyone #public' })
68 t.is(r1.length, 1)
69
70 const r2 = await engine.fire('message', { text: 'private message' })
71 t.is(r2.length, 0)
72
73 engine.destroy()
74})
75
76test('TriggerEngine - filter by pattern', async (t) => {
77 const engine = new TriggerEngine()
78
79 engine.register('url-detect', {
80 type: 'message',
81 filter: { pattern: 'https?://\\S+' },
82 action: async () => 'url found'
83 })
84
85 const r1 = await engine.fire('message', { text: 'Check https://example.com' })
86 t.is(r1.length, 1)
87
88 const r2 = await engine.fire('message', { text: 'no urls here' })
89 t.is(r2.length, 0)
90
91 engine.destroy()
92})
93
94test('TriggerEngine - filter by author', async (t) => {
95 const engine = new TriggerEngine()
96
97 engine.register('alice-only', {
98 type: 'message',
99 filter: { author: 'alice' },
100 action: async () => 'from alice'
101 })
102
103 const r1 = await engine.fire('message', { author: 'alice', text: 'hi' })
104 t.is(r1.length, 1)
105
106 const r2 = await engine.fire('message', { author: 'bob', text: 'hi' })
107 t.is(r2.length, 0)
108
109 engine.destroy()
110})
111
112test('TriggerEngine - filter by tags', async (t) => {
113 const engine = new TriggerEngine()
114
115 engine.register('tagged', {
116 type: 'message',
117 filter: { tags: ['urgent', 'critical'] },
118 action: async () => 'tagged'
119 })
120
121 const r1 = await engine.fire('message', { text: 'This is URGENT please help' })
122 t.is(r1.length, 1)
123
124 const r2 = await engine.fire('message', { text: 'normal message' })
125 t.is(r2.length, 0)
126
127 engine.destroy()
128})
129
130test('TriggerEngine - enable/disable', async (t) => {
131 const engine = new TriggerEngine()
132
133 engine.register('toggle', {
134 type: 'message',
135 action: async () => 'fired'
136 })
137
138 engine.disable('toggle')
139 const r1 = await engine.fire('message', { text: 'hi' })
140 t.is(r1.length, 0)
141
142 engine.enable('toggle')
143 const r2 = await engine.fire('message', { text: 'hi' })
144 t.is(r2.length, 1)
145
146 engine.destroy()
147})
148
149test('TriggerEngine - unregister', async (t) => {
150 const engine = new TriggerEngine()
151
152 engine.register('temp', {
153 type: 'message',
154 action: async () => 'fired'
155 })
156
157 t.is(engine.count, 1)
158 engine.unregister('temp')
159 t.is(engine.count, 0)
160
161 const results = await engine.fire('message', {})
162 t.is(results.length, 0)
163
164 engine.destroy()
165})
166
167test('TriggerEngine - list triggers', (t) => {
168 const engine = new TriggerEngine()
169
170 engine.register('a', { type: 'message', action: async () => {} })
171 engine.register('b', { type: 'file-change', filter: { channel: 'x' }, action: async () => {} })
172
173 const list = engine.list()
174 t.is(list.length, 2)
175 t.ok(list.find(t => t.id === 'a' && t.type === 'message'))
176 t.ok(list.find(t => t.id === 'b' && t.hasFilter === true))
177
178 engine.destroy()
179})
180
181test('TriggerEngine - multiple triggers fire in order', async (t) => {
182 const engine = new TriggerEngine()
183 const order = []
184
185 engine.register('first', {
186 type: 'message',
187 action: async () => { order.push('first') }
188 })
189 engine.register('second', {
190 type: 'message',
191 action: async () => { order.push('second') }
192 })
193
194 await engine.fire('message', {})
195 t.alike(order, ['first', 'second'])
196
197 engine.destroy()
198})
199
200test('TriggerEngine - action error captured', async (t) => {
201 const engine = new TriggerEngine()
202
203 engine.register('broken', {
204 type: 'message',
205 action: async () => { throw new Error('boom') }
206 })
207
208 const results = await engine.fire('message', {})
209 t.is(results.length, 1)
210 t.absent(results[0].ok)
211 t.is(results[0].error, 'boom')
212
213 engine.destroy()
214})
215
216test('TriggerEngine - schedule trigger', async (t) => {
217 const engine = new TriggerEngine()
218 let count = 0
219
220 engine.startSchedule('ticker', 30, async () => {
221 count++
222 })
223
224 await new Promise(r => setTimeout(r, 80))
225 t.ok(count >= 2, `scheduled ran ${count} times`)
226
227 engine.destroy()
228})
hyper-feed/index.js +4 -0
……@@ -0,0 +1,4 @@
1import { Feed } from './lib/feed.js'
2import { FeedStore, SOURCES, TYPES } from './lib/feed-store.js'
3
4export { Feed, FeedStore, SOURCES, TYPES }
hyper-feed/lib/feed-store.js +243 -0
……@@ -0,0 +1,243 @@
1import { randomBytes } from 'crypto'
2
3const SOURCES = ['irc', 'keet', 'mastodon', 'discord', 'email']
4const TYPES = ['post', 'reply', 'boost', 'reaction', 'system']
5
6class FeedStore {
7 constructor (bee) {
8 this.bee = bee
9 }
10
11 _genId () {
12 return randomBytes(8).toString('hex')
13 }
14
15 _itemKey (id) {
16 return `item/${id}`
17 }
18
19 _timelineKey (timestamp, id) {
20 return `timeline/${this._padTimestamp(timestamp)}/${id}`
21 }
22
23 _sourceKey (source, timestamp, id) {
24 return `source/${source}/${this._padTimestamp(timestamp)}/${id}`
25 }
26
27 _authorKey (authorId, timestamp, id) {
28 return `author/${authorId}/${this._padTimestamp(timestamp)}/${id}`
29 }
30
31 _padTimestamp (ts) {
32 return String(ts).padStart(16, '0')
33 }
34
35 async ready () {
36 await this.bee.ready()
37 }
38
39 async add (opts = {}) {
40 const id = opts.id || this._genId()
41 const now = Date.now()
42
43 const item = {
44 id,
45 source: opts.source || 'keet',
46 author: opts.author || 'unknown',
47 authorId: opts.authorId || opts.author || 'unknown',
48 text: opts.text || '',
49 timestamp: opts.timestamp || now,
50 type: opts.type || 'post',
51 tags: opts.tags || [],
52 url: opts.url || null,
53 metadata: opts.metadata || {}
54 }
55
56 const batch = this.bee.batch()
57
58 // Primary record
59 await batch.put(this._itemKey(id), item)
60
61 // Timeline index (stores just the id for lookup)
62 await batch.put(this._timelineKey(item.timestamp, id), { id })
63
64 // Source index
65 await batch.put(this._sourceKey(item.source, item.timestamp, id), { id })
66
67 // Author index
68 await batch.put(this._authorKey(item.authorId, item.timestamp, id), { id })
69
70 await batch.flush()
71
72 return item
73 }
74
75 async get (id) {
76 const node = await this.bee.get(this._itemKey(id))
77 return node ? node.value : null
78 }
79
80 async remove (id) {
81 const item = await this.get(id)
82 if (!item) return false
83
84 const batch = this.bee.batch()
85
86 // Remove primary record
87 await batch.del(this._itemKey(id))
88
89 // Remove timeline index
90 await batch.del(this._timelineKey(item.timestamp, id))
91
92 // Remove source index
93 await batch.del(this._sourceKey(item.source, item.timestamp, id))
94
95 // Remove author index
96 await batch.del(this._authorKey(item.authorId, item.timestamp, id))
97
98 await batch.flush()
99
100 return true
101 }
102
103 async timeline (opts = {}) {
104 const { limit, before, after, source, type } = opts
105 const items = []
106
107 const rangeOpts = {}
108
109 if (after) {
110 rangeOpts.gt = `timeline/${this._padTimestamp(after)}/~`
111 } else {
112 rangeOpts.gte = 'timeline/'
113 }
114
115 if (before) {
116 rangeOpts.lt = `timeline/${this._padTimestamp(before)}/`
117 } else {
118 rangeOpts.lte = 'timeline/~'
119 }
120
121 for await (const node of this.bee.createReadStream(rangeOpts)) {
122 if (!node.value) continue
123
124 const item = await this.get(node.value.id)
125 if (!item) continue
126
127 // Filter by source
128 if (source && item.source !== source) continue
129
130 // Filter by type
131 if (type && item.type !== type) continue
132
133 items.push(item)
134
135 if (limit && items.length >= limit) break
136 }
137
138 return items
139 }
140
141 async bySource (source, opts = {}) {
142 const { limit, before } = opts
143 const items = []
144
145 const rangeOpts = {
146 gte: `source/${source}/`
147 }
148
149 if (before) {
150 rangeOpts.lt = `source/${source}/${this._padTimestamp(before)}/`
151 } else {
152 rangeOpts.lte = `source/${source}/~`
153 }
154
155 for await (const node of this.bee.createReadStream(rangeOpts)) {
156 if (!node.value) continue
157
158 const item = await this.get(node.value.id)
159 if (!item) continue
160
161 items.push(item)
162
163 if (limit && items.length >= limit) break
164 }
165
166 return items
167 }
168
169 async byAuthor (authorId, opts = {}) {
170 const { limit } = opts
171 const items = []
172
173 for await (const node of this.bee.createReadStream({
174 gte: `author/${authorId}/`,
175 lte: `author/${authorId}/~`
176 })) {
177 if (!node.value) continue
178
179 const item = await this.get(node.value.id)
180 if (!item) continue
181
182 items.push(item)
183
184 if (limit && items.length >= limit) break
185 }
186
187 return items
188 }
189
190 async search (query) {
191 const q = query.toLowerCase()
192 const results = []
193
194 for await (const node of this.bee.createReadStream({
195 gte: 'item/',
196 lte: 'item/~'
197 })) {
198 const item = node.value
199 if (!item) continue
200
201 if (item.text && item.text.toLowerCase().includes(q)) {
202 results.push(item)
203 continue
204 }
205
206 if (item.tags && item.tags.some(t => t.toLowerCase().includes(q))) {
207 results.push(item)
208 continue
209 }
210 }
211
212 return results
213 }
214
215 async stats () {
216 const bySource = {}
217 const byType = {}
218
219 for await (const node of this.bee.createReadStream({
220 gte: 'item/',
221 lte: 'item/~'
222 })) {
223 const item = node.value
224 if (!item) continue
225
226 bySource[item.source] = (bySource[item.source] || 0) + 1
227 byType[item.type] = (byType[item.type] || 0) + 1
228 }
229
230 let total = 0
231 for (const count of Object.values(bySource)) {
232 total += count
233 }
234
235 return {
236 total,
237 bySource,
238 byType
239 }
240 }
241}
242
243export { FeedStore, SOURCES, TYPES }
hyper-feed/lib/feed.js +59 -0
……@@ -0,0 +1,59 @@
1import Corestore from 'corestore'
2import Hyperbee from 'hyperbee'
3import { FeedStore } from './feed-store.js'
4
5class Feed {
6 constructor (storage) {
7 this.store = new Corestore(storage)
8 this.bee = new Hyperbee(this.store.get({ name: 'feed' }), {
9 keyEncoding: 'utf-8',
10 valueEncoding: 'json'
11 })
12 this.items = new FeedStore(this.bee)
13 }
14
15 async ready () {
16 await this.store.ready()
17 await this.items.ready()
18 }
19
20 // Item CRUD
21 async add (opts) {
22 return this.items.add(opts)
23 }
24
25 async get (id) {
26 return this.items.get(id)
27 }
28
29 async remove (id) {
30 return this.items.remove(id)
31 }
32
33 // Queries
34 async timeline (opts) {
35 return this.items.timeline(opts)
36 }
37
38 async bySource (source, opts) {
39 return this.items.bySource(source, opts)
40 }
41
42 async byAuthor (authorId, opts) {
43 return this.items.byAuthor(authorId, opts)
44 }
45
46 async search (query) {
47 return this.items.search(query)
48 }
49
50 async stats () {
51 return this.items.stats()
52 }
53
54 async close () {
55 await this.store.close()
56 }
57}
58
59export { Feed }
hyper-feed/package.json +18 -0
……@@ -0,0 +1,18 @@
1{
2 "name": "hyper-feed",
3 "version": "1.0.0",
4 "description": "P2P social timeline aggregator backed by Hyperbee",
5 "type": "module",
6 "main": "lib/feed.js",
7 "scripts": {
8 "test": "brittle test/*.test.js"
9 },
10 "dependencies": {
11 "hyperbee": "^2.0.0",
12 "corestore": "^6.0.0"
13 },
14 "devDependencies": {
15 "brittle": "^3.0.0"
16 },
17 "license": "MIT"
18}
hyper-feed/test/feed.test.js +596 -0
……@@ -0,0 +1,596 @@
1import test from 'brittle'
2import { Feed } from '../lib/feed.js'
3import { tmpdir } from 'os'
4import { join } from 'path'
5import { randomBytes } from 'crypto'
6
7function tmpStorage () {
8 return join(tmpdir(), 'hyper-feed-test-' + randomBytes(8).toString('hex'))
9}
10
11test('Feed - ready initializes without error', async (t) => {
12 const feed = new Feed(tmpStorage())
13 await feed.ready()
14
15 t.pass('feed ready')
16
17 await feed.close()
18})
19
20test('Feed - add and get item', async (t) => {
21 const feed = new Feed(tmpStorage())
22 await feed.ready()
23
24 const item = await feed.add({
25 source: 'irc',
26 author: 'alice',
27 authorId: 'alice@irc',
28 text: 'Hello from IRC!',
29 type: 'post',
30 tags: ['general']
31 })
32
33 t.ok(item.id)
34 t.is(item.source, 'irc')
35 t.is(item.author, 'alice')
36 t.is(item.authorId, 'alice@irc')
37 t.is(item.text, 'Hello from IRC!')
38 t.is(item.type, 'post')
39 t.alike(item.tags, ['general'])
40 t.ok(item.timestamp > 0)
41
42 const fetched = await feed.get(item.id)
43 t.is(fetched.text, 'Hello from IRC!')
44 t.is(fetched.id, item.id)
45
46 await feed.close()
47})
48
49test('Feed - add with defaults', async (t) => {
50 const feed = new Feed(tmpStorage())
51 await feed.ready()
52
53 const item = await feed.add({ text: 'Minimal' })
54
55 t.is(item.source, 'keet')
56 t.is(item.author, 'unknown')
57 t.is(item.type, 'post')
58 t.alike(item.tags, [])
59 t.is(item.url, null)
60
61 await feed.close()
62})
63
64test('Feed - add with custom id', async (t) => {
65 const feed = new Feed(tmpStorage())
66 await feed.ready()
67
68 const item = await feed.add({ id: 'custom123', text: 'Custom ID' })
69 t.is(item.id, 'custom123')
70
71 const fetched = await feed.get('custom123')
72 t.is(fetched.text, 'Custom ID')
73
74 await feed.close()
75})
76
77test('Feed - get nonexistent returns null', async (t) => {
78 const feed = new Feed(tmpStorage())
79 await feed.ready()
80
81 const result = await feed.get('nonexistent')
82 t.is(result, null)
83
84 await feed.close()
85})
86
87test('Feed - remove item cleans all indexes', async (t) => {
88 const feed = new Feed(tmpStorage())
89 await feed.ready()
90
91 const item = await feed.add({
92 source: 'mastodon',
93 author: 'bob',
94 authorId: 'bob@mastodon',
95 text: 'Remove me',
96 tags: ['test']
97 })
98
99 // Verify it exists in all views
100 t.ok(await feed.get(item.id))
101
102 const tl = await feed.timeline()
103 t.is(tl.length, 1)
104
105 const src = await feed.bySource('mastodon')
106 t.is(src.length, 1)
107
108 const auth = await feed.byAuthor('bob@mastodon')
109 t.is(auth.length, 1)
110
111 // Remove it
112 const ok = await feed.remove(item.id)
113 t.ok(ok)
114
115 // Verify cleaned from all indexes
116 t.is(await feed.get(item.id), null)
117
118 const tl2 = await feed.timeline()
119 t.is(tl2.length, 0)
120
121 const src2 = await feed.bySource('mastodon')
122 t.is(src2.length, 0)
123
124 const auth2 = await feed.byAuthor('bob@mastodon')
125 t.is(auth2.length, 0)
126
127 await feed.close()
128})
129
130test('Feed - remove nonexistent returns false', async (t) => {
131 const feed = new Feed(tmpStorage())
132 await feed.ready()
133
134 const ok = await feed.remove('nonexistent')
135 t.is(ok, false)
136
137 await feed.close()
138})
139
140test('Feed - timeline returns items in order', async (t) => {
141 const feed = new Feed(tmpStorage())
142 await feed.ready()
143
144 const base = Date.now()
145
146 await feed.add({ text: 'First', timestamp: base })
147 await feed.add({ text: 'Second', timestamp: base + 1000 })
148 await feed.add({ text: 'Third', timestamp: base + 2000 })
149
150 const tl = await feed.timeline()
151 t.is(tl.length, 3)
152 t.is(tl[0].text, 'First')
153 t.is(tl[1].text, 'Second')
154 t.is(tl[2].text, 'Third')
155
156 await feed.close()
157})
158
159test('Feed - timeline with limit', async (t) => {
160 const feed = new Feed(tmpStorage())
161 await feed.ready()
162
163 const base = Date.now()
164
165 await feed.add({ text: 'A', timestamp: base })
166 await feed.add({ text: 'B', timestamp: base + 1000 })
167 await feed.add({ text: 'C', timestamp: base + 2000 })
168
169 const tl = await feed.timeline({ limit: 2 })
170 t.is(tl.length, 2)
171 t.is(tl[0].text, 'A')
172 t.is(tl[1].text, 'B')
173
174 await feed.close()
175})
176
177test('Feed - timeline with before', async (t) => {
178 const feed = new Feed(tmpStorage())
179 await feed.ready()
180
181 const base = Date.now()
182
183 await feed.add({ text: 'A', timestamp: base })
184 await feed.add({ text: 'B', timestamp: base + 1000 })
185 await feed.add({ text: 'C', timestamp: base + 2000 })
186
187 const tl = await feed.timeline({ before: base + 1500 })
188 t.is(tl.length, 2)
189 t.is(tl[0].text, 'A')
190 t.is(tl[1].text, 'B')
191
192 await feed.close()
193})
194
195test('Feed - timeline with after', async (t) => {
196 const feed = new Feed(tmpStorage())
197 await feed.ready()
198
199 const base = Date.now()
200
201 await feed.add({ text: 'A', timestamp: base })
202 await feed.add({ text: 'B', timestamp: base + 1000 })
203 await feed.add({ text: 'C', timestamp: base + 2000 })
204
205 const tl = await feed.timeline({ after: base + 500 })
206 t.is(tl.length, 2)
207 t.is(tl[0].text, 'B')
208 t.is(tl[1].text, 'C')
209
210 await feed.close()
211})
212
213test('Feed - timeline with before and after', async (t) => {
214 const feed = new Feed(tmpStorage())
215 await feed.ready()
216
217 const base = Date.now()
218
219 await feed.add({ text: 'A', timestamp: base })
220 await feed.add({ text: 'B', timestamp: base + 1000 })
221 await feed.add({ text: 'C', timestamp: base + 2000 })
222 await feed.add({ text: 'D', timestamp: base + 3000 })
223
224 const tl = await feed.timeline({ after: base + 500, before: base + 2500 })
225 t.is(tl.length, 2)
226 t.is(tl[0].text, 'B')
227 t.is(tl[1].text, 'C')
228
229 await feed.close()
230})
231
232test('Feed - timeline filtered by source', async (t) => {
233 const feed = new Feed(tmpStorage())
234 await feed.ready()
235
236 const base = Date.now()
237
238 await feed.add({ text: 'IRC msg', source: 'irc', timestamp: base })
239 await feed.add({ text: 'Keet msg', source: 'keet', timestamp: base + 1000 })
240 await feed.add({ text: 'IRC msg 2', source: 'irc', timestamp: base + 2000 })
241
242 const tl = await feed.timeline({ source: 'irc' })
243 t.is(tl.length, 2)
244 t.is(tl[0].text, 'IRC msg')
245 t.is(tl[1].text, 'IRC msg 2')
246
247 await feed.close()
248})
249
250test('Feed - timeline filtered by type', async (t) => {
251 const feed = new Feed(tmpStorage())
252 await feed.ready()
253
254 const base = Date.now()
255
256 await feed.add({ text: 'Post', type: 'post', timestamp: base })
257 await feed.add({ text: 'Reply', type: 'reply', timestamp: base + 1000 })
258 await feed.add({ text: 'Boost', type: 'boost', timestamp: base + 2000 })
259
260 const tl = await feed.timeline({ type: 'reply' })
261 t.is(tl.length, 1)
262 t.is(tl[0].text, 'Reply')
263
264 await feed.close()
265})
266
267test('Feed - bySource returns items from one source', async (t) => {
268 const feed = new Feed(tmpStorage())
269 await feed.ready()
270
271 const base = Date.now()
272
273 await feed.add({ text: 'IRC 1', source: 'irc', timestamp: base })
274 await feed.add({ text: 'Keet 1', source: 'keet', timestamp: base + 1000 })
275 await feed.add({ text: 'IRC 2', source: 'irc', timestamp: base + 2000 })
276 await feed.add({ text: 'Discord 1', source: 'discord', timestamp: base + 3000 })
277
278 const irc = await feed.bySource('irc')
279 t.is(irc.length, 2)
280 t.is(irc[0].text, 'IRC 1')
281 t.is(irc[1].text, 'IRC 2')
282
283 const discord = await feed.bySource('discord')
284 t.is(discord.length, 1)
285 t.is(discord[0].text, 'Discord 1')
286
287 await feed.close()
288})
289
290test('Feed - bySource with limit', async (t) => {
291 const feed = new Feed(tmpStorage())
292 await feed.ready()
293
294 const base = Date.now()
295
296 await feed.add({ text: 'A', source: 'irc', timestamp: base })
297 await feed.add({ text: 'B', source: 'irc', timestamp: base + 1000 })
298 await feed.add({ text: 'C', source: 'irc', timestamp: base + 2000 })
299
300 const items = await feed.bySource('irc', { limit: 2 })
301 t.is(items.length, 2)
302 t.is(items[0].text, 'A')
303 t.is(items[1].text, 'B')
304
305 await feed.close()
306})
307
308test('Feed - bySource with before', async (t) => {
309 const feed = new Feed(tmpStorage())
310 await feed.ready()
311
312 const base = Date.now()
313
314 await feed.add({ text: 'A', source: 'irc', timestamp: base })
315 await feed.add({ text: 'B', source: 'irc', timestamp: base + 1000 })
316 await feed.add({ text: 'C', source: 'irc', timestamp: base + 2000 })
317
318 const items = await feed.bySource('irc', { before: base + 1500 })
319 t.is(items.length, 2)
320 t.is(items[0].text, 'A')
321 t.is(items[1].text, 'B')
322
323 await feed.close()
324})
325
326test('Feed - byAuthor returns items from one author', async (t) => {
327 const feed = new Feed(tmpStorage())
328 await feed.ready()
329
330 const base = Date.now()
331
332 await feed.add({ text: 'Alice 1', author: 'Alice', authorId: 'alice@irc', timestamp: base })
333 await feed.add({ text: 'Bob 1', author: 'Bob', authorId: 'bob@keet', timestamp: base + 1000 })
334 await feed.add({ text: 'Alice 2', author: 'Alice', authorId: 'alice@irc', timestamp: base + 2000 })
335
336 const alice = await feed.byAuthor('alice@irc')
337 t.is(alice.length, 2)
338 t.is(alice[0].text, 'Alice 1')
339 t.is(alice[1].text, 'Alice 2')
340
341 const bob = await feed.byAuthor('bob@keet')
342 t.is(bob.length, 1)
343 t.is(bob[0].text, 'Bob 1')
344
345 await feed.close()
346})
347
348test('Feed - byAuthor with limit', async (t) => {
349 const feed = new Feed(tmpStorage())
350 await feed.ready()
351
352 const base = Date.now()
353
354 await feed.add({ text: 'A', authorId: 'alice', timestamp: base })
355 await feed.add({ text: 'B', authorId: 'alice', timestamp: base + 1000 })
356 await feed.add({ text: 'C', authorId: 'alice', timestamp: base + 2000 })
357
358 const items = await feed.byAuthor('alice', { limit: 1 })
359 t.is(items.length, 1)
360 t.is(items[0].text, 'A')
361
362 await feed.close()
363})
364
365test('Feed - search by text', async (t) => {
366 const feed = new Feed(tmpStorage())
367 await feed.ready()
368
369 await feed.add({ text: 'Hello world from IRC' })
370 await feed.add({ text: 'Goodbye moon from Keet' })
371 await feed.add({ text: 'Hello again from Mastodon' })
372
373 const results = await feed.search('hello')
374 t.is(results.length, 2)
375
376 await feed.close()
377})
378
379test('Feed - search by tags', async (t) => {
380 const feed = new Feed(tmpStorage())
381 await feed.ready()
382
383 await feed.add({ text: 'Post about cats', tags: ['animals', 'cats'] })
384 await feed.add({ text: 'Post about dogs', tags: ['animals', 'dogs'] })
385 await feed.add({ text: 'Post about code', tags: ['programming'] })
386
387 const results = await feed.search('cats')
388 t.is(results.length, 1) // "Post about cats" matches text, tag also matches but same item
389
390 const results2 = await feed.search('programming')
391 t.is(results2.length, 1)
392
393 await feed.close()
394})
395
396test('Feed - search case insensitive', async (t) => {
397 const feed = new Feed(tmpStorage())
398 await feed.ready()
399
400 await feed.add({ text: 'HELLO WORLD' })
401 await feed.add({ text: 'hello world' })
402 await feed.add({ text: 'Hello World' })
403
404 const results = await feed.search('hello')
405 t.is(results.length, 3)
406
407 await feed.close()
408})
409
410test('Feed - search no matches', async (t) => {
411 const feed = new Feed(tmpStorage())
412 await feed.ready()
413
414 await feed.add({ text: 'Something else' })
415
416 const results = await feed.search('nonexistent')
417 t.is(results.length, 0)
418
419 await feed.close()
420})
421
422test('Feed - stats counts by source and type', async (t) => {
423 const feed = new Feed(tmpStorage())
424 await feed.ready()
425
426 await feed.add({ text: 'A', source: 'irc', type: 'post' })
427 await feed.add({ text: 'B', source: 'irc', type: 'reply' })
428 await feed.add({ text: 'C', source: 'keet', type: 'post' })
429 await feed.add({ text: 'D', source: 'mastodon', type: 'boost' })
430 await feed.add({ text: 'E', source: 'irc', type: 'post' })
431
432 const stats = await feed.stats()
433 t.is(stats.total, 5)
434 t.is(stats.bySource.irc, 3)
435 t.is(stats.bySource.keet, 1)
436 t.is(stats.bySource.mastodon, 1)
437 t.is(stats.byType.post, 3)
438 t.is(stats.byType.reply, 1)
439 t.is(stats.byType.boost, 1)
440
441 await feed.close()
442})
443
444test('Feed - stats empty', async (t) => {
445 const feed = new Feed(tmpStorage())
446 await feed.ready()
447
448 const stats = await feed.stats()
449 t.is(stats.total, 0)
450 t.alike(stats.bySource, {})
451 t.alike(stats.byType, {})
452
453 await feed.close()
454})
455
456test('Feed - multiple sources interleaved in timeline', async (t) => {
457 const feed = new Feed(tmpStorage())
458 await feed.ready()
459
460 const base = Date.now()
461
462 await feed.add({ text: 'IRC first', source: 'irc', timestamp: base })
463 await feed.add({ text: 'Keet first', source: 'keet', timestamp: base + 100 })
464 await feed.add({ text: 'Discord first', source: 'discord', timestamp: base + 200 })
465 await feed.add({ text: 'IRC second', source: 'irc', timestamp: base + 300 })
466 await feed.add({ text: 'Mastodon first', source: 'mastodon', timestamp: base + 400 })
467 await feed.add({ text: 'Keet second', source: 'keet', timestamp: base + 500 })
468 await feed.add({ text: 'Email first', source: 'email', timestamp: base + 600 })
469
470 // Full timeline is interleaved across all sources
471 const tl = await feed.timeline()
472 t.is(tl.length, 7)
473 t.is(tl[0].text, 'IRC first')
474 t.is(tl[0].source, 'irc')
475 t.is(tl[1].source, 'keet')
476 t.is(tl[2].source, 'discord')
477 t.is(tl[3].source, 'irc')
478 t.is(tl[4].source, 'mastodon')
479 t.is(tl[5].source, 'keet')
480 t.is(tl[6].source, 'email')
481
482 // Filter just irc from the interleaved timeline
483 const irc = await feed.timeline({ source: 'irc' })
484 t.is(irc.length, 2)
485 t.is(irc[0].text, 'IRC first')
486 t.is(irc[1].text, 'IRC second')
487
488 await feed.close()
489})
490
491test('Feed - item with url and metadata', async (t) => {
492 const feed = new Feed(tmpStorage())
493 await feed.ready()
494
495 const item = await feed.add({
496 source: 'mastodon',
497 author: 'user@instance.social',
498 authorId: 'user@instance.social',
499 text: 'Check out this link!',
500 url: 'https://instance.social/@user/123',
501 metadata: {
502 visibility: 'public',
503 reblogsCount: 42,
504 favouritesCount: 7
505 }
506 })
507
508 t.is(item.url, 'https://instance.social/@user/123')
509 t.is(item.metadata.visibility, 'public')
510 t.is(item.metadata.reblogsCount, 42)
511
512 const fetched = await feed.get(item.id)
513 t.is(fetched.url, 'https://instance.social/@user/123')
514 t.is(fetched.metadata.favouritesCount, 7)
515
516 await feed.close()
517})
518
519test('Feed - persistence across close/reopen', async (t) => {
520 const storage = tmpStorage()
521
522 // Create and populate
523 const f1 = new Feed(storage)
524 await f1.ready()
525
526 await f1.add({ text: 'Persistent item', source: 'irc', authorId: 'alice' })
527 await f1.close()
528
529 // Reopen and verify
530 const f2 = new Feed(storage)
531 await f2.ready()
532
533 const tl = await f2.timeline()
534 t.is(tl.length, 1)
535 t.is(tl[0].text, 'Persistent item')
536
537 const irc = await f2.bySource('irc')
538 t.is(irc.length, 1)
539
540 const alice = await f2.byAuthor('alice')
541 t.is(alice.length, 1)
542
543 await f2.close()
544})
545
546test('Feed - remove then re-add does not leave ghosts', async (t) => {
547 const feed = new Feed(tmpStorage())
548 await feed.ready()
549
550 const base = Date.now()
551
552 const item = await feed.add({ text: 'Ghost?', source: 'keet', authorId: 'ghost', timestamp: base })
553 await feed.remove(item.id)
554
555 // Add a new item — timeline should only have the new one
556 await feed.add({ text: 'Not a ghost', source: 'keet', authorId: 'ghost', timestamp: base + 1000 })
557
558 const tl = await feed.timeline()
559 t.is(tl.length, 1)
560 t.is(tl[0].text, 'Not a ghost')
561
562 const src = await feed.bySource('keet')
563 t.is(src.length, 1)
564
565 const auth = await feed.byAuthor('ghost')
566 t.is(auth.length, 1)
567
568 await feed.close()
569})
570
571test('Feed - many items pagination', async (t) => {
572 const feed = new Feed(tmpStorage())
573 await feed.ready()
574
575 const base = Date.now()
576
577 // Add 20 items
578 for (let i = 0; i < 20; i++) {
579 await feed.add({ text: `Item ${i}`, timestamp: base + i * 100 })
580 }
581
582 // Page 1: first 5
583 const page1 = await feed.timeline({ limit: 5 })
584 t.is(page1.length, 5)
585 t.is(page1[0].text, 'Item 0')
586 t.is(page1[4].text, 'Item 4')
587
588 // Page 2: next 5 after page 1
589 const lastTs = page1[page1.length - 1].timestamp
590 const page2 = await feed.timeline({ limit: 5, after: lastTs })
591 t.is(page2.length, 5)
592 t.is(page2[0].text, 'Item 5')
593 t.is(page2[4].text, 'Item 9')
594
595 await feed.close()
596})
hyper-social/index.js +8 -0
……@@ -0,0 +1,8 @@
1import { Hub } from './lib/hub.js'
2import { ServiceRegistry } from './lib/service-registry.js'
3import { MessageBus } from './lib/message-bus.js'
4import { RoomMap } from './lib/room-map.js'
5import { IdentityGraph } from './lib/identity-graph.js'
6import { Config } from './lib/config.js'
7
8export { Hub, ServiceRegistry, MessageBus, RoomMap, IdentityGraph, Config }
hyper-social/lib/config.js +89 -0
……@@ -0,0 +1,89 @@
1// Config — persistent configuration store backed by Hyperbee
2// Stores bridge configs, room settings, preferences
3
4class Config {
5 constructor (bee) {
6 this.bee = bee
7 }
8
9 _key (section, name) {
10 return `config/${section}/${name}`
11 }
12
13 async set (section, name, value) {
14 await this.bee.put(this._key(section, name), {
15 section,
16 name,
17 value,
18 updated: Date.now()
19 })
20 }
21
22 async get (section, name) {
23 const node = await this.bee.get(this._key(section, name))
24 return node ? node.value.value : undefined
25 }
26
27 async del (section, name) {
28 await this.bee.del(this._key(section, name))
29 }
30
31 // Get all values in a section
32 async getSection (section) {
33 const entries = {}
34
35 for await (const node of this.bee.createReadStream({
36 gte: `config/${section}/`,
37 lte: `config/${section}/~`
38 })) {
39 if (node.value) {
40 entries[node.value.name] = node.value.value
41 }
42 }
43
44 return entries
45 }
46
47 // List all sections
48 async listSections () {
49 const sections = new Set()
50
51 for await (const node of this.bee.createReadStream({
52 gte: 'config/',
53 lte: 'config/~'
54 })) {
55 if (node.value) {
56 sections.add(node.value.section)
57 }
58 }
59
60 return [...sections]
61 }
62
63 // Bulk set (for importing config)
64 async setAll (section, entries) {
65 for (const [name, value] of Object.entries(entries)) {
66 await this.set(section, name, value)
67 }
68 }
69
70 // Export all config as JSON
71 async exportAll () {
72 const data = {}
73
74 for await (const node of this.bee.createReadStream({
75 gte: 'config/',
76 lte: 'config/~'
77 })) {
78 if (node.value) {
79 const { section, name, value } = node.value
80 if (!data[section]) data[section] = {}
81 data[section][name] = value
82 }
83 }
84
85 return data
86 }
87}
88
89export { Config }
hyper-social/lib/hub.js +160 -0
……@@ -0,0 +1,160 @@
1import Corestore from 'corestore'
2import Hyperbee from 'hyperbee'
3import { ServiceRegistry } from './service-registry.js'
4import { MessageBus } from './message-bus.js'
5import { RoomMap } from './room-map.js'
6import { IdentityGraph } from './identity-graph.js'
7import { Config } from './config.js'
8
9class Hub {
10 constructor (storage, opts = {}) {
11 this.store = new Corestore(storage)
12 this.bee = new Hyperbee(this.store.get({ name: 'hub' }), {
13 keyEncoding: 'utf-8',
14 valueEncoding: 'json'
15 })
16
17 this.services = new ServiceRegistry()
18 this.bus = new MessageBus({ ttl: opts.dedupTtl })
19 this.rooms = new RoomMap(this.bee)
20 this.identities = new IdentityGraph(this.bee)
21 this.config = new Config(this.bee)
22 this.onLog = opts.onLog || (() => {})
23
24 this._started = false
25 }
26
27 async ready () {
28 await this.store.ready()
29 await this.bee.ready()
30 this.onLog('Hub ready')
31 }
32
33 // Register and optionally start a service
34 async addService (id, service, opts = {}) {
35 const entry = this.services.register(id, service)
36 this.onLog(`Registered service: ${id} (${service.type || 'unknown'})`)
37
38 // Wire the service into the message bus
39 if (typeof service.onBusMessage === 'function') {
40 service._busSubscription = this.bus.subscribe(id, (msg) => {
41 service.onBusMessage(msg)
42 })
43 }
44
45 // If the service can emit messages, connect it to the bus
46 if (typeof service.setBus === 'function') {
47 service.setBus(this.bus, id)
48 }
49
50 // Link to rooms if specified
51 if (opts.roomId) {
52 await this.rooms.link(opts.roomId, id, {
53 platform: service.platform || '',
54 channel: opts.channel || '',
55 direction: opts.direction || 'bidirectional',
56 roomName: opts.roomName || ''
57 })
58 }
59
60 // Auto-start if requested
61 if (opts.autoStart) {
62 await this.services.start(id)
63 this._started = true
64 }
65
66 return entry
67 }
68
69 // Remove a service
70 async removeService (id) {
71 const entry = this.services.get(id)
72 if (!entry) return false
73
74 // Unsubscribe from bus
75 if (entry.service._busSubscription) {
76 this.bus.unsubscribe(entry.service._busSubscription)
77 }
78
79 // Unlink from all rooms
80 const rooms = await this.rooms.getRooms(id)
81 for (const room of rooms) {
82 await this.rooms.unlink(room.roomId, id)
83 }
84
85 await this.services.unregister(id)
86 this.onLog(`Removed service: ${id}`)
87 return true
88 }
89
90 // Start all services
91 async startAll () {
92 const results = await this.services.startAll()
93 this._started = true
94
95 const ok = results.filter(r => r.ok).length
96 const failed = results.filter(r => !r.ok).length
97 this.onLog(`Started ${ok} services (${failed} failed)`)
98
99 return results
100 }
101
102 // Stop all services
103 async stopAll () {
104 const results = await this.services.stopAll()
105 this._started = false
106 this.onLog('All services stopped')
107 return results
108 }
109
110 // Get full system status
111 status () {
112 return {
113 started: this._started,
114 services: this.services.status(),
115 bus: this.bus.stats
116 }
117 }
118
119 // Route a message through the hub (manually inject)
120 route (msg) {
121 return this.bus.publish(msg)
122 }
123
124 // Get the graph view — rooms with their linked services
125 async graph () {
126 const mappings = await this.rooms.listAll()
127 const serviceList = this.services.list()
128 const roomGroups = {}
129
130 for (const mapping of mappings) {
131 if (!roomGroups[mapping.roomId]) {
132 roomGroups[mapping.roomId] = {
133 roomId: mapping.roomId,
134 services: []
135 }
136 }
137
138 const service = serviceList.find(s => s.id === mapping.serviceId)
139 roomGroups[mapping.roomId].services.push({
140 id: mapping.serviceId,
141 platform: mapping.platform,
142 channel: mapping.channel,
143 direction: mapping.direction,
144 enabled: mapping.enabled,
145 status: service ? service.status : 'unknown'
146 })
147 }
148
149 return Object.values(roomGroups)
150 }
151
152 async close () {
153 await this.stopAll()
154 this.bus.removeAllListeners()
155 await this.store.close()
156 this.onLog('Hub closed')
157 }
158}
159
160export { Hub }
hyper-social/lib/identity-graph.js +177 -0
……@@ -0,0 +1,177 @@
1// IdentityGraph — links identities across platforms
2// alice@mastodon.social = alice on IRC = alice-key in Keet
3// Forms a graph where each node is a platform identity
4// and edges link the same person across platforms
5
6class IdentityGraph {
7 constructor (bee) {
8 this.bee = bee
9 }
10
11 _identityKey (platform, handle) {
12 return `identity/${platform}/${handle}`
13 }
14
15 _aliasKey (alias) {
16 return `alias/${alias}`
17 }
18
19 // Create or update an identity
20 async addIdentity (platform, handle, opts = {}) {
21 const key = this._identityKey(platform, handle)
22 const existing = await this.bee.get(key)
23
24 const identity = {
25 platform,
26 handle,
27 alias: opts.alias || existing?.value?.alias || '',
28 displayName: opts.displayName || existing?.value?.displayName || handle,
29 metadata: { ...(existing?.value?.metadata || {}), ...(opts.metadata || {}) },
30 created: existing?.value?.created || Date.now(),
31 updated: Date.now()
32 }
33
34 await this.bee.put(key, identity)
35
36 // If alias is set, maintain reverse index
37 if (identity.alias) {
38 const aliasEntry = await this.bee.get(this._aliasKey(identity.alias)) || { value: { identities: [] } }
39 const identities = aliasEntry.value.identities || []
40 const ref = `${platform}:${handle}`
41 if (!identities.includes(ref)) {
42 identities.push(ref)
43 }
44 await this.bee.put(this._aliasKey(identity.alias), {
45 alias: identity.alias,
46 identities,
47 updated: Date.now()
48 })
49 }
50
51 return identity
52 }
53
54 // Get an identity by platform + handle
55 async getIdentity (platform, handle) {
56 const node = await this.bee.get(this._identityKey(platform, handle))
57 return node ? node.value : null
58 }
59
60 // Link two identities as the same person
61 async link (alias, platform, handle) {
62 // Update the identity with the alias
63 await this.addIdentity(platform, handle, { alias })
64 }
65
66 // Get all identities for an alias (the same person)
67 async getLinked (alias) {
68 const node = await this.bee.get(this._aliasKey(alias))
69 if (!node) return []
70
71 const results = []
72 for (const ref of node.value.identities || []) {
73 const [platform, handle] = ref.split(':', 2)
74 const identity = await this.getIdentity(platform, handle)
75 if (identity) results.push(identity)
76 }
77
78 return results
79 }
80
81 // Find who someone is on another platform
82 async resolve (fromPlatform, fromHandle, toPlatform) {
83 const identity = await this.getIdentity(fromPlatform, fromHandle)
84 if (!identity || !identity.alias) return null
85
86 const linked = await this.getLinked(identity.alias)
87 return linked.find(i => i.platform === toPlatform) || null
88 }
89
90 // List all identities for a platform
91 async listPlatform (platform) {
92 const identities = []
93
94 for await (const node of this.bee.createReadStream({
95 gte: `identity/${platform}/`,
96 lte: `identity/${platform}/~`
97 })) {
98 if (node.value) identities.push(node.value)
99 }
100
101 return identities
102 }
103
104 // List all aliases (unique people)
105 async listAliases () {
106 const aliases = []
107
108 for await (const node of this.bee.createReadStream({
109 gte: 'alias/',
110 lte: 'alias/~'
111 })) {
112 if (node.value) aliases.push(node.value)
113 }
114
115 return aliases
116 }
117
118 // Remove an identity
119 async removeIdentity (platform, handle) {
120 const identity = await this.getIdentity(platform, handle)
121 if (!identity) return false
122
123 await this.bee.del(this._identityKey(platform, handle))
124
125 // Clean up alias index
126 if (identity.alias) {
127 const aliasNode = await this.bee.get(this._aliasKey(identity.alias))
128 if (aliasNode) {
129 const ref = `${platform}:${handle}`
130 const remaining = (aliasNode.value.identities || []).filter(r => r !== ref)
131 if (remaining.length === 0) {
132 await this.bee.del(this._aliasKey(identity.alias))
133 } else {
134 await this.bee.put(this._aliasKey(identity.alias), {
135 ...aliasNode.value,
136 identities: remaining,
137 updated: Date.now()
138 })
139 }
140 }
141 }
142
143 return true
144 }
145
146 // Stats
147 async stats () {
148 let identityCount = 0
149 let aliasCount = 0
150 const platforms = new Set()
151
152 for await (const node of this.bee.createReadStream({
153 gte: 'identity/',
154 lte: 'identity/~'
155 })) {
156 if (node.value) {
157 identityCount++
158 platforms.add(node.value.platform)
159 }
160 }
161
162 for await (const node of this.bee.createReadStream({
163 gte: 'alias/',
164 lte: 'alias/~'
165 })) {
166 if (node.value) aliasCount++
167 }
168
169 return {
170 identities: identityCount,
171 aliases: aliasCount,
172 platforms: [...platforms]
173 }
174 }
175}
176
177export { IdentityGraph }
hyper-social/lib/message-bus.js +97 -0
……@@ -0,0 +1,97 @@
1import { EventEmitter } from 'events'
2import { createHash } from 'crypto'
3
4class MessageBus extends EventEmitter {
5 constructor (opts = {}) {
6 super()
7 this._seen = new Map()
8 this._ttl = opts.ttl || 60000
9 this._maxSeen = opts.maxSeen || 50000
10 this._stats = { routed: 0, deduped: 0, errors: 0 }
11 }
12
13 _hash (msg) {
14 return createHash('sha256')
15 .update(msg.source || '')
16 .update(msg.author || '')
17 .update(msg.text || '')
18 .digest('hex')
19 .slice(0, 16)
20 }
21
22 _isDuplicate (hash) {
23 const now = Date.now()
24
25 if (this._seen.size > this._maxSeen) {
26 for (const [k, ts] of this._seen) {
27 if (now - ts > this._ttl) this._seen.delete(k)
28 }
29 }
30
31 if (this._seen.has(hash)) {
32 const ts = this._seen.get(hash)
33 if (now - ts < this._ttl) return true
34 }
35
36 this._seen.set(hash, now)
37 return false
38 }
39
40 // Publish a message to all subscribers except the source
41 publish (msg) {
42 if (!msg || !msg.source) return false
43
44 const hash = this._hash(msg)
45 if (this._isDuplicate(hash)) {
46 this._stats.deduped++
47 return false
48 }
49
50 this._stats.routed++
51
52 // Emit to all listeners, each bridge/service filters by interest
53 this.emit('message', msg)
54
55 // Also emit typed events for targeted listening
56 if (msg.type) {
57 this.emit(`message:${msg.type}`, msg)
58 }
59
60 // Platform-specific channel
61 if (msg.platform) {
62 this.emit(`platform:${msg.platform}`, msg)
63 }
64
65 return true
66 }
67
68 // Subscribe a handler that skips messages from its own source
69 subscribe (sourceId, handler) {
70 const wrapper = (msg) => {
71 if (msg.source === sourceId) return // don't echo back to origin
72 try {
73 handler(msg)
74 } catch (err) {
75 this._stats.errors++
76 this.emit('error', { source: sourceId, error: err.message })
77 }
78 }
79 this.on('message', wrapper)
80 return wrapper // return ref for unsubscribe
81 }
82
83 unsubscribe (wrapper) {
84 this.removeListener('message', wrapper)
85 }
86
87 get stats () {
88 return { ...this._stats, seen: this._seen.size }
89 }
90
91 reset () {
92 this._seen.clear()
93 this._stats = { routed: 0, deduped: 0, errors: 0 }
94 }
95}
96
97export { MessageBus }
hyper-social/lib/room-map.js +139 -0
……@@ -0,0 +1,139 @@
1// RoomMap — maps Keet rooms to external platform channels/bridges
2// A room can have multiple bridges (IRC + Discord + Mastodon all on one room)
3// A bridge can serve multiple rooms (one IRC bot covering several rooms)
4
5class RoomMap {
6 constructor (bee) {
7 this.bee = bee
8 }
9
10 _key (roomId, serviceId) {
11 return `room-map/${roomId}/${serviceId}`
12 }
13
14 _roomKey (roomId) {
15 return `room/${roomId}`
16 }
17
18 // Link a service (bridge/panel/daemon) to a room
19 async link (roomId, serviceId, config = {}) {
20 const entry = {
21 roomId,
22 serviceId,
23 platform: config.platform || '',
24 channel: config.channel || '',
25 direction: config.direction || 'bidirectional', // in, out, bidirectional
26 enabled: config.enabled !== false,
27 linked: Date.now()
28 }
29
30 await this.bee.put(this._key(roomId, serviceId), entry)
31
32 // Also store room metadata if provided
33 if (config.roomName) {
34 const existing = await this.getRoom(roomId)
35 if (!existing) {
36 await this.bee.put(this._roomKey(roomId), {
37 id: roomId,
38 name: config.roomName,
39 created: Date.now()
40 })
41 }
42 }
43
44 return entry
45 }
46
47 // Unlink a service from a room
48 async unlink (roomId, serviceId) {
49 const key = this._key(roomId, serviceId)
50 const node = await this.bee.get(key)
51 if (!node) return false
52
53 await this.bee.del(key)
54 return true
55 }
56
57 // Get all services linked to a room
58 async getServices (roomId) {
59 const services = []
60
61 for await (const node of this.bee.createReadStream({
62 gte: `room-map/${roomId}/`,
63 lte: `room-map/${roomId}/~`
64 })) {
65 if (node.value) services.push(node.value)
66 }
67
68 return services
69 }
70
71 // Get all rooms a service is linked to
72 async getRooms (serviceId) {
73 const rooms = []
74
75 for await (const node of this.bee.createReadStream({
76 gte: 'room-map/',
77 lte: 'room-map/~'
78 })) {
79 if (node.value && node.value.serviceId === serviceId) {
80 rooms.push(node.value)
81 }
82 }
83
84 return rooms
85 }
86
87 // Get room metadata
88 async getRoom (roomId) {
89 const node = await this.bee.get(this._roomKey(roomId))
90 return node ? node.value : null
91 }
92
93 // List all room mappings
94 async listAll () {
95 const mappings = []
96
97 for await (const node of this.bee.createReadStream({
98 gte: 'room-map/',
99 lte: 'room-map/~'
100 })) {
101 if (node.value) mappings.push(node.value)
102 }
103
104 return mappings
105 }
106
107 // List all known rooms
108 async listRooms () {
109 const rooms = []
110
111 for await (const node of this.bee.createReadStream({
112 gte: 'room/',
113 lte: 'room/~'
114 })) {
115 if (node.value) rooms.push(node.value)
116 }
117
118 return rooms
119 }
120
121 // Check if a specific service is linked to a room
122 async isLinked (roomId, serviceId) {
123 const node = await this.bee.get(this._key(roomId, serviceId))
124 return !!node
125 }
126
127 // Enable/disable a link
128 async setEnabled (roomId, serviceId, enabled) {
129 const key = this._key(roomId, serviceId)
130 const node = await this.bee.get(key)
131 if (!node) throw new Error(`Link not found: ${roomId} → ${serviceId}`)
132
133 const entry = { ...node.value, enabled }
134 await this.bee.put(key, entry)
135 return entry
136 }
137}
138
139export { RoomMap }
hyper-social/lib/service-registry.js +156 -0
……@@ -0,0 +1,156 @@
1class ServiceRegistry {
2 constructor () {
3 this._services = new Map()
4 }
5
6 // Register a service (bridge, panel, or daemon)
7 register (id, service) {
8 if (this._services.has(id)) {
9 throw new Error(`Service already registered: ${id}`)
10 }
11
12 const entry = {
13 id,
14 type: service.type || 'unknown', // bridge, panel, daemon
15 name: service.name || id,
16 service,
17 status: 'registered',
18 startedAt: null,
19 error: null
20 }
21
22 this._services.set(id, entry)
23 return entry
24 }
25
26 // Start a service
27 async start (id) {
28 const entry = this._services.get(id)
29 if (!entry) throw new Error(`Service not found: ${id}`)
30 if (entry.status === 'running') return entry
31
32 try {
33 entry.status = 'starting'
34 if (typeof entry.service.start === 'function') {
35 await entry.service.start()
36 }
37 entry.status = 'running'
38 entry.startedAt = Date.now()
39 entry.error = null
40 } catch (err) {
41 entry.status = 'error'
42 entry.error = err.message
43 throw err
44 }
45
46 return entry
47 }
48
49 // Stop a service
50 async stop (id) {
51 const entry = this._services.get(id)
52 if (!entry) throw new Error(`Service not found: ${id}`)
53 if (entry.status === 'stopped') return entry
54
55 try {
56 entry.status = 'stopping'
57 if (typeof entry.service.stop === 'function') {
58 await entry.service.stop()
59 } else if (typeof entry.service.close === 'function') {
60 await entry.service.close()
61 }
62 entry.status = 'stopped'
63 } catch (err) {
64 entry.status = 'error'
65 entry.error = err.message
66 }
67
68 return entry
69 }
70
71 // Unregister a service (stops it first if running)
72 async unregister (id) {
73 const entry = this._services.get(id)
74 if (!entry) return false
75
76 if (entry.status === 'running') {
77 await this.stop(id)
78 }
79
80 this._services.delete(id)
81 return true
82 }
83
84 // Get a service entry
85 get (id) {
86 return this._services.get(id) || null
87 }
88
89 // List all services
90 list (filter) {
91 const entries = [...this._services.values()]
92
93 if (filter?.type) {
94 return entries.filter(e => e.type === filter.type)
95 }
96 if (filter?.status) {
97 return entries.filter(e => e.status === filter.status)
98 }
99
100 return entries
101 }
102
103 // Get service status summary
104 status () {
105 const entries = [...this._services.values()]
106 const byType = {}
107 const byStatus = {}
108
109 for (const entry of entries) {
110 byType[entry.type] = (byType[entry.type] || 0) + 1
111 byStatus[entry.status] = (byStatus[entry.status] || 0) + 1
112 }
113
114 return {
115 total: entries.length,
116 byType,
117 byStatus
118 }
119 }
120
121 // Start all registered services
122 async startAll () {
123 const results = []
124 for (const [id] of this._services) {
125 try {
126 await this.start(id)
127 results.push({ id, ok: true })
128 } catch (err) {
129 results.push({ id, ok: false, error: err.message })
130 }
131 }
132 return results
133 }
134
135 // Stop all running services
136 async stopAll () {
137 const results = []
138 for (const [id, entry] of this._services) {
139 if (entry.status === 'running') {
140 try {
141 await this.stop(id)
142 results.push({ id, ok: true })
143 } catch (err) {
144 results.push({ id, ok: false, error: err.message })
145 }
146 }
147 }
148 return results
149 }
150
151 get count () {
152 return this._services.size
153 }
154}
155
156export { ServiceRegistry }
hyper-social/package-lock.json +1458 -0
……@@ -0,0 +1,1458 @@
1{
2 "name": "hyper-social",
3 "version": "1.0.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "hyper-social",
9 "version": "1.0.0",
10 "license": "MIT",
11 "dependencies": {
12 "b4a": "^1.0.0",
13 "bridge-envelope": "file:../bridge-envelope",
14 "corestore": "^6.0.0",
15 "hyperbee": "^2.0.0"
16 },
17 "devDependencies": {
18 "brittle": "^3.0.0"
19 }
20 },
21 "../bridge-envelope": {
22 "version": "1.0.0",
23 "license": "MIT",
24 "dependencies": {
25 "b4a": "^1.0.0",
26 "compact-encoding": "^2.0.0"
27 },
28 "devDependencies": {
29 "brittle": "^3.0.0"
30 }
31 },
32 "node_modules/@hyperswarm/secret-stream": {
33 "version": "6.9.1",
34 "resolved": "https://registry.npmjs.org/@hyperswarm/secret-stream/-/secret-stream-6.9.1.tgz",
35 "integrity": "sha512-xb0S5y3YJwBakD77JOGBHlBxdp63mHClZoXBYoLv+9wH8e054ESKlmQptWqjJK5dv5VMUIVYOJB4MaOpB0JdGw==",
36 "license": "Apache-2.0",
37 "dependencies": {
38 "b4a": "^1.1.0",
39 "hypercore-crypto": "^3.3.1",
40 "noise-curve-ed": "^2.0.1",
41 "noise-handshake": "^4.0.0",
42 "sodium-secretstream": "^1.1.0",
43 "sodium-universal": "^5.0.0",
44 "streamx": "^2.14.0",
45 "timeout-refresh": "^2.0.0",
46 "unslab": "^1.3.0"
47 }
48 },
49 "node_modules/@hyperswarm/secret-stream/node_modules/sodium-native": {
50 "version": "5.0.10",
51 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
52 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
53 "license": "MIT",
54 "dependencies": {
55 "require-addon": "^1.1.0",
56 "which-runtime": "^1.2.1"
57 },
58 "engines": {
59 "bare": ">=1.16.0"
60 }
61 },
62 "node_modules/@hyperswarm/secret-stream/node_modules/sodium-universal": {
63 "version": "5.0.1",
64 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
65 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
66 "license": "MIT",
67 "dependencies": {
68 "sodium-native": "^5.0.1"
69 },
70 "peerDependencies": {
71 "sodium-javascript": "~0.8.0"
72 },
73 "peerDependenciesMeta": {
74 "sodium-javascript": {
75 "optional": true
76 }
77 }
78 },
79 "node_modules/@jridgewell/resolve-uri": {
80 "version": "3.1.2",
81 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
82 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
83 "dev": true,
84 "license": "MIT",
85 "engines": {
86 "node": ">=6.0.0"
87 }
88 },
89 "node_modules/@jridgewell/sourcemap-codec": {
90 "version": "1.5.5",
91 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
92 "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
93 "dev": true,
94 "license": "MIT"
95 },
96 "node_modules/@jridgewell/trace-mapping": {
97 "version": "0.3.31",
98 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
99 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
100 "dev": true,
101 "license": "MIT",
102 "dependencies": {
103 "@jridgewell/resolve-uri": "^3.1.0",
104 "@jridgewell/sourcemap-codec": "^1.4.14"
105 }
106 },
107 "node_modules/@types/istanbul-lib-coverage": {
108 "version": "2.0.6",
109 "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
110 "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
111 "dev": true,
112 "license": "MIT"
113 },
114 "node_modules/b4a": {
115 "version": "1.7.4",
116 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.4.tgz",
117 "integrity": "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug==",
118 "license": "Apache-2.0",
119 "peerDependencies": {
120 "react-native-b4a": "*"
121 },
122 "peerDependenciesMeta": {
123 "react-native-b4a": {
124 "optional": true
125 }
126 }
127 },
128 "node_modules/bare-addon-resolve": {
129 "version": "1.10.0",
130 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
131 "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
132 "license": "Apache-2.0",
133 "dependencies": {
134 "bare-module-resolve": "^1.10.0",
135 "bare-semver": "^1.0.0"
136 },
137 "peerDependencies": {
138 "bare-url": "*"
139 },
140 "peerDependenciesMeta": {
141 "bare-url": {
142 "optional": true
143 }
144 }
145 },
146 "node_modules/bare-ansi-escapes": {
147 "version": "2.2.3",
148 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
149 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
150 "dev": true,
151 "license": "Apache-2.0",
152 "dependencies": {
153 "bare-stream": "^2.6.5"
154 },
155 "peerDependencies": {
156 "bare-buffer": "*"
157 },
158 "peerDependenciesMeta": {
159 "bare-buffer": {
160 "optional": true
161 }
162 }
163 },
164 "node_modules/bare-assert": {
165 "version": "1.2.0",
166 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
167 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
168 "dev": true,
169 "license": "Apache-2.0",
170 "dependencies": {
171 "bare-inspect": "^3.1.2"
172 }
173 },
174 "node_modules/bare-bundle": {
175 "version": "1.10.0",
176 "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
177 "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
178 "dev": true,
179 "license": "Apache-2.0",
180 "peerDependencies": {
181 "bare-buffer": "*",
182 "bare-url": "*"
183 },
184 "peerDependenciesMeta": {
185 "bare-buffer": {
186 "optional": true
187 },
188 "bare-url": {
189 "optional": true
190 }
191 }
192 },
193 "node_modules/bare-cov": {
194 "version": "1.2.1",
195 "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz",
196 "integrity": "sha512-SHNqb/WG9udUEj8m5qGSehoUQ9o0Xzv8nrf4HIdmG+KNU/XdhPLhTP5VPrHnSAMAXhAzEB4Q+2IYZ0x9qPzN1g==",
197 "dev": true,
198 "license": "Apache-2.0",
199 "dependencies": {
200 "bare-fs": "^4.1.2",
201 "bare-inspector": "^4.0.1",
202 "bare-path": "^3.0.0",
203 "bare-process": "^4.2.1",
204 "bare-url": "^2.1.5",
205 "bare-v8-to-istanbul": "^1.0.2",
206 "picomatch": "^4.0.2",
207 "which-runtime": "^1.2.1"
208 }
209 },
210 "node_modules/bare-crypto": {
211 "version": "1.13.0",
212 "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.0.tgz",
213 "integrity": "sha512-RQl13yD+YTACWUZHMMck0C6LNjXgGgRyhVC557ag0xIrkxXf4IQwpnc8WOB58f4k5kSzKhIjy05oW3HUBrFpSQ==",
214 "dev": true,
215 "license": "Apache-2.0",
216 "dependencies": {
217 "bare-stream": "^2.6.3"
218 },
219 "peerDependencies": {
220 "bare-buffer": "*"
221 },
222 "peerDependenciesMeta": {
223 "bare-buffer": {
224 "optional": true
225 }
226 }
227 },
228 "node_modules/bare-debug-log": {
229 "version": "2.0.0",
230 "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
231 "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
232 "dev": true,
233 "license": "Apache-2.0",
234 "dependencies": {
235 "bare-os": "^3.0.1"
236 }
237 },
238 "node_modules/bare-dns": {
239 "version": "2.1.4",
240 "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
241 "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==",
242 "dev": true,
243 "license": "Apache-2.0",
244 "engines": {
245 "bare": ">=1.7.0"
246 }
247 },
248 "node_modules/bare-encoding": {
249 "version": "1.0.3",
250 "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
251 "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
252 "dev": true,
253 "license": "Apache-2.0",
254 "peerDependencies": {
255 "bare-buffer": "*"
256 },
257 "peerDependenciesMeta": {
258 "bare-buffer": {
259 "optional": true
260 }
261 }
262 },
263 "node_modules/bare-env": {
264 "version": "3.0.0",
265 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
266 "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
267 "dev": true,
268 "license": "Apache-2.0",
269 "dependencies": {
270 "bare-os": "^3.0.1"
271 }
272 },
273 "node_modules/bare-events": {
274 "version": "2.8.2",
275 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
276 "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
277 "license": "Apache-2.0",
278 "peerDependencies": {
279 "bare-abort-controller": "*"
280 },
281 "peerDependenciesMeta": {
282 "bare-abort-controller": {
283 "optional": true
284 }
285 }
286 },
287 "node_modules/bare-format": {
288 "version": "1.0.1",
289 "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.1.tgz",
290 "integrity": "sha512-1oS+LZrWK6tnYnvNSHDGljc2MPunRxwhpFriuCgzNF+oklrnwmBKD91tS0yt+jpl2j3UgcSDzBIMiVTvLs9A8w==",
291 "dev": true,
292 "license": "Apache-2.0",
293 "dependencies": {
294 "bare-inspect": "^3.0.0"
295 }
296 },
297 "node_modules/bare-fs": {
298 "version": "4.5.4",
299 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz",
300 "integrity": "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==",
301 "license": "Apache-2.0",
302 "dependencies": {
303 "bare-events": "^2.5.4",
304 "bare-path": "^3.0.0",
305 "bare-stream": "^2.6.4",
306 "bare-url": "^2.2.2",
307 "fast-fifo": "^1.3.2"
308 },
309 "engines": {
310 "bare": ">=1.16.0"
311 },
312 "peerDependencies": {
313 "bare-buffer": "*"
314 },
315 "peerDependenciesMeta": {
316 "bare-buffer": {
317 "optional": true
318 }
319 }
320 },
321 "node_modules/bare-hrtime": {
322 "version": "2.1.1",
323 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
324 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
325 "dev": true,
326 "license": "Apache-2.0"
327 },
328 "node_modules/bare-http-parser": {
329 "version": "1.0.1",
330 "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.0.1.tgz",
331 "integrity": "sha512-A3LTDTcELcmNJ3g5liIaS038v/BQxOhA9cjhBESn7eoV7QCuMoIRBKLDadDe08flxyLbxI2f+1l2MZ/5+HnKPA==",
332 "dev": true,
333 "license": "Apache-2.0"
334 },
335 "node_modules/bare-http1": {
336 "version": "4.2.3",
337 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.2.3.tgz",
338 "integrity": "sha512-zyvHF3xzvBJNpuw0QydE9etTn2AyC/TRVDWTo8Ed3LQEEGaFOkv6KHhaJPlf4eWEeTekk41Fowie+fTsXoNxkA==",
339 "dev": true,
340 "license": "Apache-2.0",
341 "dependencies": {
342 "bare-events": "^2.6.0",
343 "bare-http-parser": "^1.0.0",
344 "bare-stream": "^2.3.0",
345 "bare-tcp": "^2.2.0"
346 },
347 "peerDependencies": {
348 "bare-buffer": "*",
349 "bare-url": "*"
350 },
351 "peerDependenciesMeta": {
352 "bare-buffer": {
353 "optional": true
354 },
355 "bare-url": {
356 "optional": true
357 }
358 }
359 },
360 "node_modules/bare-https": {
361 "version": "2.1.2",
362 "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.2.tgz",
363 "integrity": "sha512-Q+TTydUDsuKQJvh8dX2dvOXCR9fM3xR5TBmKaFrs5p7Lj7XbKX7v4vIUJ36H0SXg2xCOQxXKIbjwrLg5tfJNYg==",
364 "dev": true,
365 "license": "Apache-2.0",
366 "dependencies": {
367 "bare-http1": "^4.0.0",
368 "bare-tcp": "^2.2.0",
369 "bare-tls": "^2.0.0"
370 }
371 },
372 "node_modules/bare-inspect": {
373 "version": "3.1.4",
374 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
375 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
376 "dev": true,
377 "license": "Apache-2.0",
378 "dependencies": {
379 "bare-ansi-escapes": "^2.1.0",
380 "bare-type": "^1.0.0"
381 },
382 "engines": {
383 "bare": ">=1.18.0"
384 }
385 },
386 "node_modules/bare-inspector": {
387 "version": "4.0.3",
388 "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-4.0.3.tgz",
389 "integrity": "sha512-yYk6+7tTVKJKHOja5+QBn989ETv8oVsmwoH0nCM+QYC6w5MC03GC4aQNK+lipAZSz1EaAj42WZ8Wewjpy0L9TA==",
390 "dev": true,
391 "license": "Apache-2.0",
392 "dependencies": {
393 "bare-events": "^2.1.0",
394 "bare-http1": "^4.0.0",
395 "bare-stream": "^2.0.0",
396 "bare-url": "^2.0.0",
397 "bare-ws": "^2.0.0"
398 },
399 "engines": {
400 "bare": ">=1.2.0"
401 },
402 "peerDependencies": {
403 "bare-tcp": "*"
404 },
405 "peerDependenciesMeta": {
406 "bare-tcp": {
407 "optional": true
408 }
409 }
410 },
411 "node_modules/bare-module": {
412 "version": "6.1.3",
413 "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz",
414 "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==",
415 "dev": true,
416 "license": "Apache-2.0",
417 "dependencies": {
418 "bare-bundle": "^1.3.0",
419 "bare-module-lexer": "^1.0.0",
420 "bare-module-resolve": "^1.8.0",
421 "bare-path": "^3.0.0",
422 "bare-url": "^2.0.1"
423 },
424 "engines": {
425 "bare": ">=1.23.0"
426 },
427 "peerDependencies": {
428 "bare-buffer": "*"
429 },
430 "peerDependenciesMeta": {
431 "bare-buffer": {
432 "optional": true
433 }
434 }
435 },
436 "node_modules/bare-module-lexer": {
437 "version": "1.4.7",
438 "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
439 "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
440 "dev": true,
441 "license": "Apache-2.0",
442 "dependencies": {
443 "require-addon": "^1.0.2"
444 },
445 "peerDependencies": {
446 "bare-buffer": "*"
447 },
448 "peerDependenciesMeta": {
449 "bare-buffer": {
450 "optional": true
451 }
452 }
453 },
454 "node_modules/bare-module-resolve": {
455 "version": "1.12.1",
456 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz",
457 "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==",
458 "license": "Apache-2.0",
459 "dependencies": {
460 "bare-semver": "^1.0.0"
461 },
462 "peerDependencies": {
463 "bare-url": "*"
464 },
465 "peerDependenciesMeta": {
466 "bare-url": {
467 "optional": true
468 }
469 }
470 },
471 "node_modules/bare-net": {
472 "version": "2.2.0",
473 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.2.0.tgz",
474 "integrity": "sha512-UF7cAbHsGE+H6uEqWF5IULBow1x58chZz4g3ALgHtv7wZsFcCbRDt0JKWEumf5Oma3QWS1Q6aLi0Rpll8RElMg==",
475 "dev": true,
476 "license": "Apache-2.0",
477 "dependencies": {
478 "bare-events": "^2.2.2",
479 "bare-pipe": "^4.0.0",
480 "bare-stream": "^2.0.0",
481 "bare-tcp": "^2.0.0"
482 }
483 },
484 "node_modules/bare-os": {
485 "version": "3.6.2",
486 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
487 "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
488 "license": "Apache-2.0",
489 "engines": {
490 "bare": ">=1.14.0"
491 }
492 },
493 "node_modules/bare-path": {
494 "version": "3.0.0",
495 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
496 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
497 "license": "Apache-2.0",
498 "dependencies": {
499 "bare-os": "^3.0.1"
500 }
501 },
502 "node_modules/bare-pipe": {
503 "version": "4.1.3",
504 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.3.tgz",
505 "integrity": "sha512-DqQsx93rAzre6yJ9T6l/Vgh+X+bntkVMB1X5ggtXjXtqtMmF2Y2RVlCzxxy/09R6yeR9FSWBEUIaMYJL1/5VDA==",
506 "dev": true,
507 "license": "Apache-2.0",
508 "dependencies": {
509 "bare-events": "^2.0.0",
510 "bare-stream": "^2.0.0"
511 },
512 "engines": {
513 "bare": ">=1.16.0"
514 }
515 },
516 "node_modules/bare-process": {
517 "version": "4.2.2",
518 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.2.2.tgz",
519 "integrity": "sha512-ikzEw+HGLB+2lS/WLVEsqi8BXBwzleG3n7DYI0v6/YNklvNabOIGlLd1dof+7Jw5ob4jsBRPtyflweVahY7rFA==",
520 "dev": true,
521 "license": "Apache-2.0",
522 "dependencies": {
523 "bare-env": "^3.0.0",
524 "bare-events": "^2.3.1",
525 "bare-hrtime": "^2.0.0",
526 "bare-os": "^3.5.0",
527 "bare-pipe": "^4.0.0",
528 "bare-signals": "^4.0.0",
529 "bare-tty": "^5.0.0"
530 }
531 },
532 "node_modules/bare-semver": {
533 "version": "1.0.2",
534 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz",
535 "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==",
536 "license": "Apache-2.0"
537 },
538 "node_modules/bare-signals": {
539 "version": "4.2.0",
540 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz",
541 "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==",
542 "dev": true,
543 "license": "Apache-2.0",
544 "dependencies": {
545 "bare-events": "^2.5.3",
546 "bare-os": "^3.3.1"
547 },
548 "engines": {
549 "bare": ">=1.7.0"
550 }
551 },
552 "node_modules/bare-stream": {
553 "version": "2.7.0",
554 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
555 "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
556 "license": "Apache-2.0",
557 "dependencies": {
558 "streamx": "^2.21.0"
559 },
560 "peerDependencies": {
561 "bare-buffer": "*",
562 "bare-events": "*"
563 },
564 "peerDependenciesMeta": {
565 "bare-buffer": {
566 "optional": true
567 },
568 "bare-events": {
569 "optional": true
570 }
571 }
572 },
573 "node_modules/bare-subprocess": {
574 "version": "5.2.2",
575 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.2.tgz",
576 "integrity": "sha512-L6oXgQ1aWs25RtG5Ky0bDD06p3RAcVVrDDMWb1DfXpHtyEWxamcyIWUbSykxMTWrpLlmSj6ytbb6yoKehGFfmw==",
577 "dev": true,
578 "license": "Apache-2.0",
579 "dependencies": {
580 "bare-env": "^3.0.0",
581 "bare-events": "^2.5.4",
582 "bare-os": "^3.0.1",
583 "bare-pipe": "^4.0.0",
584 "bare-url": "^2.2.2"
585 },
586 "engines": {
587 "bare": ">=1.7.0"
588 },
589 "peerDependencies": {
590 "bare-buffer": "*"
591 },
592 "peerDependenciesMeta": {
593 "bare-buffer": {
594 "optional": true
595 }
596 }
597 },
598 "node_modules/bare-tcp": {
599 "version": "2.2.4",
600 "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.4.tgz",
601 "integrity": "sha512-vGjfMkyhS1lxp+Y4+gXRDR1ijZMK2cKx0RBwtYKUuZbD90ARCoM6bAiTa1HBztumCIC+NXb8eXdMFXyuYwlPyw==",
602 "dev": true,
603 "license": "Apache-2.0",
604 "dependencies": {
605 "bare-dns": "^2.0.4",
606 "bare-events": "^2.5.4",
607 "bare-stream": "^2.6.4"
608 },
609 "engines": {
610 "bare": ">=1.16.0"
611 }
612 },
613 "node_modules/bare-tls": {
614 "version": "2.1.7",
615 "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.1.7.tgz",
616 "integrity": "sha512-h6wcNXQdBeTX7fed9tjPp0/9cA/QfcBTv3ItgjnbUk4rWAU8bEFalZCZnUDdCK/t9zrNfJ+yvcPx4D/1Y6biyA==",
617 "dev": true,
618 "license": "Apache-2.0",
619 "dependencies": {
620 "bare-net": "^2.0.1",
621 "bare-stream": "^2.6.4"
622 },
623 "engines": {
624 "bare": ">=1.7.0"
625 }
626 },
627 "node_modules/bare-tty": {
628 "version": "5.0.3",
629 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.0.3.tgz",
630 "integrity": "sha512-jW24RBWRZOMHuSEWC9mh8wUKO5WUNl0UWUTNPn3yZ8qkOqwa8vaV2dR3a+BvblONPk7V35wuvK9P8508+judAg==",
631 "dev": true,
632 "license": "Apache-2.0",
633 "dependencies": {
634 "bare-events": "^2.2.0",
635 "bare-signals": "^4.0.0",
636 "bare-stream": "^2.0.0"
637 },
638 "engines": {
639 "bare": ">=1.16.0"
640 }
641 },
642 "node_modules/bare-type": {
643 "version": "1.1.0",
644 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
645 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
646 "dev": true,
647 "license": "Apache-2.0",
648 "engines": {
649 "bare": ">=1.2.0"
650 }
651 },
652 "node_modules/bare-url": {
653 "version": "2.3.2",
654 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
655 "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
656 "license": "Apache-2.0",
657 "peer": true,
658 "dependencies": {
659 "bare-path": "^3.0.0"
660 }
661 },
662 "node_modules/bare-utils": {
663 "version": "1.5.1",
664 "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.5.1.tgz",
665 "integrity": "sha512-mxCkFvmDU3mlD/mb+pT64kKXOsx2KMsWLQbngN1LB+NOXfhfnRnyvpy3VZc6m7gzQxe57Bsi+aTCBqA4/S3elQ==",
666 "dev": true,
667 "license": "Apache-2.0",
668 "dependencies": {
669 "bare-debug-log": "^2.0.0",
670 "bare-encoding": "^1.0.0",
671 "bare-format": "^1.0.0",
672 "bare-inspect": "^3.0.0",
673 "bare-type": "^1.0.6"
674 }
675 },
676 "node_modules/bare-v8-to-istanbul": {
677 "version": "1.0.2",
678 "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
679 "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==",
680 "dev": true,
681 "license": "Apache-2.0",
682 "dependencies": {
683 "bare-assert": "^1.0.2",
684 "bare-fs": "^4.1.2",
685 "bare-module": "^6.1.2",
686 "bare-path": "^3.0.0",
687 "bare-process": "^4.2.0",
688 "bare-url": "^2.1.5",
689 "bare-utils": "^1.2.0",
690 "v8-to-istanbul": "^9.3.0",
691 "which-runtime": "^1.2.1"
692 }
693 },
694 "node_modules/bare-ws": {
695 "version": "2.0.4",
696 "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.0.4.tgz",
697 "integrity": "sha512-SQMXzBYna9dRj57Dz1/ag+VWHCRXbfCjMHgyfM2F2lhkVLzMjnVSZP72aVeFWPFqe494Rd70Kzhe2JElGwFlJQ==",
698 "dev": true,
699 "license": "Apache-2.0",
700 "dependencies": {
701 "bare-crypto": "^1.2.0",
702 "bare-events": "^2.3.1",
703 "bare-http1": "^4.0.0",
704 "bare-https": "^2.0.0",
705 "bare-stream": "^2.1.2"
706 },
707 "peerDependencies": {
708 "bare-buffer": "*",
709 "bare-url": "*"
710 },
711 "peerDependenciesMeta": {
712 "bare-buffer": {
713 "optional": true
714 },
715 "bare-url": {
716 "optional": true
717 }
718 }
719 },
720 "node_modules/big-sparse-array": {
721 "version": "1.0.3",
722 "resolved": "https://registry.npmjs.org/big-sparse-array/-/big-sparse-array-1.0.3.tgz",
723 "integrity": "sha512-6RjV/3mSZORlMdpUaQ6rUSpG637cZm0//E54YYGtQg1c1O+AbZP8UTdJ/TchsDZcTVLmyWZcseBfp2HBeXUXOQ==",
724 "license": "MIT"
725 },
726 "node_modules/bridge-envelope": {
727 "resolved": "../bridge-envelope",
728 "link": true
729 },
730 "node_modules/brittle": {
731 "version": "3.19.1",
732 "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz",
733 "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==",
734 "dev": true,
735 "license": "Apache-2.0",
736 "dependencies": {
737 "b4a": "^1.6.0",
738 "bare-assert": "^1.0.2",
739 "bare-cov": "^1.1.0",
740 "bare-fs": "^4.1.6",
741 "bare-os": "^3.6.1",
742 "bare-path": "^3.0.0",
743 "bare-process": "^4.2.1",
744 "bare-subprocess": "^5.0.0",
745 "bare-url": "^2.1.6",
746 "error-stack-parser": "^2.1.4",
747 "globbie": "^1.0.2",
748 "paparam": "^1.6.2",
749 "same-object": "^1.0.2",
750 "test-tmp": "^1.4.0",
751 "tmatch": "^5.0.0"
752 },
753 "bin": {
754 "brittle": "bin/node.js",
755 "brittle-bare": "bin/bare.js",
756 "brittle-node": "bin/node.js",
757 "brittle-pear": "bin/pear.js"
758 }
759 },
760 "node_modules/codecs": {
761 "version": "3.1.0",
762 "resolved": "https://registry.npmjs.org/codecs/-/codecs-3.1.0.tgz",
763 "integrity": "sha512-Dqx8NwvBvnMeuPQdVKy/XEF71igjR5apxBvCGeV0pP1tXadOiaLvDTXt7xh+/5wI1ASB195mXQGJbw3Ml4YDWQ==",
764 "license": "MIT",
765 "dependencies": {
766 "b4a": "^1.6.3"
767 }
768 },
769 "node_modules/compact-encoding": {
770 "version": "2.18.0",
771 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-2.18.0.tgz",
772 "integrity": "sha512-goACAOlhMI2xo5jGOMUDfOLnGdRE1jGfyZ+zie8N5114nHrbPIqf6GLUtzbLof6DSyrERlYRm3EcBplte5LcQw==",
773 "license": "Apache-2.0",
774 "dependencies": {
775 "b4a": "^1.3.0"
776 }
777 },
778 "node_modules/convert-source-map": {
779 "version": "2.0.0",
780 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
781 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
782 "dev": true,
783 "license": "MIT"
784 },
785 "node_modules/corestore": {
786 "version": "6.18.4",
787 "resolved": "https://registry.npmjs.org/corestore/-/corestore-6.18.4.tgz",
788 "integrity": "sha512-KL1cOUyj3DFRNzeVRekGPRiGN4sIfGo7BsncSJQQBT3Rh2y1nQ32MOR9X5DlOMlWIT4txwqBzhzam6+JXtwFVw==",
789 "license": "MIT",
790 "dependencies": {
791 "b4a": "^1.6.4",
792 "hypercore": "^10.37.10",
793 "hypercore-crypto": "^3.4.0",
794 "hypercore-id-encoding": "^1.2.0",
795 "read-write-mutexify": "^2.1.0",
796 "ready-resource": "^1.0.0",
797 "safety-catch": "^1.0.1",
798 "sodium-universal": "^4.0.0",
799 "xache": "^1.1.0"
800 }
801 },
802 "node_modules/crc-native": {
803 "version": "1.1.8",
804 "resolved": "https://registry.npmjs.org/crc-native/-/crc-native-1.1.8.tgz",
805 "integrity": "sha512-R/RYbD8lhG5M1k0iLX49tJdwHM0ipevJPpZw2Wk7b4EShyvx2P0uaq79xyRpcknjiVGiUfjFA8Gyy0mOlhABvw==",
806 "license": "Apache-2.0",
807 "optional": true,
808 "dependencies": {
809 "require-addon": "^1.1.0"
810 }
811 },
812 "node_modules/crc-universal": {
813 "version": "1.0.4",
814 "resolved": "https://registry.npmjs.org/crc-universal/-/crc-universal-1.0.4.tgz",
815 "integrity": "sha512-CE9xWEI6Gd5V0Bdmj5NoWH3d7+EIe4zUpy1sv6uvKYznzsDP1vhiWsTBmL6q9IrH2P6RRshp+8AkhR4CMtY5Hg==",
816 "license": "ISC",
817 "optionalDependencies": {
818 "crc-native": "^1.0.3"
819 }
820 },
821 "node_modules/debounceify": {
822 "version": "1.1.0",
823 "resolved": "https://registry.npmjs.org/debounceify/-/debounceify-1.1.0.tgz",
824 "integrity": "sha512-eKuHDVfJVg+u/0nPy8P+fhnLgbyuTgVxuCRrS/R7EpDSMMkBDgSes41MJtSAY1F1hcqfHz3Zy/qpqHHIp/EhdA==",
825 "license": "MIT"
826 },
827 "node_modules/error-stack-parser": {
828 "version": "2.1.4",
829 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
830 "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
831 "dev": true,
832 "license": "MIT",
833 "dependencies": {
834 "stackframe": "^1.3.4"
835 }
836 },
837 "node_modules/events-universal": {
838 "version": "1.0.1",
839 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
840 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
841 "license": "Apache-2.0",
842 "dependencies": {
843 "bare-events": "^2.7.0"
844 }
845 },
846 "node_modules/fast-fifo": {
847 "version": "1.3.2",
848 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
849 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
850 "license": "MIT"
851 },
852 "node_modules/flat-tree": {
853 "version": "1.13.0",
854 "resolved": "https://registry.npmjs.org/flat-tree/-/flat-tree-1.13.0.tgz",
855 "integrity": "sha512-fT3HIuCPwHhFgJ20QYzDHgUG0zMmFg5cHvFiFo5h+QMSJ28TihsEVY0f8HGliuO+pOzmvjMx1odToeaEWkTnyQ==",
856 "license": "MIT"
857 },
858 "node_modules/fs-native-extensions": {
859 "version": "1.4.5",
860 "resolved": "https://registry.npmjs.org/fs-native-extensions/-/fs-native-extensions-1.4.5.tgz",
861 "integrity": "sha512-ekV0T//iDm4AvhOcuPaHpxub4DI7HvY5ucLJVDvi7T2J+NZkQ9S6MuvgP0yeQvoqNUaAGyLjVYb1905BF9bpmg==",
862 "license": "Apache-2.0",
863 "optional": true,
864 "dependencies": {
865 "require-addon": "^1.1.0",
866 "which-runtime": "^1.2.0"
867 }
868 },
869 "node_modules/globbie": {
870 "version": "1.0.3",
871 "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz",
872 "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==",
873 "dev": true,
874 "license": "Apache-2.0",
875 "dependencies": {
876 "bare-fs": "^4.1.2",
877 "bare-path": "^3.0.0",
878 "bare-process": "^4.2.1",
879 "picomatch": "^4.0.2"
880 }
881 },
882 "node_modules/hyperbee": {
883 "version": "2.27.3",
884 "resolved": "https://registry.npmjs.org/hyperbee/-/hyperbee-2.27.3.tgz",
885 "integrity": "sha512-PXURH2U4juUZyJRKHTrY5z1zX851pmI1Q0jfv5F/hCIErDt/ND8jOZuxc3hfOLM9f0W3qJEDTMlV5AJBkVPy8w==",
886 "license": "MIT",
887 "dependencies": {
888 "b4a": "^1.6.0",
889 "codecs": "^3.0.0",
890 "debounceify": "^1.0.0",
891 "hypercore-errors": "^1.0.0",
892 "mutexify": "^1.4.0",
893 "protocol-buffers-encodings": "^1.2.0",
894 "rache": "^1.0.0",
895 "ready-resource": "^1.0.0",
896 "resolve-reject-promise": "^1.1.0",
897 "safety-catch": "^1.0.2",
898 "streamx": "^2.12.4",
899 "unslab": "^1.2.0"
900 }
901 },
902 "node_modules/hypercore": {
903 "version": "10.38.2",
904 "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-10.38.2.tgz",
905 "integrity": "sha512-2vx42g2RKdP/o2XzlxKfvgEaZ3+h/B855gCR8iscUDnxUoOZej7vXZ2buex0sweDUEW5aEfMxFR6o5hncio3Nw==",
906 "license": "MIT",
907 "dependencies": {
908 "@hyperswarm/secret-stream": "^6.0.0",
909 "b4a": "^1.1.0",
910 "bare-events": "^2.2.0",
911 "big-sparse-array": "^1.0.3",
912 "compact-encoding": "^2.11.0",
913 "crc-universal": "^1.0.2",
914 "fast-fifo": "^1.3.0",
915 "flat-tree": "^1.9.0",
916 "hypercore-crypto": "^3.2.1",
917 "hypercore-errors": "^1.2.0",
918 "hypercore-id-encoding": "^1.2.0",
919 "is-options": "^1.0.1",
920 "protomux": "^3.5.0",
921 "quickbit-universal": "^2.2.0",
922 "random-access-file": "^4.0.0",
923 "random-array-iterator": "^1.0.0",
924 "safety-catch": "^1.0.1",
925 "sodium-universal": "^4.0.0",
926 "streamx": "^2.12.4",
927 "unslab": "^1.3.0",
928 "xache": "^1.1.0",
929 "z32": "^1.0.0"
930 }
931 },
932 "node_modules/hypercore-crypto": {
933 "version": "3.6.1",
934 "resolved": "https://registry.npmjs.org/hypercore-crypto/-/hypercore-crypto-3.6.1.tgz",
935 "integrity": "sha512-ltIz2uDwy9pO/ZGTvqcjzyBkvt6O4cVm4r/nNxh0GFs/RbQtqP/i4wCvLEdmU7ptgtnw7fI67WYD1aHPuv4OVA==",
936 "license": "MIT",
937 "dependencies": {
938 "b4a": "^1.6.6",
939 "compact-encoding": "^2.15.0",
940 "sodium-universal": "^5.0.0"
941 }
942 },
943 "node_modules/hypercore-crypto/node_modules/sodium-native": {
944 "version": "5.0.10",
945 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
946 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
947 "license": "MIT",
948 "dependencies": {
949 "require-addon": "^1.1.0",
950 "which-runtime": "^1.2.1"
951 },
952 "engines": {
953 "bare": ">=1.16.0"
954 }
955 },
956 "node_modules/hypercore-crypto/node_modules/sodium-universal": {
957 "version": "5.0.1",
958 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
959 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
960 "license": "MIT",
961 "dependencies": {
962 "sodium-native": "^5.0.1"
963 },
964 "peerDependencies": {
965 "sodium-javascript": "~0.8.0"
966 },
967 "peerDependenciesMeta": {
968 "sodium-javascript": {
969 "optional": true
970 }
971 }
972 },
973 "node_modules/hypercore-errors": {
974 "version": "1.5.0",
975 "resolved": "https://registry.npmjs.org/hypercore-errors/-/hypercore-errors-1.5.0.tgz",
976 "integrity": "sha512-5KQ/SuDxsvet+7qWA35Ay6zdD9WyAHQoyWHGcPUTbmJBd300gvNIJoi3oma7kp4TTCSzii6qYumNZe/s0j/saQ==",
977 "license": "Apache-2.0",
978 "dependencies": {
979 "hypercore-id-encoding": "^1.3.0"
980 }
981 },
982 "node_modules/hypercore-id-encoding": {
983 "version": "1.3.0",
984 "resolved": "https://registry.npmjs.org/hypercore-id-encoding/-/hypercore-id-encoding-1.3.0.tgz",
985 "integrity": "sha512-W6sHdGo5h7LXEsoWfKf/KfuROZmZRQDlGqJF2EPHW+noCK66Vvr0+zE6cL0vqQi18s0kQPeN7Sq3QyR0Ytc2VQ==",
986 "license": "Apache-2.0",
987 "dependencies": {
988 "b4a": "^1.5.3",
989 "z32": "^1.0.0"
990 }
991 },
992 "node_modules/is-options": {
993 "version": "1.0.2",
994 "resolved": "https://registry.npmjs.org/is-options/-/is-options-1.0.2.tgz",
995 "integrity": "sha512-u+Ai74c8Q74aS8BuHwPdI1jptGOT1FQXgCq8/zv0xRuE+wRgSMEJLj8lVO8Zp9BeGb29BXY6AsNPinfqjkr7Fg==",
996 "license": "MIT",
997 "dependencies": {
998 "b4a": "^1.1.1"
999 }
1000 },
1001 "node_modules/mutexify": {
1002 "version": "1.4.0",
1003 "resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz",
1004 "integrity": "sha512-pbYSsOrSB/AKN5h/WzzLRMFgZhClWccf2XIB4RSMC8JbquiB0e0/SH5AIfdQMdyHmYtv4seU7yV/TvAwPLJ1Yg==",
1005 "license": "MIT",
1006 "dependencies": {
1007 "queue-tick": "^1.0.0"
1008 }
1009 },
1010 "node_modules/nanoassert": {
1011 "version": "2.0.0",
1012 "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz",
1013 "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==",
1014 "license": "ISC"
1015 },
1016 "node_modules/noise-curve-ed": {
1017 "version": "2.1.0",
1018 "resolved": "https://registry.npmjs.org/noise-curve-ed/-/noise-curve-ed-2.1.0.tgz",
1019 "integrity": "sha512-zAzJx+VwZM3w6EA1hTmDhJfvAnCeBQn/1FAeZ0LtGxCcCtlAK/uJXQVF/eDVUOaAZ286lHlx77WJ+qj9SmsRRg==",
1020 "license": "ISC",
1021 "dependencies": {
1022 "b4a": "^1.1.0",
1023 "nanoassert": "^2.0.0",
1024 "sodium-universal": "^5.0.0"
1025 }
1026 },
1027 "node_modules/noise-curve-ed/node_modules/sodium-native": {
1028 "version": "5.0.10",
1029 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
1030 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
1031 "license": "MIT",
1032 "dependencies": {
1033 "require-addon": "^1.1.0",
1034 "which-runtime": "^1.2.1"
1035 },
1036 "engines": {
1037 "bare": ">=1.16.0"
1038 }
1039 },
1040 "node_modules/noise-curve-ed/node_modules/sodium-universal": {
1041 "version": "5.0.1",
1042 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1043 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1044 "license": "MIT",
1045 "dependencies": {
1046 "sodium-native": "^5.0.1"
1047 },
1048 "peerDependencies": {
1049 "sodium-javascript": "~0.8.0"
1050 },
1051 "peerDependenciesMeta": {
1052 "sodium-javascript": {
1053 "optional": true
1054 }
1055 }
1056 },
1057 "node_modules/noise-handshake": {
1058 "version": "4.2.0",
1059 "resolved": "https://registry.npmjs.org/noise-handshake/-/noise-handshake-4.2.0.tgz",
1060 "integrity": "sha512-9O/VTNX/E2/AToyMTTDU0J/4WhaXMTdqc2DHs9vf+snoZ0cenSBq0dNYTVV1snYYEkmo6QeRrYMxtqtoYnY+LA==",
1061 "license": "Apache-2.0",
1062 "dependencies": {
1063 "b4a": "^1.1.0",
1064 "nanoassert": "^2.0.0",
1065 "sodium-universal": "^5.0.0"
1066 }
1067 },
1068 "node_modules/noise-handshake/node_modules/sodium-native": {
1069 "version": "5.0.10",
1070 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
1071 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
1072 "license": "MIT",
1073 "dependencies": {
1074 "require-addon": "^1.1.0",
1075 "which-runtime": "^1.2.1"
1076 },
1077 "engines": {
1078 "bare": ">=1.16.0"
1079 }
1080 },
1081 "node_modules/noise-handshake/node_modules/sodium-universal": {
1082 "version": "5.0.1",
1083 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1084 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1085 "license": "MIT",
1086 "dependencies": {
1087 "sodium-native": "^5.0.1"
1088 },
1089 "peerDependencies": {
1090 "sodium-javascript": "~0.8.0"
1091 },
1092 "peerDependenciesMeta": {
1093 "sodium-javascript": {
1094 "optional": true
1095 }
1096 }
1097 },
1098 "node_modules/paparam": {
1099 "version": "1.10.0",
1100 "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.0.tgz",
1101 "integrity": "sha512-p36QQrwU3X6fj5d2JN20B4lcZI/O3fxMVUHc1xD7MNe5bcOf3jIgE0CIK5Zv/s5YAUGlobTJ/kKXrh1FJVrs3w==",
1102 "dev": true,
1103 "license": "Apache-2.0"
1104 },
1105 "node_modules/picomatch": {
1106 "version": "4.0.3",
1107 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
1108 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
1109 "dev": true,
1110 "license": "MIT",
1111 "engines": {
1112 "node": ">=12"
1113 },
1114 "funding": {
1115 "url": "https://github.com/sponsors/jonschlinkert"
1116 }
1117 },
1118 "node_modules/protocol-buffers-encodings": {
1119 "version": "1.2.0",
1120 "resolved": "https://registry.npmjs.org/protocol-buffers-encodings/-/protocol-buffers-encodings-1.2.0.tgz",
1121 "integrity": "sha512-daeNPuKh1NlLD1uDfbLpD+xyUTc07nEtfHwmBZmt/vH0B7VOM+JOCOpDcx9ZRpqHjAiIkGqyTDi+wfGSl17R9w==",
1122 "license": "MIT",
1123 "dependencies": {
1124 "b4a": "^1.6.0",
1125 "signed-varint": "^2.0.1",
1126 "varint": "5.0.0"
1127 }
1128 },
1129 "node_modules/protomux": {
1130 "version": "3.10.1",
1131 "resolved": "https://registry.npmjs.org/protomux/-/protomux-3.10.1.tgz",
1132 "integrity": "sha512-jgBqx8ZyaBWea/DFG4eOu1scOaeBwcnagiRC1XFVrjeGt7oAb0Pk5udPpBUpJ4DJBRjra50jD6YcZiQQTRqaaA==",
1133 "license": "MIT",
1134 "dependencies": {
1135 "b4a": "^1.3.1",
1136 "compact-encoding": "^2.5.1",
1137 "queue-tick": "^1.0.0",
1138 "safety-catch": "^1.0.1",
1139 "unslab": "^1.3.0"
1140 }
1141 },
1142 "node_modules/queue-tick": {
1143 "version": "1.0.1",
1144 "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
1145 "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
1146 "license": "MIT"
1147 },
1148 "node_modules/quickbit-native": {
1149 "version": "2.4.8",
1150 "resolved": "https://registry.npmjs.org/quickbit-native/-/quickbit-native-2.4.8.tgz",
1151 "integrity": "sha512-FcCcqI+nIAWGknqhtrYT5TSD7t/N+Xd8ctM+2PrIIBuwOi5hx0SxAvuPtzLIEMfT/2h9+fhBakUe2uALOHX6yw==",
1152 "license": "Apache-2.0",
1153 "optional": true,
1154 "dependencies": {
1155 "require-addon": "^1.1.0"
1156 }
1157 },
1158 "node_modules/quickbit-universal": {
1159 "version": "2.2.0",
1160 "resolved": "https://registry.npmjs.org/quickbit-universal/-/quickbit-universal-2.2.0.tgz",
1161 "integrity": "sha512-w02i1R8n7+6pEKTud8DfF8zbFY9o7RtPlUc3jWbtCkDKvhbx/AvV7oNnz4/TcmsPGpSJS+fq5Ud6RH6+YPvSGg==",
1162 "license": "ISC",
1163 "dependencies": {
1164 "b4a": "^1.6.0",
1165 "simdle-universal": "^1.1.0"
1166 },
1167 "optionalDependencies": {
1168 "quickbit-native": "^2.2.0"
1169 }
1170 },
1171 "node_modules/rache": {
1172 "version": "1.0.0",
1173 "resolved": "https://registry.npmjs.org/rache/-/rache-1.0.0.tgz",
1174 "integrity": "sha512-e0k0g0w/8jOCB+7YqCIlOa+OJ38k0wrYS4x18pMSmqOvLKoyhmMhmQyCcvfY6VaP8D75cqkEnlakXs+RYYLqNg==",
1175 "license": "Apache-2.0"
1176 },
1177 "node_modules/random-access-file": {
1178 "version": "4.1.2",
1179 "resolved": "https://registry.npmjs.org/random-access-file/-/random-access-file-4.1.2.tgz",
1180 "integrity": "sha512-GQM6R78DceZDcQod8KxlDFwXIiUvlvuy1EOzxTDsjuDjW5NlnlZi0MOk6iI4itAj/2vcvdqcEExYbVpC/dJcEw==",
1181 "license": "MIT",
1182 "dependencies": {
1183 "bare-fs": "^4.0.1",
1184 "bare-path": "^3.0.0",
1185 "random-access-storage": "^3.0.0"
1186 },
1187 "optionalDependencies": {
1188 "fs-native-extensions": "^1.3.1"
1189 }
1190 },
1191 "node_modules/random-access-storage": {
1192 "version": "3.0.2",
1193 "resolved": "https://registry.npmjs.org/random-access-storage/-/random-access-storage-3.0.2.tgz",
1194 "integrity": "sha512-Es9maUyWdJXWKckKy9s1+vT+DEgAt+PBb9lxPaake/0EDUsHehloKGv9v1zimS2V3gpFAcQXubvc1Rgci2sDPQ==",
1195 "license": "MIT",
1196 "dependencies": {
1197 "bare-events": "^2.2.0",
1198 "queue-tick": "^1.0.0"
1199 }
1200 },
1201 "node_modules/random-array-iterator": {
1202 "version": "1.0.0",
1203 "resolved": "https://registry.npmjs.org/random-array-iterator/-/random-array-iterator-1.0.0.tgz",
1204 "integrity": "sha512-u7xCM93XqKEvPTP6xZp2ehttcAemKnh73oKNf1FvzuVCfpt6dILDt1Kxl1LeBjm2iNIeR49VGFhy4Iz3yOun+Q==",
1205 "license": "MIT"
1206 },
1207 "node_modules/read-write-mutexify": {
1208 "version": "2.1.0",
1209 "resolved": "https://registry.npmjs.org/read-write-mutexify/-/read-write-mutexify-2.1.0.tgz",
1210 "integrity": "sha512-fDw/p5/acI1ytVY1UbxEDma/ej1yJH/n9NcjS9YNzcE6sPBPWdlru3ydRa/UBowUg4zqOvNMD5SOGYJrlQ6MzQ==",
1211 "license": "MIT"
1212 },
1213 "node_modules/ready-resource": {
1214 "version": "1.2.0",
1215 "resolved": "https://registry.npmjs.org/ready-resource/-/ready-resource-1.2.0.tgz",
1216 "integrity": "sha512-nfcco/8iAFV0M+2PYnmIc+/xY0iRb35d42HFHQ7AfjulbGEAFa+XWpByfwSyeVeiBoMLLFVMv1HixxNCqzSQ1g==",
1217 "license": "MIT",
1218 "dependencies": {
1219 "bare-events": "^2.2.0"
1220 }
1221 },
1222 "node_modules/require-addon": {
1223 "version": "1.2.0",
1224 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
1225 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
1226 "license": "Apache-2.0",
1227 "dependencies": {
1228 "bare-addon-resolve": "^1.3.0"
1229 },
1230 "engines": {
1231 "bare": ">=1.10.0"
1232 }
1233 },
1234 "node_modules/resolve-reject-promise": {
1235 "version": "1.1.0",
1236 "resolved": "https://registry.npmjs.org/resolve-reject-promise/-/resolve-reject-promise-1.1.0.tgz",
1237 "integrity": "sha512-LWsTOA91AqzBTjSGgX79Tc130pwcBK6xjpJEO+qRT5IKZ6bGnHKcc8QL3upUBcWuU8OTIDzKK2VNSwmmlqvAVg==",
1238 "license": "MIT"
1239 },
1240 "node_modules/safety-catch": {
1241 "version": "1.0.2",
1242 "resolved": "https://registry.npmjs.org/safety-catch/-/safety-catch-1.0.2.tgz",
1243 "integrity": "sha512-C1UYVZ4dtbBxEtvOcpjBaaD27nP8MlvyAQEp2fOTOEe6pfUpk1cDUxij6BR1jZup6rSyUTaBBplK7LanskrULA==",
1244 "license": "MIT"
1245 },
1246 "node_modules/same-object": {
1247 "version": "1.0.2",
1248 "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz",
1249 "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==",
1250 "dev": true,
1251 "license": "MIT"
1252 },
1253 "node_modules/signed-varint": {
1254 "version": "2.0.1",
1255 "resolved": "https://registry.npmjs.org/signed-varint/-/signed-varint-2.0.1.tgz",
1256 "integrity": "sha512-abgDPg1106vuZZOvw7cFwdCABddfJRz5akcCcchzTbhyhYnsG31y4AlZEgp315T7W3nQq5P4xeOm186ZiPVFzw==",
1257 "license": "MIT",
1258 "dependencies": {
1259 "varint": "~5.0.0"
1260 }
1261 },
1262 "node_modules/simdle-native": {
1263 "version": "1.3.9",
1264 "resolved": "https://registry.npmjs.org/simdle-native/-/simdle-native-1.3.9.tgz",
1265 "integrity": "sha512-Isc8sP4OiiIU0mpslD4GHEnR0VQWvR/54WN7YtwEDkNdTJVWtpmvsSvsgRlw5BNGxdYXlVRegdnrSu10H/PhvA==",
1266 "license": "Apache-2.0",
1267 "optional": true,
1268 "dependencies": {
1269 "b4a": "^1.6.0",
1270 "require-addon": "^1.1.0"
1271 }
1272 },
1273 "node_modules/simdle-universal": {
1274 "version": "1.1.2",
1275 "resolved": "https://registry.npmjs.org/simdle-universal/-/simdle-universal-1.1.2.tgz",
1276 "integrity": "sha512-3n3w1bs+uwgHKQjt6arez83EywNlhZzYvNOhvAASTl/8KqNIcqr6aHyGt3JRlfuUC7iB0tomJRPlJ2cRGIpBzA==",
1277 "license": "ISC",
1278 "dependencies": {
1279 "b4a": "^1.6.0"
1280 },
1281 "optionalDependencies": {
1282 "simdle-native": "^1.1.1"
1283 }
1284 },
1285 "node_modules/sodium-native": {
1286 "version": "4.3.3",
1287 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz",
1288 "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==",
1289 "license": "MIT",
1290 "dependencies": {
1291 "require-addon": "^1.1.0"
1292 }
1293 },
1294 "node_modules/sodium-secretstream": {
1295 "version": "1.2.0",
1296 "resolved": "https://registry.npmjs.org/sodium-secretstream/-/sodium-secretstream-1.2.0.tgz",
1297 "integrity": "sha512-q/DbraNFXm1KfCiiZvapmz5UC3OlpirYFIvBK2MhGaOFSb3gRyk8OXTi17UI9SGfshQNCpsVvlopogbzZNyW6Q==",
1298 "license": "MIT",
1299 "dependencies": {
1300 "b4a": "^1.1.1",
1301 "sodium-universal": "^5.0.0"
1302 }
1303 },
1304 "node_modules/sodium-secretstream/node_modules/sodium-native": {
1305 "version": "5.0.10",
1306 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
1307 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
1308 "license": "MIT",
1309 "dependencies": {
1310 "require-addon": "^1.1.0",
1311 "which-runtime": "^1.2.1"
1312 },
1313 "engines": {
1314 "bare": ">=1.16.0"
1315 }
1316 },
1317 "node_modules/sodium-secretstream/node_modules/sodium-universal": {
1318 "version": "5.0.1",
1319 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1320 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1321 "license": "MIT",
1322 "dependencies": {
1323 "sodium-native": "^5.0.1"
1324 },
1325 "peerDependencies": {
1326 "sodium-javascript": "~0.8.0"
1327 },
1328 "peerDependenciesMeta": {
1329 "sodium-javascript": {
1330 "optional": true
1331 }
1332 }
1333 },
1334 "node_modules/sodium-universal": {
1335 "version": "4.0.1",
1336 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-4.0.1.tgz",
1337 "integrity": "sha512-sNp13PrxYLaUFHTGoDKkSDFvoEu51bfzE12RwGlqU1fcrkpAOK0NvizaJzOWV0Omtk9me2+Pnbjcf/l0efxuGQ==",
1338 "license": "MIT",
1339 "dependencies": {
1340 "sodium-native": "^4.0.0"
1341 },
1342 "peerDependencies": {
1343 "sodium-javascript": "~0.8.0"
1344 },
1345 "peerDependenciesMeta": {
1346 "sodium-javascript": {
1347 "optional": true
1348 }
1349 }
1350 },
1351 "node_modules/stackframe": {
1352 "version": "1.3.4",
1353 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
1354 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
1355 "dev": true,
1356 "license": "MIT"
1357 },
1358 "node_modules/streamx": {
1359 "version": "2.23.0",
1360 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
1361 "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
1362 "license": "MIT",
1363 "dependencies": {
1364 "events-universal": "^1.0.0",
1365 "fast-fifo": "^1.3.2",
1366 "text-decoder": "^1.1.0"
1367 }
1368 },
1369 "node_modules/test-tmp": {
1370 "version": "1.4.0",
1371 "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz",
1372 "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==",
1373 "dev": true,
1374 "license": "MIT",
1375 "dependencies": {
1376 "bare-fs": "^4.0.1",
1377 "bare-os": "^3.3.0",
1378 "bare-path": "^3.0.0"
1379 }
1380 },
1381 "node_modules/text-decoder": {
1382 "version": "1.2.6",
1383 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.6.tgz",
1384 "integrity": "sha512-27FeW5GQFDfw0FpwMQhMagB7BztOOlmjcSRi97t2oplhKVTZtp0DZbSegSaXS5IIC6mxMvBG4AR1Sgc6BX3CQg==",
1385 "license": "Apache-2.0",
1386 "dependencies": {
1387 "b4a": "^1.6.4"
1388 }
1389 },
1390 "node_modules/timeout-refresh": {
1391 "version": "2.0.1",
1392 "resolved": "https://registry.npmjs.org/timeout-refresh/-/timeout-refresh-2.0.1.tgz",
1393 "integrity": "sha512-SVqEcMZBsZF9mA78rjzCrYrUs37LMJk3ShZ851ygZYW1cMeIjs9mL57KO6Iv5mmjSQnOe/29/VAfGXo+oRCiVw==",
1394 "license": "MIT"
1395 },
1396 "node_modules/tmatch": {
1397 "version": "5.0.0",
1398 "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz",
1399 "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==",
1400 "dev": true,
1401 "license": "ISC",
1402 "engines": {
1403 "node": ">=8"
1404 }
1405 },
1406 "node_modules/unslab": {
1407 "version": "1.3.0",
1408 "resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz",
1409 "integrity": "sha512-YATkfKAFj47kTzmiQrWXMyRvaVrHsW6MEALa4bm+FhiA2YG4oira+Z3DXN6LrYOYn2Y8eO94Lwl9DOHjs1FpoQ==",
1410 "license": "Apache-2.0",
1411 "dependencies": {
1412 "b4a": "^1.6.6"
1413 }
1414 },
1415 "node_modules/v8-to-istanbul": {
1416 "version": "9.3.0",
1417 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
1418 "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
1419 "dev": true,
1420 "license": "ISC",
1421 "dependencies": {
1422 "@jridgewell/trace-mapping": "^0.3.12",
1423 "@types/istanbul-lib-coverage": "^2.0.1",
1424 "convert-source-map": "^2.0.0"
1425 },
1426 "engines": {
1427 "node": ">=10.12.0"
1428 }
1429 },
1430 "node_modules/varint": {
1431 "version": "5.0.0",
1432 "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz",
1433 "integrity": "sha512-gC13b/bWrqQoKY2EmROCZ+AR0jitc6DnDGaQ6Ls9QpKmuSgJB1eQ7H3KETtQm7qSdMWMKCmsshyCmUwMLh3OAA==",
1434 "license": "MIT"
1435 },
1436 "node_modules/which-runtime": {
1437 "version": "1.3.2",
1438 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
1439 "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
1440 "license": "Apache-2.0"
1441 },
1442 "node_modules/xache": {
1443 "version": "1.2.1",
1444 "resolved": "https://registry.npmjs.org/xache/-/xache-1.2.1.tgz",
1445 "integrity": "sha512-igRS6jPreJ54ABdzhh4mCDXcz+XMaWO2q1ABRV2yWYuk29jlp8VT7UBdCqNkX7rpYBbXsebVVKkwIuYZjyZNqA==",
1446 "license": "MIT"
1447 },
1448 "node_modules/z32": {
1449 "version": "1.1.0",
1450 "resolved": "https://registry.npmjs.org/z32/-/z32-1.1.0.tgz",
1451 "integrity": "sha512-1WUHy+VS6d0HPNspDxvLssBbeQjXMjSnpv0vH82vRAUfg847NmX3OXozp/hRP5jPhxBbrVzrgvAt+UsGNzRFQQ==",
1452 "license": "MIT",
1453 "dependencies": {
1454 "b4a": "^1.5.3"
1455 }
1456 }
1457 }
1458}
hyper-social/package.json +24 -0
……@@ -0,0 +1,24 @@
1{
2 "name": "hyper-social",
3 "version": "1.0.0",
4 "description": "P2P social hub — composes bridges, panels, and daemons into a unified system",
5 "type": "module",
6 "main": "index.js",
7 "pear": {
8 "name": "hyper-social",
9 "type": "terminal"
10 },
11 "scripts": {
12 "test": "brittle test/*.test.js"
13 },
14 "dependencies": {
15 "bridge-envelope": "file:../bridge-envelope",
16 "hyperbee": "^2.0.0",
17 "corestore": "^6.0.0",
18 "b4a": "^1.0.0"
19 },
20 "devDependencies": {
21 "brittle": "^3.0.0"
22 },
23 "license": "MIT"
24}
hyper-social/test/config.test.js +141 -0
……@@ -0,0 +1,141 @@
1import test from 'brittle'
2import Corestore from 'corestore'
3import Hyperbee from 'hyperbee'
4import { Config } from '../lib/config.js'
5import { tmpdir } from 'os'
6import { join } from 'path'
7import { randomBytes } from 'crypto'
8
9function tmpStorage () {
10 return join(tmpdir(), 'config-test-' + randomBytes(8).toString('hex'))
11}
12
13async function createConfig () {
14 const store = new Corestore(tmpStorage())
15 const bee = new Hyperbee(store.get({ name: 'test' }), {
16 keyEncoding: 'utf-8',
17 valueEncoding: 'json'
18 })
19 await bee.ready()
20 return { config: new Config(bee), store }
21}
22
23test('Config - set and get', async (t) => {
24 const { config, store } = await createConfig()
25
26 await config.set('bridge-irc', 'server', 'irc.libera.chat')
27 const val = await config.get('bridge-irc', 'server')
28 t.is(val, 'irc.libera.chat')
29
30 await store.close()
31})
32
33test('Config - get missing returns undefined', async (t) => {
34 const { config, store } = await createConfig()
35
36 const val = await config.get('nonexistent', 'key')
37 t.is(val, undefined)
38
39 await store.close()
40})
41
42test('Config - overwrite value', async (t) => {
43 const { config, store } = await createConfig()
44
45 await config.set('irc', 'channel', '#old')
46 await config.set('irc', 'channel', '#new')
47
48 t.is(await config.get('irc', 'channel'), '#new')
49
50 await store.close()
51})
52
53test('Config - del', async (t) => {
54 const { config, store } = await createConfig()
55
56 await config.set('irc', 'key', 'value')
57 await config.del('irc', 'key')
58
59 t.is(await config.get('irc', 'key'), undefined)
60
61 await store.close()
62})
63
64test('Config - getSection', async (t) => {
65 const { config, store } = await createConfig()
66
67 await config.set('irc', 'server', 'libera')
68 await config.set('irc', 'channel', '#test')
69 await config.set('irc', 'nick', 'bridge-bot')
70 await config.set('discord', 'token', 'abc123')
71
72 const irc = await config.getSection('irc')
73 t.is(irc.server, 'libera')
74 t.is(irc.channel, '#test')
75 t.is(irc.nick, 'bridge-bot')
76 t.is(Object.keys(irc).length, 3)
77
78 await store.close()
79})
80
81test('Config - listSections', async (t) => {
82 const { config, store } = await createConfig()
83
84 await config.set('irc', 'key', 'val')
85 await config.set('discord', 'key', 'val')
86 await config.set('mastodon', 'key', 'val')
87
88 const sections = await config.listSections()
89 t.is(sections.length, 3)
90 t.ok(sections.includes('irc'))
91 t.ok(sections.includes('discord'))
92 t.ok(sections.includes('mastodon'))
93
94 await store.close()
95})
96
97test('Config - setAll bulk', async (t) => {
98 const { config, store } = await createConfig()
99
100 await config.setAll('irc', {
101 server: 'libera',
102 channel: '#keet',
103 nick: 'bot'
104 })
105
106 t.is(await config.get('irc', 'server'), 'libera')
107 t.is(await config.get('irc', 'channel'), '#keet')
108 t.is(await config.get('irc', 'nick'), 'bot')
109
110 await store.close()
111})
112
113test('Config - exportAll', async (t) => {
114 const { config, store } = await createConfig()
115
116 await config.set('irc', 'server', 'libera')
117 await config.set('irc', 'channel', '#test')
118 await config.set('discord', 'token', 'abc')
119
120 const exported = await config.exportAll()
121 t.is(exported.irc.server, 'libera')
122 t.is(exported.irc.channel, '#test')
123 t.is(exported.discord.token, 'abc')
124
125 await store.close()
126})
127
128test('Config - complex values (objects)', async (t) => {
129 const { config, store } = await createConfig()
130
131 await config.set('irc', 'rooms', [
132 { channel: '#a', roomId: 'r1' },
133 { channel: '#b', roomId: 'r2' }
134 ])
135
136 const rooms = await config.get('irc', 'rooms')
137 t.is(rooms.length, 2)
138 t.is(rooms[0].channel, '#a')
139
140 await store.close()
141})
hyper-social/test/hub.test.js +299 -0
……@@ -0,0 +1,299 @@
1import test from 'brittle'
2import { Hub } from '../lib/hub.js'
3import { tmpdir } from 'os'
4import { join } from 'path'
5import { randomBytes } from 'crypto'
6
7function tmpStorage () {
8 return join(tmpdir(), 'hub-test-' + randomBytes(8).toString('hex'))
9}
10
11function mockBridge (id, opts = {}) {
12 const state = { started: false, messages: [] }
13 return {
14 type: 'bridge',
15 name: opts.name || id,
16 platform: opts.platform || id,
17 state,
18 start: async () => { state.started = true },
19 stop: async () => { state.started = false },
20 onBusMessage: (msg) => { state.messages.push(msg) },
21 setBus: (bus, sourceId) => {
22 state.bus = bus
23 state.sourceId = sourceId
24 }
25 }
26}
27
28test('Hub - ready', async (t) => {
29 const hub = new Hub(tmpStorage())
30 await hub.ready()
31
32 const s = hub.status()
33 t.absent(s.started)
34 t.is(s.services.total, 0)
35
36 await hub.close()
37})
38
39test('Hub - addService registers and wires', async (t) => {
40 const hub = new Hub(tmpStorage())
41 await hub.ready()
42
43 const irc = mockBridge('irc', { platform: 'irc' })
44 await hub.addService('irc', irc)
45
46 t.is(hub.services.count, 1)
47 t.ok(irc.state.bus) // bus was set
48 t.is(irc.state.sourceId, 'irc')
49
50 await hub.close()
51})
52
53test('Hub - addService with room link', async (t) => {
54 const hub = new Hub(tmpStorage())
55 await hub.ready()
56
57 const irc = mockBridge('irc')
58 await hub.addService('irc', irc, {
59 roomId: 'room-abc',
60 channel: '#test',
61 roomName: 'The Garage'
62 })
63
64 const services = await hub.rooms.getServices('room-abc')
65 t.is(services.length, 1)
66 t.is(services[0].serviceId, 'irc')
67
68 const room = await hub.rooms.getRoom('room-abc')
69 t.is(room.name, 'The Garage')
70
71 await hub.close()
72})
73
74test('Hub - addService with autoStart', async (t) => {
75 const hub = new Hub(tmpStorage())
76 await hub.ready()
77
78 const irc = mockBridge('irc')
79 await hub.addService('irc', irc, { autoStart: true })
80
81 t.ok(irc.state.started)
82 t.is(hub.services.get('irc').status, 'running')
83
84 await hub.close()
85})
86
87test('Hub - removeService cleans up', async (t) => {
88 const hub = new Hub(tmpStorage())
89 await hub.ready()
90
91 const irc = mockBridge('irc')
92 await hub.addService('irc', irc, {
93 roomId: 'room-1',
94 autoStart: true
95 })
96
97 await hub.removeService('irc')
98
99 t.is(hub.services.count, 0)
100 const services = await hub.rooms.getServices('room-1')
101 t.is(services.length, 0)
102
103 await hub.close()
104})
105
106test('Hub - message bus routing between services', async (t) => {
107 const hub = new Hub(tmpStorage())
108 await hub.ready()
109
110 const irc = mockBridge('irc')
111 const discord = mockBridge('discord')
112 const mastodon = mockBridge('mastodon')
113
114 await hub.addService('irc', irc)
115 await hub.addService('discord', discord)
116 await hub.addService('mastodon', mastodon)
117
118 // IRC sends a message — should reach discord and mastodon, not irc
119 hub.route({ source: 'irc', author: 'alice', text: 'hello', type: 'chat' })
120
121 t.is(irc.state.messages.length, 0) // source filtered out
122 t.is(discord.state.messages.length, 1)
123 t.is(mastodon.state.messages.length, 1)
124 t.is(discord.state.messages[0].author, 'alice')
125
126 await hub.close()
127})
128
129test('Hub - bus dedup across services', async (t) => {
130 const hub = new Hub(tmpStorage())
131 await hub.ready()
132
133 const irc = mockBridge('irc')
134 const discord = mockBridge('discord')
135
136 await hub.addService('irc', irc)
137 await hub.addService('discord', discord)
138
139 hub.route({ source: 'irc', author: 'alice', text: 'hello' })
140 hub.route({ source: 'irc', author: 'alice', text: 'hello' }) // deduped
141
142 t.is(discord.state.messages.length, 1)
143 t.is(hub.bus.stats.deduped, 1)
144
145 await hub.close()
146})
147
148test('Hub - startAll and stopAll', async (t) => {
149 const hub = new Hub(tmpStorage())
150 await hub.ready()
151
152 const irc = mockBridge('irc')
153 const discord = mockBridge('discord')
154
155 await hub.addService('irc', irc)
156 await hub.addService('discord', discord)
157
158 const startResults = await hub.startAll()
159 t.is(startResults.length, 2)
160 t.ok(startResults.every(r => r.ok))
161 t.ok(irc.state.started)
162 t.ok(discord.state.started)
163
164 const stopResults = await hub.stopAll()
165 t.is(stopResults.length, 2)
166 t.absent(irc.state.started)
167 t.absent(discord.state.started)
168
169 await hub.close()
170})
171
172test('Hub - graph view', async (t) => {
173 const hub = new Hub(tmpStorage())
174 await hub.ready()
175
176 const irc = mockBridge('irc', { platform: 'irc' })
177 const discord = mockBridge('discord', { platform: 'discord' })
178
179 await hub.addService('irc', irc, {
180 roomId: 'room-1',
181 autoStart: true
182 })
183 await hub.addService('discord', discord, {
184 roomId: 'room-1',
185 autoStart: true
186 })
187
188 const graph = await hub.graph()
189 t.is(graph.length, 1) // one room
190 t.is(graph[0].roomId, 'room-1')
191 t.is(graph[0].services.length, 2)
192 t.ok(graph[0].services.find(s => s.id === 'irc' && s.status === 'running'))
193 t.ok(graph[0].services.find(s => s.id === 'discord' && s.status === 'running'))
194
195 await hub.close()
196})
197
198test('Hub - graph with multiple rooms', async (t) => {
199 const hub = new Hub(tmpStorage())
200 await hub.ready()
201
202 await hub.addService('irc-1', mockBridge('irc-1'), { roomId: 'room-a' })
203 await hub.addService('irc-2', mockBridge('irc-2'), { roomId: 'room-b' })
204 await hub.addService('discord', mockBridge('discord'), { roomId: 'room-a' })
205
206 const graph = await hub.graph()
207 t.is(graph.length, 2) // two rooms
208
209 const roomA = graph.find(g => g.roomId === 'room-a')
210 t.is(roomA.services.length, 2)
211
212 const roomB = graph.find(g => g.roomId === 'room-b')
213 t.is(roomB.services.length, 1)
214
215 await hub.close()
216})
217
218test('Hub - status', async (t) => {
219 const hub = new Hub(tmpStorage())
220 await hub.ready()
221
222 await hub.addService('irc', mockBridge('irc'), { autoStart: true })
223 await hub.addService('discord', mockBridge('discord'))
224
225 hub.route({ source: 'irc', author: 'a', text: 'msg' })
226
227 const s = hub.status()
228 t.ok(s.started) // startAll was called implicitly via autoStart
229 t.is(s.services.total, 2)
230 t.is(s.bus.routed, 1)
231
232 await hub.close()
233})
234
235test('Hub - config persists', async (t) => {
236 const storage = tmpStorage()
237
238 const h1 = new Hub(storage)
239 await h1.ready()
240 await h1.config.set('irc', 'server', 'libera')
241 await h1.close()
242
243 const h2 = new Hub(storage)
244 await h2.ready()
245 const val = await h2.config.get('irc', 'server')
246 t.is(val, 'libera')
247 await h2.close()
248})
249
250test('Hub - identities persist', async (t) => {
251 const storage = tmpStorage()
252
253 const h1 = new Hub(storage)
254 await h1.ready()
255 await h1.identities.addIdentity('irc', 'alice', { alias: 'alice-real' })
256 await h1.close()
257
258 const h2 = new Hub(storage)
259 await h2.ready()
260 const id = await h2.identities.getIdentity('irc', 'alice')
261 t.is(id.alias, 'alice-real')
262 await h2.close()
263})
264
265test('Hub - room map persists', async (t) => {
266 const storage = tmpStorage()
267
268 const h1 = new Hub(storage)
269 await h1.ready()
270 await h1.rooms.link('room-1', 'irc', { platform: 'irc' })
271 await h1.close()
272
273 const h2 = new Hub(storage)
274 await h2.ready()
275 const linked = await h2.rooms.isLinked('room-1', 'irc')
276 t.ok(linked)
277 await h2.close()
278})
279
280test('Hub - close stops all services', async (t) => {
281 const hub = new Hub(tmpStorage())
282 await hub.ready()
283
284 const irc = mockBridge('irc')
285 await hub.addService('irc', irc, { autoStart: true })
286 t.ok(irc.state.started)
287
288 await hub.close()
289 t.absent(irc.state.started)
290})
291
292test('Hub - removeService nonexistent returns false', async (t) => {
293 const hub = new Hub(tmpStorage())
294 await hub.ready()
295
296 t.absent(await hub.removeService('ghost'))
297
298 await hub.close()
299})
hyper-social/test/identity-graph.test.js +198 -0
……@@ -0,0 +1,198 @@
1import test from 'brittle'
2import Corestore from 'corestore'
3import Hyperbee from 'hyperbee'
4import { IdentityGraph } from '../lib/identity-graph.js'
5import { tmpdir } from 'os'
6import { join } from 'path'
7import { randomBytes } from 'crypto'
8
9function tmpStorage () {
10 return join(tmpdir(), 'identity-test-' + randomBytes(8).toString('hex'))
11}
12
13async function createGraph () {
14 const store = new Corestore(tmpStorage())
15 const bee = new Hyperbee(store.get({ name: 'test' }), {
16 keyEncoding: 'utf-8',
17 valueEncoding: 'json'
18 })
19 await bee.ready()
20 return { graph: new IdentityGraph(bee), store }
21}
22
23test('IdentityGraph - add and get identity', async (t) => {
24 const { graph, store } = await createGraph()
25
26 const id = await graph.addIdentity('irc', 'alice', {
27 displayName: 'Alice',
28 metadata: { server: 'libera.chat' }
29 })
30
31 t.is(id.platform, 'irc')
32 t.is(id.handle, 'alice')
33 t.is(id.displayName, 'Alice')
34 t.is(id.metadata.server, 'libera.chat')
35
36 const fetched = await graph.getIdentity('irc', 'alice')
37 t.is(fetched.displayName, 'Alice')
38
39 await store.close()
40})
41
42test('IdentityGraph - update identity preserves fields', async (t) => {
43 const { graph, store } = await createGraph()
44
45 await graph.addIdentity('irc', 'alice', { displayName: 'Alice' })
46 await graph.addIdentity('irc', 'alice', { metadata: { verified: true } })
47
48 const id = await graph.getIdentity('irc', 'alice')
49 t.is(id.displayName, 'Alice') // preserved
50 t.ok(id.metadata.verified) // merged
51
52 await store.close()
53})
54
55test('IdentityGraph - link identities with alias', async (t) => {
56 const { graph, store } = await createGraph()
57
58 await graph.addIdentity('irc', 'alice', { alias: 'alice-real' })
59 await graph.addIdentity('mastodon', 'alice@mastodon.social', { alias: 'alice-real' })
60 await graph.addIdentity('keet', 'alice-key-123', { alias: 'alice-real' })
61
62 const linked = await graph.getLinked('alice-real')
63 t.is(linked.length, 3)
64 t.ok(linked.find(i => i.platform === 'irc'))
65 t.ok(linked.find(i => i.platform === 'mastodon'))
66 t.ok(linked.find(i => i.platform === 'keet'))
67
68 await store.close()
69})
70
71test('IdentityGraph - link via link method', async (t) => {
72 const { graph, store } = await createGraph()
73
74 await graph.addIdentity('irc', 'bob')
75 await graph.link('bob-real', 'irc', 'bob')
76 await graph.link('bob-real', 'discord', 'bob#1234')
77
78 const linked = await graph.getLinked('bob-real')
79 t.is(linked.length, 2)
80
81 await store.close()
82})
83
84test('IdentityGraph - resolve cross-platform', async (t) => {
85 const { graph, store } = await createGraph()
86
87 await graph.addIdentity('irc', 'carol', { alias: 'carol-person' })
88 await graph.addIdentity('mastodon', 'carol@social.coop', { alias: 'carol-person' })
89
90 const resolved = await graph.resolve('irc', 'carol', 'mastodon')
91 t.ok(resolved)
92 t.is(resolved.handle, 'carol@social.coop')
93
94 // Resolve to unknown platform returns null
95 const none = await graph.resolve('irc', 'carol', 'discord')
96 t.is(none, null)
97
98 await store.close()
99})
100
101test('IdentityGraph - resolve without alias returns null', async (t) => {
102 const { graph, store } = await createGraph()
103
104 await graph.addIdentity('irc', 'lonely')
105
106 const result = await graph.resolve('irc', 'lonely', 'mastodon')
107 t.is(result, null)
108
109 await store.close()
110})
111
112test('IdentityGraph - listPlatform', async (t) => {
113 const { graph, store } = await createGraph()
114
115 await graph.addIdentity('irc', 'alice')
116 await graph.addIdentity('irc', 'bob')
117 await graph.addIdentity('mastodon', 'carol@m.social')
118
119 const ircUsers = await graph.listPlatform('irc')
120 t.is(ircUsers.length, 2)
121
122 const mastoUsers = await graph.listPlatform('mastodon')
123 t.is(mastoUsers.length, 1)
124
125 await store.close()
126})
127
128test('IdentityGraph - listAliases', async (t) => {
129 const { graph, store } = await createGraph()
130
131 await graph.addIdentity('irc', 'alice', { alias: 'alice-real' })
132 await graph.addIdentity('irc', 'bob', { alias: 'bob-real' })
133
134 const aliases = await graph.listAliases()
135 t.is(aliases.length, 2)
136 t.ok(aliases.find(a => a.alias === 'alice-real'))
137 t.ok(aliases.find(a => a.alias === 'bob-real'))
138
139 await store.close()
140})
141
142test('IdentityGraph - removeIdentity', async (t) => {
143 const { graph, store } = await createGraph()
144
145 await graph.addIdentity('irc', 'temp', { alias: 'temp-alias' })
146 await graph.addIdentity('discord', 'temp#0', { alias: 'temp-alias' })
147
148 const ok = await graph.removeIdentity('irc', 'temp')
149 t.ok(ok)
150
151 const gone = await graph.getIdentity('irc', 'temp')
152 t.is(gone, null)
153
154 // Alias should still have the discord entry
155 const linked = await graph.getLinked('temp-alias')
156 t.is(linked.length, 1)
157 t.is(linked[0].platform, 'discord')
158
159 await store.close()
160})
161
162test('IdentityGraph - remove last identity cleans up alias', async (t) => {
163 const { graph, store } = await createGraph()
164
165 await graph.addIdentity('irc', 'solo', { alias: 'solo-alias' })
166
167 await graph.removeIdentity('irc', 'solo')
168
169 const linked = await graph.getLinked('solo-alias')
170 t.is(linked.length, 0)
171
172 await store.close()
173})
174
175test('IdentityGraph - remove nonexistent returns false', async (t) => {
176 const { graph, store } = await createGraph()
177
178 t.absent(await graph.removeIdentity('irc', 'ghost'))
179
180 await store.close()
181})
182
183test('IdentityGraph - stats', async (t) => {
184 const { graph, store } = await createGraph()
185
186 await graph.addIdentity('irc', 'alice', { alias: 'a' })
187 await graph.addIdentity('mastodon', 'alice@m', { alias: 'a' })
188 await graph.addIdentity('discord', 'bob#0', { alias: 'b' })
189
190 const s = await graph.stats()
191 t.is(s.identities, 3)
192 t.is(s.aliases, 2)
193 t.ok(s.platforms.includes('irc'))
194 t.ok(s.platforms.includes('mastodon'))
195 t.ok(s.platforms.includes('discord'))
196
197 await store.close()
198})
hyper-social/test/message-bus.test.js +141 -0
……@@ -0,0 +1,141 @@
1import test from 'brittle'
2import { MessageBus } from '../lib/message-bus.js'
3
4test('MessageBus - publish emits message', (t) => {
5 const bus = new MessageBus()
6 let received = null
7
8 bus.on('message', (msg) => { received = msg })
9
10 bus.publish({ source: 'irc', author: 'alice', text: 'hello' })
11 t.ok(received)
12 t.is(received.author, 'alice')
13 t.is(received.source, 'irc')
14})
15
16test('MessageBus - publish requires source', (t) => {
17 const bus = new MessageBus()
18 const ok = bus.publish({ author: 'alice', text: 'hello' })
19 t.is(ok, false)
20})
21
22test('MessageBus - typed events', (t) => {
23 const bus = new MessageBus()
24 let typed = null
25
26 bus.on('message:chat', (msg) => { typed = msg })
27
28 bus.publish({ source: 'irc', type: 'chat', text: 'hi' })
29 t.ok(typed)
30 t.is(typed.type, 'chat')
31})
32
33test('MessageBus - platform events', (t) => {
34 const bus = new MessageBus()
35 let platform = null
36
37 bus.on('platform:mastodon', (msg) => { platform = msg })
38
39 bus.publish({ source: 'masto-bridge', platform: 'mastodon', text: 'toot' })
40 t.ok(platform)
41 t.is(platform.platform, 'mastodon')
42})
43
44test('MessageBus - dedup identical messages', (t) => {
45 const bus = new MessageBus()
46 let count = 0
47
48 bus.on('message', () => { count++ })
49
50 bus.publish({ source: 'irc', author: 'alice', text: 'hello' })
51 bus.publish({ source: 'irc', author: 'alice', text: 'hello' })
52 bus.publish({ source: 'irc', author: 'alice', text: 'hello' })
53
54 t.is(count, 1)
55 t.is(bus.stats.deduped, 2)
56})
57
58test('MessageBus - different messages not deduped', (t) => {
59 const bus = new MessageBus()
60 let count = 0
61
62 bus.on('message', () => { count++ })
63
64 bus.publish({ source: 'irc', author: 'alice', text: 'msg 1' })
65 bus.publish({ source: 'irc', author: 'alice', text: 'msg 2' })
66 bus.publish({ source: 'irc', author: 'bob', text: 'msg 1' })
67
68 t.is(count, 3)
69})
70
71test('MessageBus - subscribe skips own source', (t) => {
72 const bus = new MessageBus()
73 let received = []
74
75 bus.subscribe('irc', (msg) => { received.push(msg) })
76
77 bus.publish({ source: 'irc', text: 'from irc' }) // should be skipped
78 bus.publish({ source: 'discord', text: 'from discord' }) // should arrive
79 bus.publish({ source: 'mastodon', text: 'from mastodon' }) // should arrive
80
81 t.is(received.length, 2)
82 t.is(received[0].source, 'discord')
83 t.is(received[1].source, 'mastodon')
84})
85
86test('MessageBus - unsubscribe', (t) => {
87 const bus = new MessageBus()
88 let count = 0
89
90 const ref = bus.subscribe('irc', () => { count++ })
91
92 bus.publish({ source: 'discord', text: 'before' })
93 t.is(count, 1)
94
95 bus.unsubscribe(ref)
96
97 bus.publish({ source: 'discord', text: 'after' })
98 t.is(count, 1)
99})
100
101test('MessageBus - subscriber error captured', (t) => {
102 const bus = new MessageBus()
103 let errorEvent = null
104
105 bus.on('error', (e) => { errorEvent = e })
106 bus.subscribe('discord', () => { throw new Error('boom') })
107
108 bus.publish({ source: 'irc', text: 'test' })
109 t.ok(errorEvent)
110 t.is(errorEvent.source, 'discord')
111 t.is(errorEvent.error, 'boom')
112 t.is(bus.stats.errors, 1)
113})
114
115test('MessageBus - stats', (t) => {
116 const bus = new MessageBus()
117
118 bus.publish({ source: 'a', text: '1' })
119 bus.publish({ source: 'a', text: '2' })
120 bus.publish({ source: 'a', text: '1' }) // deduped
121
122 const s = bus.stats
123 t.is(s.routed, 2)
124 t.is(s.deduped, 1)
125 t.is(s.seen, 2)
126})
127
128test('MessageBus - reset', (t) => {
129 const bus = new MessageBus()
130
131 bus.publish({ source: 'a', text: '1' })
132 bus.publish({ source: 'a', text: '1' }) // deduped first time
133
134 bus.reset()
135
136 // After reset, same message should be routable again
137 const ok = bus.publish({ source: 'a', text: '1' })
138 t.ok(ok)
139 t.is(bus.stats.routed, 1)
140 t.is(bus.stats.deduped, 0)
141})
hyper-social/test/room-map.test.js +176 -0
……@@ -0,0 +1,176 @@
1import test from 'brittle'
2import Corestore from 'corestore'
3import Hyperbee from 'hyperbee'
4import { RoomMap } from '../lib/room-map.js'
5import { tmpdir } from 'os'
6import { join } from 'path'
7import { randomBytes } from 'crypto'
8
9function tmpStorage () {
10 return join(tmpdir(), 'room-map-test-' + randomBytes(8).toString('hex'))
11}
12
13async function createRoomMap () {
14 const store = new Corestore(tmpStorage())
15 const bee = new Hyperbee(store.get({ name: 'test' }), {
16 keyEncoding: 'utf-8',
17 valueEncoding: 'json'
18 })
19 await bee.ready()
20 return { rooms: new RoomMap(bee), store }
21}
22
23test('RoomMap - link service to room', async (t) => {
24 const { rooms, store } = await createRoomMap()
25
26 const entry = await rooms.link('room-abc', 'irc-bridge', {
27 platform: 'irc',
28 channel: '#test',
29 roomName: 'The Garage'
30 })
31
32 t.is(entry.roomId, 'room-abc')
33 t.is(entry.serviceId, 'irc-bridge')
34 t.is(entry.platform, 'irc')
35 t.is(entry.direction, 'bidirectional')
36 t.ok(entry.enabled)
37
38 await store.close()
39})
40
41test('RoomMap - get services for room', async (t) => {
42 const { rooms, store } = await createRoomMap()
43
44 await rooms.link('room-1', 'irc', { platform: 'irc' })
45 await rooms.link('room-1', 'discord', { platform: 'discord' })
46 await rooms.link('room-2', 'mastodon', { platform: 'mastodon' })
47
48 const services = await rooms.getServices('room-1')
49 t.is(services.length, 2)
50 t.ok(services.find(s => s.serviceId === 'irc'))
51 t.ok(services.find(s => s.serviceId === 'discord'))
52
53 await store.close()
54})
55
56test('RoomMap - get rooms for service', async (t) => {
57 const { rooms, store } = await createRoomMap()
58
59 await rooms.link('room-1', 'irc', { platform: 'irc' })
60 await rooms.link('room-2', 'irc', { platform: 'irc' })
61 await rooms.link('room-3', 'discord', { platform: 'discord' })
62
63 const ircRooms = await rooms.getRooms('irc')
64 t.is(ircRooms.length, 2)
65
66 await store.close()
67})
68
69test('RoomMap - unlink', async (t) => {
70 const { rooms, store } = await createRoomMap()
71
72 await rooms.link('room-1', 'irc', {})
73 t.ok(await rooms.isLinked('room-1', 'irc'))
74
75 const ok = await rooms.unlink('room-1', 'irc')
76 t.ok(ok)
77 t.absent(await rooms.isLinked('room-1', 'irc'))
78
79 // Unlink nonexistent returns false
80 t.absent(await rooms.unlink('room-1', 'nope'))
81
82 await store.close()
83})
84
85test('RoomMap - isLinked', async (t) => {
86 const { rooms, store } = await createRoomMap()
87
88 await rooms.link('room-1', 'irc', {})
89
90 t.ok(await rooms.isLinked('room-1', 'irc'))
91 t.absent(await rooms.isLinked('room-1', 'discord'))
92 t.absent(await rooms.isLinked('room-2', 'irc'))
93
94 await store.close()
95})
96
97test('RoomMap - listAll', async (t) => {
98 const { rooms, store } = await createRoomMap()
99
100 await rooms.link('r1', 'a', {})
101 await rooms.link('r1', 'b', {})
102 await rooms.link('r2', 'a', {})
103
104 const all = await rooms.listAll()
105 t.is(all.length, 3)
106
107 await store.close()
108})
109
110test('RoomMap - room metadata stored', async (t) => {
111 const { rooms, store } = await createRoomMap()
112
113 await rooms.link('room-abc', 'irc', { roomName: 'The Garage' })
114
115 const room = await rooms.getRoom('room-abc')
116 t.ok(room)
117 t.is(room.name, 'The Garage')
118 t.is(room.id, 'room-abc')
119
120 await store.close()
121})
122
123test('RoomMap - listRooms', async (t) => {
124 const { rooms, store } = await createRoomMap()
125
126 await rooms.link('r1', 'a', { roomName: 'Room One' })
127 await rooms.link('r2', 'b', { roomName: 'Room Two' })
128
129 const list = await rooms.listRooms()
130 t.is(list.length, 2)
131
132 await store.close()
133})
134
135test('RoomMap - setEnabled', async (t) => {
136 const { rooms, store } = await createRoomMap()
137
138 await rooms.link('r1', 'irc', {})
139
140 const disabled = await rooms.setEnabled('r1', 'irc', false)
141 t.absent(disabled.enabled)
142
143 const enabled = await rooms.setEnabled('r1', 'irc', true)
144 t.ok(enabled.enabled)
145
146 await store.close()
147})
148
149test('RoomMap - setEnabled on nonexistent throws', async (t) => {
150 const { rooms, store } = await createRoomMap()
151
152 try {
153 await rooms.setEnabled('nope', 'nah', false)
154 t.fail('Should have thrown')
155 } catch (err) {
156 t.ok(err.message.includes('not found'))
157 }
158
159 await store.close()
160})
161
162test('RoomMap - direction options', async (t) => {
163 const { rooms, store } = await createRoomMap()
164
165 await rooms.link('r1', 'feed-in', { direction: 'in' })
166 await rooms.link('r1', 'feed-out', { direction: 'out' })
167
168 const services = await rooms.getServices('r1')
169 const inSvc = services.find(s => s.serviceId === 'feed-in')
170 const outSvc = services.find(s => s.serviceId === 'feed-out')
171
172 t.is(inSvc.direction, 'in')
173 t.is(outSvc.direction, 'out')
174
175 await store.close()
176})
hyper-social/test/service-registry.test.js +227 -0
……@@ -0,0 +1,227 @@
1import test from 'brittle'
2import { ServiceRegistry } from '../lib/service-registry.js'
3
4function mockService (opts = {}) {
5 return {
6 type: opts.type || 'bridge',
7 name: opts.name || 'test-service',
8 started: false,
9 start: async () => { mockService.started = true },
10 stop: async () => { mockService.started = false },
11 ...opts
12 }
13}
14
15test('ServiceRegistry - register', (t) => {
16 const reg = new ServiceRegistry()
17 const entry = reg.register('irc', mockService({ type: 'bridge', name: 'IRC Bridge' }))
18
19 t.is(entry.id, 'irc')
20 t.is(entry.type, 'bridge')
21 t.is(entry.status, 'registered')
22 t.is(reg.count, 1)
23})
24
25test('ServiceRegistry - duplicate register throws', (t) => {
26 const reg = new ServiceRegistry()
27 reg.register('irc', mockService())
28
29 try {
30 reg.register('irc', mockService())
31 t.fail('Should have thrown')
32 } catch (err) {
33 t.ok(err.message.includes('already registered'))
34 }
35})
36
37test('ServiceRegistry - start and stop', async (t) => {
38 const reg = new ServiceRegistry()
39 let started = false
40
41 reg.register('svc', {
42 type: 'bridge',
43 start: async () => { started = true },
44 stop: async () => { started = false }
45 })
46
47 await reg.start('svc')
48 t.ok(started)
49 t.is(reg.get('svc').status, 'running')
50 t.ok(reg.get('svc').startedAt > 0)
51
52 await reg.stop('svc')
53 t.absent(started)
54 t.is(reg.get('svc').status, 'stopped')
55})
56
57test('ServiceRegistry - start nonexistent throws', async (t) => {
58 const reg = new ServiceRegistry()
59
60 try {
61 await reg.start('nope')
62 t.fail('Should have thrown')
63 } catch (err) {
64 t.ok(err.message.includes('not found'))
65 }
66})
67
68test('ServiceRegistry - start error captured', async (t) => {
69 const reg = new ServiceRegistry()
70 reg.register('broken', {
71 type: 'bridge',
72 start: async () => { throw new Error('auth failed') }
73 })
74
75 try {
76 await reg.start('broken')
77 t.fail('Should have thrown')
78 } catch (err) {
79 t.is(err.message, 'auth failed')
80 }
81
82 t.is(reg.get('broken').status, 'error')
83 t.is(reg.get('broken').error, 'auth failed')
84})
85
86test('ServiceRegistry - unregister', async (t) => {
87 const reg = new ServiceRegistry()
88 reg.register('temp', mockService())
89
90 t.is(reg.count, 1)
91 const ok = await reg.unregister('temp')
92 t.ok(ok)
93 t.is(reg.count, 0)
94
95 const nope = await reg.unregister('nonexistent')
96 t.absent(nope)
97})
98
99test('ServiceRegistry - unregister stops running service', async (t) => {
100 const reg = new ServiceRegistry()
101 let stopped = false
102
103 reg.register('svc', {
104 type: 'bridge',
105 start: async () => {},
106 stop: async () => { stopped = true }
107 })
108
109 await reg.start('svc')
110 await reg.unregister('svc')
111 t.ok(stopped)
112})
113
114test('ServiceRegistry - list all', (t) => {
115 const reg = new ServiceRegistry()
116 reg.register('a', { type: 'bridge', name: 'A' })
117 reg.register('b', { type: 'panel', name: 'B' })
118 reg.register('c', { type: 'daemon', name: 'C' })
119
120 t.is(reg.list().length, 3)
121})
122
123test('ServiceRegistry - list by type', (t) => {
124 const reg = new ServiceRegistry()
125 reg.register('a', { type: 'bridge' })
126 reg.register('b', { type: 'panel' })
127 reg.register('c', { type: 'bridge' })
128
129 const bridges = reg.list({ type: 'bridge' })
130 t.is(bridges.length, 2)
131
132 const panels = reg.list({ type: 'panel' })
133 t.is(panels.length, 1)
134})
135
136test('ServiceRegistry - list by status', async (t) => {
137 const reg = new ServiceRegistry()
138 reg.register('a', { type: 'bridge', start: async () => {}, stop: async () => {} })
139 reg.register('b', { type: 'bridge' })
140
141 await reg.start('a')
142
143 const running = reg.list({ status: 'running' })
144 t.is(running.length, 1)
145 t.is(running[0].id, 'a')
146})
147
148test('ServiceRegistry - status summary', async (t) => {
149 const reg = new ServiceRegistry()
150 reg.register('a', { type: 'bridge', start: async () => {}, stop: async () => {} })
151 reg.register('b', { type: 'panel' })
152 reg.register('c', { type: 'bridge' })
153
154 await reg.start('a')
155
156 const s = reg.status()
157 t.is(s.total, 3)
158 t.is(s.byType.bridge, 2)
159 t.is(s.byType.panel, 1)
160 t.is(s.byStatus.running, 1)
161 t.is(s.byStatus.registered, 2)
162})
163
164test('ServiceRegistry - startAll and stopAll', async (t) => {
165 const reg = new ServiceRegistry()
166 let started = 0
167
168 for (const id of ['a', 'b', 'c']) {
169 reg.register(id, {
170 type: 'bridge',
171 start: async () => { started++ },
172 stop: async () => { started-- }
173 })
174 }
175
176 const startResults = await reg.startAll()
177 t.is(startResults.length, 3)
178 t.ok(startResults.every(r => r.ok))
179 t.is(started, 3)
180
181 const stopResults = await reg.stopAll()
182 t.is(stopResults.length, 3)
183 t.is(started, 0)
184})
185
186test('ServiceRegistry - startAll with partial failure', async (t) => {
187 const reg = new ServiceRegistry()
188
189 reg.register('good', {
190 type: 'bridge',
191 start: async () => {},
192 stop: async () => {}
193 })
194 reg.register('bad', {
195 type: 'bridge',
196 start: async () => { throw new Error('nope') }
197 })
198
199 const results = await reg.startAll()
200 t.is(results.length, 2)
201
202 const good = results.find(r => r.id === 'good')
203 t.ok(good.ok)
204
205 const bad = results.find(r => r.id === 'bad')
206 t.absent(bad.ok)
207 t.is(bad.error, 'nope')
208})
209
210test('ServiceRegistry - get returns null for unknown', (t) => {
211 const reg = new ServiceRegistry()
212 t.is(reg.get('nonexistent'), null)
213})
214
215test('ServiceRegistry - close fallback', async (t) => {
216 const reg = new ServiceRegistry()
217 let closed = false
218
219 reg.register('closeable', {
220 type: 'panel',
221 close: async () => { closed = true }
222 })
223
224 await reg.start('closeable') // no start method, still goes to running
225 await reg.stop('closeable')
226 t.ok(closed)
227})
hyper-wiki/index.js +134 -0
……@@ -0,0 +1,134 @@
1import { Wiki } from './lib/wiki.js'
2
3export { Wiki }
4
5// CLI entry point
6const isMain = typeof Pear !== 'undefined' || process.argv[1]?.includes('hyper-wiki')
7
8if (isMain && process.argv.length > 2) {
9 const args = parseArgs(typeof Pear !== 'undefined' ? Pear.config.args : process.argv.slice(2))
10
11 if (args.help) {
12 console.log(`
13hyper-wiki — P2P wiki with Hyperdrive storage
14
15Usage:
16 hyper-wiki create <slug> --title "Page Title" --content "Markdown content"
17 hyper-wiki read <slug>
18 hyper-wiki update <slug> --content "Updated content"
19 hyper-wiki delete <slug>
20 hyper-wiki list
21 hyper-wiki search <query>
22
23Options:
24 --storage <path> Storage directory (default: ./wiki-data)
25 --title <title> Page title
26 --content <content> Page content (markdown)
27 --author <name> Author name
28 --replicate Enable P2P replication via Hyperswarm
29 --help Show this help
30`)
31 process.exit(0)
32 }
33
34 run(args).catch((err) => {
35 console.error('Error:', err.message)
36 process.exit(1)
37 })
38}
39
40async function run (args) {
41 const storage = args.storage || './wiki-data'
42 const wiki = new Wiki(storage, { replicate: args.replicate })
43 await wiki.ready()
44
45 const cmd = args._[0]
46 const slug = args._[1]
47
48 switch (cmd) {
49 case 'create':
50 case 'update': {
51 if (!slug) throw new Error('Usage: hyper-wiki create <slug>')
52 const content = args.content || ''
53 const meta = await wiki.create(slug, content, {
54 title: args.title || slug,
55 author: args.author || ''
56 })
57 console.log(`${cmd === 'create' ? 'Created' : 'Updated'}: ${slug} (v${meta.version})`)
58 break
59 }
60 case 'read': {
61 if (!slug) throw new Error('Usage: hyper-wiki read <slug>')
62 const page = await wiki.read(slug)
63 if (!page) {
64 console.log(`Page not found: ${slug}`)
65 } else {
66 console.log(`# ${page.title} (v${page.version})`)
67 console.log(`Author: ${page.author || 'unknown'} | Updated: ${new Date(page.updated).toISOString()}`)
68 console.log('---')
69 console.log(page.content)
70 }
71 break
72 }
73 case 'delete': {
74 if (!slug) throw new Error('Usage: hyper-wiki delete <slug>')
75 await wiki.remove(slug)
76 console.log(`Deleted: ${slug}`)
77 break
78 }
79 case 'list': {
80 const pages = await wiki.list()
81 if (pages.length === 0) {
82 console.log('No pages yet.')
83 } else {
84 for (const p of pages) {
85 console.log(` ${p.slug.padEnd(30)} v${p.version} ${new Date(p.updated).toISOString().slice(0, 10)} ${p.title}`)
86 }
87 }
88 break
89 }
90 case 'search': {
91 const query = args._.slice(1).join(' ')
92 if (!query) throw new Error('Usage: hyper-wiki search <query>')
93 const results = await wiki.search(query)
94 if (results.length === 0) {
95 console.log(`No results for: ${query}`)
96 } else {
97 for (const r of results) {
98 console.log(` ${r.slug.padEnd(30)} score:${r.score} ${r.title}`)
99 }
100 }
101 break
102 }
103 default:
104 console.error(`Unknown command: ${cmd}`)
105 console.error('Run with --help for usage')
106 process.exit(1)
107 }
108
109 await wiki.close()
110}
111
112function parseArgs (argv) {
113 const result = { _: [] }
114 for (let i = 0; i < argv.length; i++) {
115 const arg = argv[i]
116 if (arg === '--help') {
117 result.help = true
118 } else if (arg === '--replicate') {
119 result.replicate = true
120 } else if (arg.startsWith('--')) {
121 const key = arg.slice(2)
122 const next = argv[i + 1]
123 if (next && !next.startsWith('--')) {
124 result[key] = next
125 i++
126 } else {
127 result[key] = true
128 }
129 } else {
130 result._.push(arg)
131 }
132 }
133 return result
134}
hyper-wiki/lib/search.js +105 -0
……@@ -0,0 +1,105 @@
1import Hyperbee from 'hyperbee'
2
3class SearchIndex {
4 constructor (store) {
5 this._store = store
6 this._db = null
7 this._ready = false
8 }
9
10 async ready () {
11 if (this._ready) return
12
13 const core = this._store.get({ name: 'wiki-search-index' })
14 this._db = new Hyperbee(core, {
15 keyEncoding: 'utf-8',
16 valueEncoding: 'utf-8'
17 })
18 await this._db.ready()
19 this._ready = true
20 }
21
22 async index (slug, content, meta = {}) {
23 await this.ready()
24
25 const title = meta.title || slug
26 const text = `${title}\n${content}`.toLowerCase()
27
28 // Extract unique words (simple tokenizer)
29 const words = new Set(text.match(/\w{2,}/g) || [])
30
31 // Store word → slug mappings
32 for (const word of words) {
33 const key = `word/${word}/${slug}`
34 await this._db.put(key, title)
35 }
36
37 // Store slug → word count for ranking
38 await this._db.put(`doc/${slug}`, JSON.stringify({
39 title,
40 wordCount: words.size,
41 updated: meta.updated || Date.now()
42 }))
43 }
44
45 async remove (slug) {
46 await this.ready()
47
48 // Remove all word entries for this slug
49 const batch = this._db.batch()
50 for await (const entry of this._db.createReadStream({
51 gte: 'word/',
52 lt: 'word/\xff'
53 })) {
54 if (entry.key.endsWith(`/${slug}`)) {
55 await batch.del(entry.key)
56 }
57 }
58 await batch.del(`doc/${slug}`)
59 await batch.flush()
60 }
61
62 async search (query, limit = 20) {
63 await this.ready()
64
65 const terms = query.toLowerCase().match(/\w{2,}/g) || []
66 if (terms.length === 0) return []
67
68 // Find pages matching any term
69 const scores = new Map()
70
71 for (const term of terms) {
72 for await (const entry of this._db.createReadStream({
73 gte: `word/${term}/`,
74 lt: `word/${term}/\xff`
75 })) {
76 const slug = entry.key.split('/').pop()
77 scores.set(slug, (scores.get(slug) || 0) + 1)
78 }
79 }
80
81 // Sort by score (more matching terms = higher)
82 const results = [...scores.entries()]
83 .sort((a, b) => b[1] - a[1])
84 .slice(0, limit)
85
86 // Fetch metadata
87 const pages = []
88 for (const [slug, score] of results) {
89 const docEntry = await this._db.get(`doc/${slug}`)
90 let meta = { title: slug }
91 if (docEntry) {
92 try { meta = JSON.parse(docEntry.value) } catch {}
93 }
94 pages.push({ slug, score, title: meta.title })
95 }
96
97 return pages
98 }
99
100 async close () {
101 // Hyperbee closes with corestore
102 }
103}
104
105export { SearchIndex }
hyper-wiki/lib/wiki-store.js +128 -0
……@@ -0,0 +1,128 @@
1import Hyperdrive from 'hyperdrive'
2import b4a from 'b4a'
3
4class WikiStore {
5 constructor (store, opts = {}) {
6 this._store = store // Corestore instance (shared)
7 this._key = opts.key || null
8 this._drive = null
9 this._ready = false
10 }
11
12 async ready () {
13 if (this._ready) return
14
15 this._drive = this._key
16 ? new Hyperdrive(this._store, this._key)
17 : new Hyperdrive(this._store)
18
19 await this._drive.ready()
20 this._ready = true
21 }
22
23 get key () {
24 return this._drive?.key || null
25 }
26
27 get discoveryKey () {
28 return this._drive?.discoveryKey || null
29 }
30
31 _pagePath (slug) {
32 return `/wiki/${slug}.md`
33 }
34
35 _metaPath (slug) {
36 return `/wiki/${slug}.meta.json`
37 }
38
39 async put (slug, content, meta = {}) {
40 await this.ready()
41
42 const now = Date.now()
43 const existing = await this.getMeta(slug)
44
45 const metadata = {
46 title: meta.title || existing?.title || slug,
47 author: meta.author || existing?.author || '',
48 created: existing?.created || now,
49 updated: now,
50 tags: meta.tags || existing?.tags || [],
51 version: (existing?.version || 0) + 1
52 }
53
54 await this._drive.put(this._pagePath(slug), b4a.from(content))
55 await this._drive.put(this._metaPath(slug), b4a.from(JSON.stringify(metadata)))
56
57 return metadata
58 }
59
60 async get (slug) {
61 await this.ready()
62
63 const buf = await this._drive.get(this._pagePath(slug))
64 if (!buf) return null
65
66 const meta = await this.getMeta(slug)
67 return {
68 slug,
69 content: b4a.toString(buf),
70 ...meta
71 }
72 }
73
74 async getMeta (slug) {
75 await this.ready()
76
77 const buf = await this._drive.get(this._metaPath(slug))
78 if (!buf) return null
79
80 try {
81 return JSON.parse(b4a.toString(buf))
82 } catch {
83 return null
84 }
85 }
86
87 async del (slug) {
88 await this.ready()
89
90 await this._drive.del(this._pagePath(slug))
91 await this._drive.del(this._metaPath(slug))
92 }
93
94 async exists (slug) {
95 await this.ready()
96 const entry = await this._drive.entry(this._pagePath(slug))
97 return entry !== null
98 }
99
100 async list () {
101 await this.ready()
102
103 const pages = []
104 for await (const entry of this._drive.list('/wiki/')) {
105 const name = entry.key
106 if (name.endsWith('.md')) {
107 const slug = name.replace('/wiki/', '').replace('.md', '')
108 const meta = await this.getMeta(slug)
109 pages.push({
110 slug,
111 title: meta?.title || slug,
112 updated: meta?.updated || 0,
113 version: meta?.version || 1
114 })
115 }
116 }
117
118 pages.sort((a, b) => b.updated - a.updated)
119 return pages
120 }
121
122 async close () {
123 if (this._drive) await this._drive.close()
124 this._ready = false
125 }
126}
127
128export { WikiStore }
hyper-wiki/lib/wiki.js +95 -0
……@@ -0,0 +1,95 @@
1import Corestore from 'corestore'
2import Hyperswarm from 'hyperswarm'
3import { WikiStore } from './wiki-store.js'
4import { SearchIndex } from './search.js'
5
6class Wiki {
7 constructor (storage, opts = {}) {
8 this._storage = storage
9 this._key = opts.key || null
10 this._store = null
11 this._wiki = null
12 this._search = null
13 this._swarm = null
14 this._ready = false
15 this._replicate = opts.replicate ?? false
16 }
17
18 async ready () {
19 if (this._ready) return
20
21 // Single shared Corestore for all components
22 this._store = new Corestore(this._storage)
23
24 this._wiki = new WikiStore(this._store, { key: this._key })
25 await this._wiki.ready()
26
27 this._search = new SearchIndex(this._store)
28 await this._search.ready()
29
30 if (this._replicate) {
31 this._swarm = new Hyperswarm()
32 this._swarm.on('connection', (conn) => {
33 this._store.replicate(conn)
34 })
35 const topic = this._wiki.discoveryKey
36 if (topic) {
37 this._swarm.join(topic, { server: true, client: true })
38 }
39 }
40
41 this._ready = true
42 }
43
44 get key () {
45 return this._wiki?.key || null
46 }
47
48 async create (slug, content, meta = {}) {
49 await this.ready()
50 const result = await this._wiki.put(slug, content, meta)
51 await this._search.index(slug, content, { ...meta, ...result })
52 return result
53 }
54
55 async read (slug) {
56 await this.ready()
57 return this._wiki.get(slug)
58 }
59
60 async update (slug, content, meta = {}) {
61 return this.create(slug, content, meta)
62 }
63
64 async remove (slug) {
65 await this.ready()
66 await this._wiki.del(slug)
67 await this._search.remove(slug)
68 }
69
70 async exists (slug) {
71 await this.ready()
72 return this._wiki.exists(slug)
73 }
74
75 async list () {
76 await this.ready()
77 return this._wiki.list()
78 }
79
80 async search (query, limit = 20) {
81 await this.ready()
82 return this._search.search(query, limit)
83 }
84
85 async close () {
86 if (this._swarm) {
87 await this._swarm.destroy()
88 }
89 await this._wiki.close()
90 if (this._store) await this._store.close()
91 this._ready = false
92 }
93}
94
95export { Wiki }
hyper-wiki/package-lock.json +1813 -0
……@@ -0,0 +1,1813 @@
1{
2 "name": "hyper-wiki",
3 "version": "1.0.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "hyper-wiki",
9 "version": "1.0.0",
10 "license": "MIT",
11 "dependencies": {
12 "b4a": "^1.0.0",
13 "corestore": "^6.0.0",
14 "hyperbee": "^2.0.0",
15 "hypercore": "^10.0.0",
16 "hyperdrive": "^11.0.0",
17 "hyperswarm": "^4.0.0"
18 },
19 "devDependencies": {
20 "brittle": "^3.0.0"
21 }
22 },
23 "node_modules/@hyperswarm/secret-stream": {
24 "version": "6.9.1",
25 "resolved": "https://registry.npmjs.org/@hyperswarm/secret-stream/-/secret-stream-6.9.1.tgz",
26 "integrity": "sha512-xb0S5y3YJwBakD77JOGBHlBxdp63mHClZoXBYoLv+9wH8e054ESKlmQptWqjJK5dv5VMUIVYOJB4MaOpB0JdGw==",
27 "license": "Apache-2.0",
28 "dependencies": {
29 "b4a": "^1.1.0",
30 "hypercore-crypto": "^3.3.1",
31 "noise-curve-ed": "^2.0.1",
32 "noise-handshake": "^4.0.0",
33 "sodium-secretstream": "^1.1.0",
34 "sodium-universal": "^5.0.0",
35 "streamx": "^2.14.0",
36 "timeout-refresh": "^2.0.0",
37 "unslab": "^1.3.0"
38 }
39 },
40 "node_modules/@hyperswarm/secret-stream/node_modules/sodium-native": {
41 "version": "5.0.10",
42 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
43 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
44 "license": "MIT",
45 "dependencies": {
46 "require-addon": "^1.1.0",
47 "which-runtime": "^1.2.1"
48 },
49 "engines": {
50 "bare": ">=1.16.0"
51 }
52 },
53 "node_modules/@hyperswarm/secret-stream/node_modules/sodium-universal": {
54 "version": "5.0.1",
55 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
56 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
57 "license": "MIT",
58 "dependencies": {
59 "sodium-native": "^5.0.1"
60 },
61 "peerDependencies": {
62 "sodium-javascript": "~0.8.0"
63 },
64 "peerDependenciesMeta": {
65 "sodium-javascript": {
66 "optional": true
67 }
68 }
69 },
70 "node_modules/@jridgewell/resolve-uri": {
71 "version": "3.1.2",
72 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
73 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
74 "dev": true,
75 "license": "MIT",
76 "engines": {
77 "node": ">=6.0.0"
78 }
79 },
80 "node_modules/@jridgewell/sourcemap-codec": {
81 "version": "1.5.5",
82 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
83 "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
84 "dev": true,
85 "license": "MIT"
86 },
87 "node_modules/@jridgewell/trace-mapping": {
88 "version": "0.3.31",
89 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
90 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
91 "dev": true,
92 "license": "MIT",
93 "dependencies": {
94 "@jridgewell/resolve-uri": "^3.1.0",
95 "@jridgewell/sourcemap-codec": "^1.4.14"
96 }
97 },
98 "node_modules/@types/istanbul-lib-coverage": {
99 "version": "2.0.6",
100 "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
101 "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
102 "dev": true,
103 "license": "MIT"
104 },
105 "node_modules/adaptive-timeout": {
106 "version": "1.0.1",
107 "resolved": "https://registry.npmjs.org/adaptive-timeout/-/adaptive-timeout-1.0.1.tgz",
108 "integrity": "sha512-+seGiBtHqbnyZ0Quq/ZGxxwP66153WBABawa07/QeyuPFzbduPBjiGQAyCiriiLDzt3dkbMBskSlfqtRwblK8Q==",
109 "license": "Apache-2.0",
110 "dependencies": {
111 "xache": "^1.2.1"
112 }
113 },
114 "node_modules/b4a": {
115 "version": "1.7.4",
116 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.4.tgz",
117 "integrity": "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug==",
118 "license": "Apache-2.0",
119 "peerDependencies": {
120 "react-native-b4a": "*"
121 },
122 "peerDependenciesMeta": {
123 "react-native-b4a": {
124 "optional": true
125 }
126 }
127 },
128 "node_modules/bare-addon-resolve": {
129 "version": "1.10.0",
130 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
131 "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
132 "license": "Apache-2.0",
133 "dependencies": {
134 "bare-module-resolve": "^1.10.0",
135 "bare-semver": "^1.0.0"
136 },
137 "peerDependencies": {
138 "bare-url": "*"
139 },
140 "peerDependenciesMeta": {
141 "bare-url": {
142 "optional": true
143 }
144 }
145 },
146 "node_modules/bare-ansi-escapes": {
147 "version": "2.2.3",
148 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
149 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
150 "dev": true,
151 "license": "Apache-2.0",
152 "dependencies": {
153 "bare-stream": "^2.6.5"
154 },
155 "peerDependencies": {
156 "bare-buffer": "*"
157 },
158 "peerDependenciesMeta": {
159 "bare-buffer": {
160 "optional": true
161 }
162 }
163 },
164 "node_modules/bare-assert": {
165 "version": "1.2.0",
166 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
167 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
168 "dev": true,
169 "license": "Apache-2.0",
170 "dependencies": {
171 "bare-inspect": "^3.1.2"
172 }
173 },
174 "node_modules/bare-bundle": {
175 "version": "1.10.0",
176 "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
177 "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
178 "dev": true,
179 "license": "Apache-2.0",
180 "peerDependencies": {
181 "bare-buffer": "*",
182 "bare-url": "*"
183 },
184 "peerDependenciesMeta": {
185 "bare-buffer": {
186 "optional": true
187 },
188 "bare-url": {
189 "optional": true
190 }
191 }
192 },
193 "node_modules/bare-cov": {
194 "version": "1.2.1",
195 "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz",
196 "integrity": "sha512-SHNqb/WG9udUEj8m5qGSehoUQ9o0Xzv8nrf4HIdmG+KNU/XdhPLhTP5VPrHnSAMAXhAzEB4Q+2IYZ0x9qPzN1g==",
197 "dev": true,
198 "license": "Apache-2.0",
199 "dependencies": {
200 "bare-fs": "^4.1.2",
201 "bare-inspector": "^4.0.1",
202 "bare-path": "^3.0.0",
203 "bare-process": "^4.2.1",
204 "bare-url": "^2.1.5",
205 "bare-v8-to-istanbul": "^1.0.2",
206 "picomatch": "^4.0.2",
207 "which-runtime": "^1.2.1"
208 }
209 },
210 "node_modules/bare-crypto": {
211 "version": "1.13.0",
212 "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.0.tgz",
213 "integrity": "sha512-RQl13yD+YTACWUZHMMck0C6LNjXgGgRyhVC557ag0xIrkxXf4IQwpnc8WOB58f4k5kSzKhIjy05oW3HUBrFpSQ==",
214 "dev": true,
215 "license": "Apache-2.0",
216 "dependencies": {
217 "bare-stream": "^2.6.3"
218 },
219 "peerDependencies": {
220 "bare-buffer": "*"
221 },
222 "peerDependenciesMeta": {
223 "bare-buffer": {
224 "optional": true
225 }
226 }
227 },
228 "node_modules/bare-debug-log": {
229 "version": "2.0.0",
230 "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
231 "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
232 "dev": true,
233 "license": "Apache-2.0",
234 "dependencies": {
235 "bare-os": "^3.0.1"
236 }
237 },
238 "node_modules/bare-dns": {
239 "version": "2.1.4",
240 "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
241 "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==",
242 "dev": true,
243 "license": "Apache-2.0",
244 "engines": {
245 "bare": ">=1.7.0"
246 }
247 },
248 "node_modules/bare-encoding": {
249 "version": "1.0.3",
250 "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
251 "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
252 "dev": true,
253 "license": "Apache-2.0",
254 "peerDependencies": {
255 "bare-buffer": "*"
256 },
257 "peerDependenciesMeta": {
258 "bare-buffer": {
259 "optional": true
260 }
261 }
262 },
263 "node_modules/bare-env": {
264 "version": "3.0.0",
265 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
266 "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
267 "dev": true,
268 "license": "Apache-2.0",
269 "dependencies": {
270 "bare-os": "^3.0.1"
271 }
272 },
273 "node_modules/bare-events": {
274 "version": "2.8.2",
275 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
276 "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
277 "license": "Apache-2.0",
278 "peerDependencies": {
279 "bare-abort-controller": "*"
280 },
281 "peerDependenciesMeta": {
282 "bare-abort-controller": {
283 "optional": true
284 }
285 }
286 },
287 "node_modules/bare-format": {
288 "version": "1.0.1",
289 "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.1.tgz",
290 "integrity": "sha512-1oS+LZrWK6tnYnvNSHDGljc2MPunRxwhpFriuCgzNF+oklrnwmBKD91tS0yt+jpl2j3UgcSDzBIMiVTvLs9A8w==",
291 "dev": true,
292 "license": "Apache-2.0",
293 "dependencies": {
294 "bare-inspect": "^3.0.0"
295 }
296 },
297 "node_modules/bare-fs": {
298 "version": "4.5.4",
299 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz",
300 "integrity": "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==",
301 "license": "Apache-2.0",
302 "dependencies": {
303 "bare-events": "^2.5.4",
304 "bare-path": "^3.0.0",
305 "bare-stream": "^2.6.4",
306 "bare-url": "^2.2.2",
307 "fast-fifo": "^1.3.2"
308 },
309 "engines": {
310 "bare": ">=1.16.0"
311 },
312 "peerDependencies": {
313 "bare-buffer": "*"
314 },
315 "peerDependenciesMeta": {
316 "bare-buffer": {
317 "optional": true
318 }
319 }
320 },
321 "node_modules/bare-hrtime": {
322 "version": "2.1.1",
323 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
324 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
325 "dev": true,
326 "license": "Apache-2.0"
327 },
328 "node_modules/bare-http-parser": {
329 "version": "1.0.1",
330 "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.0.1.tgz",
331 "integrity": "sha512-A3LTDTcELcmNJ3g5liIaS038v/BQxOhA9cjhBESn7eoV7QCuMoIRBKLDadDe08flxyLbxI2f+1l2MZ/5+HnKPA==",
332 "dev": true,
333 "license": "Apache-2.0"
334 },
335 "node_modules/bare-http1": {
336 "version": "4.2.3",
337 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.2.3.tgz",
338 "integrity": "sha512-zyvHF3xzvBJNpuw0QydE9etTn2AyC/TRVDWTo8Ed3LQEEGaFOkv6KHhaJPlf4eWEeTekk41Fowie+fTsXoNxkA==",
339 "dev": true,
340 "license": "Apache-2.0",
341 "dependencies": {
342 "bare-events": "^2.6.0",
343 "bare-http-parser": "^1.0.0",
344 "bare-stream": "^2.3.0",
345 "bare-tcp": "^2.2.0"
346 },
347 "peerDependencies": {
348 "bare-buffer": "*",
349 "bare-url": "*"
350 },
351 "peerDependenciesMeta": {
352 "bare-buffer": {
353 "optional": true
354 },
355 "bare-url": {
356 "optional": true
357 }
358 }
359 },
360 "node_modules/bare-https": {
361 "version": "2.1.2",
362 "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.2.tgz",
363 "integrity": "sha512-Q+TTydUDsuKQJvh8dX2dvOXCR9fM3xR5TBmKaFrs5p7Lj7XbKX7v4vIUJ36H0SXg2xCOQxXKIbjwrLg5tfJNYg==",
364 "dev": true,
365 "license": "Apache-2.0",
366 "dependencies": {
367 "bare-http1": "^4.0.0",
368 "bare-tcp": "^2.2.0",
369 "bare-tls": "^2.0.0"
370 }
371 },
372 "node_modules/bare-inspect": {
373 "version": "3.1.4",
374 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
375 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
376 "dev": true,
377 "license": "Apache-2.0",
378 "dependencies": {
379 "bare-ansi-escapes": "^2.1.0",
380 "bare-type": "^1.0.0"
381 },
382 "engines": {
383 "bare": ">=1.18.0"
384 }
385 },
386 "node_modules/bare-inspector": {
387 "version": "4.0.3",
388 "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-4.0.3.tgz",
389 "integrity": "sha512-yYk6+7tTVKJKHOja5+QBn989ETv8oVsmwoH0nCM+QYC6w5MC03GC4aQNK+lipAZSz1EaAj42WZ8Wewjpy0L9TA==",
390 "dev": true,
391 "license": "Apache-2.0",
392 "dependencies": {
393 "bare-events": "^2.1.0",
394 "bare-http1": "^4.0.0",
395 "bare-stream": "^2.0.0",
396 "bare-url": "^2.0.0",
397 "bare-ws": "^2.0.0"
398 },
399 "engines": {
400 "bare": ">=1.2.0"
401 },
402 "peerDependencies": {
403 "bare-tcp": "*"
404 },
405 "peerDependenciesMeta": {
406 "bare-tcp": {
407 "optional": true
408 }
409 }
410 },
411 "node_modules/bare-module": {
412 "version": "6.1.3",
413 "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz",
414 "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==",
415 "dev": true,
416 "license": "Apache-2.0",
417 "dependencies": {
418 "bare-bundle": "^1.3.0",
419 "bare-module-lexer": "^1.0.0",
420 "bare-module-resolve": "^1.8.0",
421 "bare-path": "^3.0.0",
422 "bare-url": "^2.0.1"
423 },
424 "engines": {
425 "bare": ">=1.23.0"
426 },
427 "peerDependencies": {
428 "bare-buffer": "*"
429 },
430 "peerDependenciesMeta": {
431 "bare-buffer": {
432 "optional": true
433 }
434 }
435 },
436 "node_modules/bare-module-lexer": {
437 "version": "1.4.7",
438 "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
439 "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
440 "dev": true,
441 "license": "Apache-2.0",
442 "dependencies": {
443 "require-addon": "^1.0.2"
444 },
445 "peerDependencies": {
446 "bare-buffer": "*"
447 },
448 "peerDependenciesMeta": {
449 "bare-buffer": {
450 "optional": true
451 }
452 }
453 },
454 "node_modules/bare-module-resolve": {
455 "version": "1.12.1",
456 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz",
457 "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==",
458 "license": "Apache-2.0",
459 "dependencies": {
460 "bare-semver": "^1.0.0"
461 },
462 "peerDependencies": {
463 "bare-url": "*"
464 },
465 "peerDependenciesMeta": {
466 "bare-url": {
467 "optional": true
468 }
469 }
470 },
471 "node_modules/bare-net": {
472 "version": "2.2.0",
473 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.2.0.tgz",
474 "integrity": "sha512-UF7cAbHsGE+H6uEqWF5IULBow1x58chZz4g3ALgHtv7wZsFcCbRDt0JKWEumf5Oma3QWS1Q6aLi0Rpll8RElMg==",
475 "dev": true,
476 "license": "Apache-2.0",
477 "dependencies": {
478 "bare-events": "^2.2.2",
479 "bare-pipe": "^4.0.0",
480 "bare-stream": "^2.0.0",
481 "bare-tcp": "^2.0.0"
482 }
483 },
484 "node_modules/bare-os": {
485 "version": "3.6.2",
486 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
487 "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
488 "license": "Apache-2.0",
489 "engines": {
490 "bare": ">=1.14.0"
491 }
492 },
493 "node_modules/bare-path": {
494 "version": "3.0.0",
495 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
496 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
497 "license": "Apache-2.0",
498 "dependencies": {
499 "bare-os": "^3.0.1"
500 }
501 },
502 "node_modules/bare-pipe": {
503 "version": "4.1.3",
504 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.3.tgz",
505 "integrity": "sha512-DqQsx93rAzre6yJ9T6l/Vgh+X+bntkVMB1X5ggtXjXtqtMmF2Y2RVlCzxxy/09R6yeR9FSWBEUIaMYJL1/5VDA==",
506 "dev": true,
507 "license": "Apache-2.0",
508 "dependencies": {
509 "bare-events": "^2.0.0",
510 "bare-stream": "^2.0.0"
511 },
512 "engines": {
513 "bare": ">=1.16.0"
514 }
515 },
516 "node_modules/bare-process": {
517 "version": "4.2.2",
518 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.2.2.tgz",
519 "integrity": "sha512-ikzEw+HGLB+2lS/WLVEsqi8BXBwzleG3n7DYI0v6/YNklvNabOIGlLd1dof+7Jw5ob4jsBRPtyflweVahY7rFA==",
520 "dev": true,
521 "license": "Apache-2.0",
522 "dependencies": {
523 "bare-env": "^3.0.0",
524 "bare-events": "^2.3.1",
525 "bare-hrtime": "^2.0.0",
526 "bare-os": "^3.5.0",
527 "bare-pipe": "^4.0.0",
528 "bare-signals": "^4.0.0",
529 "bare-tty": "^5.0.0"
530 }
531 },
532 "node_modules/bare-semver": {
533 "version": "1.0.2",
534 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz",
535 "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==",
536 "license": "Apache-2.0"
537 },
538 "node_modules/bare-signals": {
539 "version": "4.2.0",
540 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz",
541 "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==",
542 "dev": true,
543 "license": "Apache-2.0",
544 "dependencies": {
545 "bare-events": "^2.5.3",
546 "bare-os": "^3.3.1"
547 },
548 "engines": {
549 "bare": ">=1.7.0"
550 }
551 },
552 "node_modules/bare-stream": {
553 "version": "2.7.0",
554 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
555 "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
556 "license": "Apache-2.0",
557 "dependencies": {
558 "streamx": "^2.21.0"
559 },
560 "peerDependencies": {
561 "bare-buffer": "*",
562 "bare-events": "*"
563 },
564 "peerDependenciesMeta": {
565 "bare-buffer": {
566 "optional": true
567 },
568 "bare-events": {
569 "optional": true
570 }
571 }
572 },
573 "node_modules/bare-subprocess": {
574 "version": "5.2.2",
575 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.2.tgz",
576 "integrity": "sha512-L6oXgQ1aWs25RtG5Ky0bDD06p3RAcVVrDDMWb1DfXpHtyEWxamcyIWUbSykxMTWrpLlmSj6ytbb6yoKehGFfmw==",
577 "dev": true,
578 "license": "Apache-2.0",
579 "dependencies": {
580 "bare-env": "^3.0.0",
581 "bare-events": "^2.5.4",
582 "bare-os": "^3.0.1",
583 "bare-pipe": "^4.0.0",
584 "bare-url": "^2.2.2"
585 },
586 "engines": {
587 "bare": ">=1.7.0"
588 },
589 "peerDependencies": {
590 "bare-buffer": "*"
591 },
592 "peerDependenciesMeta": {
593 "bare-buffer": {
594 "optional": true
595 }
596 }
597 },
598 "node_modules/bare-tcp": {
599 "version": "2.2.4",
600 "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.4.tgz",
601 "integrity": "sha512-vGjfMkyhS1lxp+Y4+gXRDR1ijZMK2cKx0RBwtYKUuZbD90ARCoM6bAiTa1HBztumCIC+NXb8eXdMFXyuYwlPyw==",
602 "dev": true,
603 "license": "Apache-2.0",
604 "dependencies": {
605 "bare-dns": "^2.0.4",
606 "bare-events": "^2.5.4",
607 "bare-stream": "^2.6.4"
608 },
609 "engines": {
610 "bare": ">=1.16.0"
611 }
612 },
613 "node_modules/bare-tls": {
614 "version": "2.1.7",
615 "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.1.7.tgz",
616 "integrity": "sha512-h6wcNXQdBeTX7fed9tjPp0/9cA/QfcBTv3ItgjnbUk4rWAU8bEFalZCZnUDdCK/t9zrNfJ+yvcPx4D/1Y6biyA==",
617 "dev": true,
618 "license": "Apache-2.0",
619 "dependencies": {
620 "bare-net": "^2.0.1",
621 "bare-stream": "^2.6.4"
622 },
623 "engines": {
624 "bare": ">=1.7.0"
625 }
626 },
627 "node_modules/bare-tty": {
628 "version": "5.0.3",
629 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.0.3.tgz",
630 "integrity": "sha512-jW24RBWRZOMHuSEWC9mh8wUKO5WUNl0UWUTNPn3yZ8qkOqwa8vaV2dR3a+BvblONPk7V35wuvK9P8508+judAg==",
631 "dev": true,
632 "license": "Apache-2.0",
633 "dependencies": {
634 "bare-events": "^2.2.0",
635 "bare-signals": "^4.0.0",
636 "bare-stream": "^2.0.0"
637 },
638 "engines": {
639 "bare": ">=1.16.0"
640 }
641 },
642 "node_modules/bare-type": {
643 "version": "1.1.0",
644 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
645 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
646 "dev": true,
647 "license": "Apache-2.0",
648 "engines": {
649 "bare": ">=1.2.0"
650 }
651 },
652 "node_modules/bare-url": {
653 "version": "2.3.2",
654 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
655 "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
656 "license": "Apache-2.0",
657 "peer": true,
658 "dependencies": {
659 "bare-path": "^3.0.0"
660 }
661 },
662 "node_modules/bare-utils": {
663 "version": "1.5.1",
664 "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.5.1.tgz",
665 "integrity": "sha512-mxCkFvmDU3mlD/mb+pT64kKXOsx2KMsWLQbngN1LB+NOXfhfnRnyvpy3VZc6m7gzQxe57Bsi+aTCBqA4/S3elQ==",
666 "dev": true,
667 "license": "Apache-2.0",
668 "dependencies": {
669 "bare-debug-log": "^2.0.0",
670 "bare-encoding": "^1.0.0",
671 "bare-format": "^1.0.0",
672 "bare-inspect": "^3.0.0",
673 "bare-type": "^1.0.6"
674 }
675 },
676 "node_modules/bare-v8-to-istanbul": {
677 "version": "1.0.2",
678 "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
679 "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==",
680 "dev": true,
681 "license": "Apache-2.0",
682 "dependencies": {
683 "bare-assert": "^1.0.2",
684 "bare-fs": "^4.1.2",
685 "bare-module": "^6.1.2",
686 "bare-path": "^3.0.0",
687 "bare-process": "^4.2.0",
688 "bare-url": "^2.1.5",
689 "bare-utils": "^1.2.0",
690 "v8-to-istanbul": "^9.3.0",
691 "which-runtime": "^1.2.1"
692 }
693 },
694 "node_modules/bare-ws": {
695 "version": "2.0.4",
696 "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.0.4.tgz",
697 "integrity": "sha512-SQMXzBYna9dRj57Dz1/ag+VWHCRXbfCjMHgyfM2F2lhkVLzMjnVSZP72aVeFWPFqe494Rd70Kzhe2JElGwFlJQ==",
698 "dev": true,
699 "license": "Apache-2.0",
700 "dependencies": {
701 "bare-crypto": "^1.2.0",
702 "bare-events": "^2.3.1",
703 "bare-http1": "^4.0.0",
704 "bare-https": "^2.0.0",
705 "bare-stream": "^2.1.2"
706 },
707 "peerDependencies": {
708 "bare-buffer": "*",
709 "bare-url": "*"
710 },
711 "peerDependenciesMeta": {
712 "bare-buffer": {
713 "optional": true
714 },
715 "bare-url": {
716 "optional": true
717 }
718 }
719 },
720 "node_modules/big-sparse-array": {
721 "version": "1.0.3",
722 "resolved": "https://registry.npmjs.org/big-sparse-array/-/big-sparse-array-1.0.3.tgz",
723 "integrity": "sha512-6RjV/3mSZORlMdpUaQ6rUSpG637cZm0//E54YYGtQg1c1O+AbZP8UTdJ/TchsDZcTVLmyWZcseBfp2HBeXUXOQ==",
724 "license": "MIT"
725 },
726 "node_modules/binary-stream-equals": {
727 "version": "1.0.0",
728 "resolved": "https://registry.npmjs.org/binary-stream-equals/-/binary-stream-equals-1.0.0.tgz",
729 "integrity": "sha512-xiUT5LGfD8JiLhbXiG+ByOnbgb9f2ssRLfZDQMl3nZdf89EotQZGZuMkDN8J3n46emabE7RnJ1q0r7Hv3INExw==",
730 "license": "MIT",
731 "dependencies": {
732 "b4a": "^1.3.1"
733 }
734 },
735 "node_modules/bits-to-bytes": {
736 "version": "1.3.0",
737 "resolved": "https://registry.npmjs.org/bits-to-bytes/-/bits-to-bytes-1.3.0.tgz",
738 "integrity": "sha512-OJoHTpFXS9bXHBCekGTByf3MqM8CGblBDIduKQeeVVeiU9dDWywSSirXIBYGgg3d1zbVuvnMa1vD4r6PA0kOKg==",
739 "license": "ISC",
740 "dependencies": {
741 "b4a": "^1.5.0"
742 }
743 },
744 "node_modules/blind-relay": {
745 "version": "1.4.0",
746 "resolved": "https://registry.npmjs.org/blind-relay/-/blind-relay-1.4.0.tgz",
747 "integrity": "sha512-6xt7fDfCs6eGmNNym6I9N42jmjcMQn2qwwOVnkP9ZnrkXFk6c4/tdO1xqRmDEzKzV8gigd+DVdCUG/RUYnen7Q==",
748 "license": "Apache-2.0",
749 "dependencies": {
750 "b4a": "^1.6.4",
751 "bare-events": "^2.2.0",
752 "bits-to-bytes": "^1.3.0",
753 "compact-encoding": "^2.12.0",
754 "compact-encoding-bitfield": "^1.0.0",
755 "protomux": "^3.5.1",
756 "sodium-universal": "^5.0.0",
757 "streamx": "^2.15.1"
758 }
759 },
760 "node_modules/blind-relay/node_modules/sodium-native": {
761 "version": "5.0.10",
762 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
763 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
764 "license": "MIT",
765 "dependencies": {
766 "require-addon": "^1.1.0",
767 "which-runtime": "^1.2.1"
768 },
769 "engines": {
770 "bare": ">=1.16.0"
771 }
772 },
773 "node_modules/blind-relay/node_modules/sodium-universal": {
774 "version": "5.0.1",
775 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
776 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
777 "license": "MIT",
778 "dependencies": {
779 "sodium-native": "^5.0.1"
780 },
781 "peerDependencies": {
782 "sodium-javascript": "~0.8.0"
783 },
784 "peerDependenciesMeta": {
785 "sodium-javascript": {
786 "optional": true
787 }
788 }
789 },
790 "node_modules/bogon": {
791 "version": "1.2.0",
792 "resolved": "https://registry.npmjs.org/bogon/-/bogon-1.2.0.tgz",
793 "integrity": "sha512-FqOBr/1VMzCOsoJd+fzNUMarUYki2+TKt07A2+xaulsNx4r53iJ7MV5k0jbqg7W2U0CsLqxCZOrFibdG6h6HSg==",
794 "license": "MIT",
795 "dependencies": {
796 "compact-encoding": "^2.11.0",
797 "compact-encoding-net": "^1.2.0"
798 }
799 },
800 "node_modules/brittle": {
801 "version": "3.19.1",
802 "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz",
803 "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==",
804 "dev": true,
805 "license": "Apache-2.0",
806 "dependencies": {
807 "b4a": "^1.6.0",
808 "bare-assert": "^1.0.2",
809 "bare-cov": "^1.1.0",
810 "bare-fs": "^4.1.6",
811 "bare-os": "^3.6.1",
812 "bare-path": "^3.0.0",
813 "bare-process": "^4.2.1",
814 "bare-subprocess": "^5.0.0",
815 "bare-url": "^2.1.6",
816 "error-stack-parser": "^2.1.4",
817 "globbie": "^1.0.2",
818 "paparam": "^1.6.2",
819 "same-object": "^1.0.2",
820 "test-tmp": "^1.4.0",
821 "tmatch": "^5.0.0"
822 },
823 "bin": {
824 "brittle": "bin/node.js",
825 "brittle-bare": "bin/bare.js",
826 "brittle-node": "bin/node.js",
827 "brittle-pear": "bin/pear.js"
828 }
829 },
830 "node_modules/codecs": {
831 "version": "3.1.0",
832 "resolved": "https://registry.npmjs.org/codecs/-/codecs-3.1.0.tgz",
833 "integrity": "sha512-Dqx8NwvBvnMeuPQdVKy/XEF71igjR5apxBvCGeV0pP1tXadOiaLvDTXt7xh+/5wI1ASB195mXQGJbw3Ml4YDWQ==",
834 "license": "MIT",
835 "dependencies": {
836 "b4a": "^1.6.3"
837 }
838 },
839 "node_modules/compact-encoding": {
840 "version": "2.18.0",
841 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-2.18.0.tgz",
842 "integrity": "sha512-goACAOlhMI2xo5jGOMUDfOLnGdRE1jGfyZ+zie8N5114nHrbPIqf6GLUtzbLof6DSyrERlYRm3EcBplte5LcQw==",
843 "license": "Apache-2.0",
844 "dependencies": {
845 "b4a": "^1.3.0"
846 }
847 },
848 "node_modules/compact-encoding-bitfield": {
849 "version": "1.0.0",
850 "resolved": "https://registry.npmjs.org/compact-encoding-bitfield/-/compact-encoding-bitfield-1.0.0.tgz",
851 "integrity": "sha512-3nMVKUg+PF72UHfainmCL8uKvyWfxsjqOtUY+HiMPGLPCTjnwzoKfFAMo1Ad7nwTPdjBqtGK5b3BOFTFW4EBTg==",
852 "license": "ISC",
853 "dependencies": {
854 "compact-encoding": "^2.4.1"
855 }
856 },
857 "node_modules/compact-encoding-net": {
858 "version": "1.2.0",
859 "resolved": "https://registry.npmjs.org/compact-encoding-net/-/compact-encoding-net-1.2.0.tgz",
860 "integrity": "sha512-LVXpNpF7PGQeHRVVLGgYWzuVoYAaDZvKUsUxRioGfkotzvOh4AzoQF1HBH3zMNaSnx7gJXuUr3hkjnijaH/Eng==",
861 "license": "ISC",
862 "dependencies": {
863 "compact-encoding": "^2.4.1"
864 }
865 },
866 "node_modules/convert-source-map": {
867 "version": "2.0.0",
868 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
869 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
870 "dev": true,
871 "license": "MIT"
872 },
873 "node_modules/corestore": {
874 "version": "6.18.4",
875 "resolved": "https://registry.npmjs.org/corestore/-/corestore-6.18.4.tgz",
876 "integrity": "sha512-KL1cOUyj3DFRNzeVRekGPRiGN4sIfGo7BsncSJQQBT3Rh2y1nQ32MOR9X5DlOMlWIT4txwqBzhzam6+JXtwFVw==",
877 "license": "MIT",
878 "dependencies": {
879 "b4a": "^1.6.4",
880 "hypercore": "^10.37.10",
881 "hypercore-crypto": "^3.4.0",
882 "hypercore-id-encoding": "^1.2.0",
883 "read-write-mutexify": "^2.1.0",
884 "ready-resource": "^1.0.0",
885 "safety-catch": "^1.0.1",
886 "sodium-universal": "^4.0.0",
887 "xache": "^1.1.0"
888 }
889 },
890 "node_modules/crc-native": {
891 "version": "1.1.8",
892 "resolved": "https://registry.npmjs.org/crc-native/-/crc-native-1.1.8.tgz",
893 "integrity": "sha512-R/RYbD8lhG5M1k0iLX49tJdwHM0ipevJPpZw2Wk7b4EShyvx2P0uaq79xyRpcknjiVGiUfjFA8Gyy0mOlhABvw==",
894 "license": "Apache-2.0",
895 "optional": true,
896 "dependencies": {
897 "require-addon": "^1.1.0"
898 }
899 },
900 "node_modules/crc-universal": {
901 "version": "1.0.4",
902 "resolved": "https://registry.npmjs.org/crc-universal/-/crc-universal-1.0.4.tgz",
903 "integrity": "sha512-CE9xWEI6Gd5V0Bdmj5NoWH3d7+EIe4zUpy1sv6uvKYznzsDP1vhiWsTBmL6q9IrH2P6RRshp+8AkhR4CMtY5Hg==",
904 "license": "ISC",
905 "optionalDependencies": {
906 "crc-native": "^1.0.3"
907 }
908 },
909 "node_modules/debounceify": {
910 "version": "1.1.0",
911 "resolved": "https://registry.npmjs.org/debounceify/-/debounceify-1.1.0.tgz",
912 "integrity": "sha512-eKuHDVfJVg+u/0nPy8P+fhnLgbyuTgVxuCRrS/R7EpDSMMkBDgSes41MJtSAY1F1hcqfHz3Zy/qpqHHIp/EhdA==",
913 "license": "MIT"
914 },
915 "node_modules/dht-rpc": {
916 "version": "6.26.1",
917 "resolved": "https://registry.npmjs.org/dht-rpc/-/dht-rpc-6.26.1.tgz",
918 "integrity": "sha512-FzveCZDP000Ks1aRlgpwM6sLGgMFnoE3FxO0BAf9XlvLo9dogq/CHjB1+I2BtyZSa4ZthAkaUN+JX9rFpO8gig==",
919 "license": "MIT",
920 "dependencies": {
921 "adaptive-timeout": "^1.0.1",
922 "b4a": "^1.6.1",
923 "bare-events": "^2.2.0",
924 "compact-encoding": "^2.11.0",
925 "compact-encoding-net": "^1.2.0",
926 "fast-fifo": "^1.1.0",
927 "kademlia-routing-table": "^1.0.1",
928 "nat-sampler": "^1.0.1",
929 "sodium-universal": "^5.0.0",
930 "streamx": "^2.13.2",
931 "time-ordered-set": "^2.0.0",
932 "udx-native": "^1.5.3"
933 }
934 },
935 "node_modules/dht-rpc/node_modules/sodium-native": {
936 "version": "5.0.10",
937 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
938 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
939 "license": "MIT",
940 "dependencies": {
941 "require-addon": "^1.1.0",
942 "which-runtime": "^1.2.1"
943 },
944 "engines": {
945 "bare": ">=1.16.0"
946 }
947 },
948 "node_modules/dht-rpc/node_modules/sodium-universal": {
949 "version": "5.0.1",
950 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
951 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
952 "license": "MIT",
953 "dependencies": {
954 "sodium-native": "^5.0.1"
955 },
956 "peerDependencies": {
957 "sodium-javascript": "~0.8.0"
958 },
959 "peerDependenciesMeta": {
960 "sodium-javascript": {
961 "optional": true
962 }
963 }
964 },
965 "node_modules/error-stack-parser": {
966 "version": "2.1.4",
967 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
968 "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
969 "dev": true,
970 "license": "MIT",
971 "dependencies": {
972 "stackframe": "^1.3.4"
973 }
974 },
975 "node_modules/events-universal": {
976 "version": "1.0.1",
977 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
978 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
979 "license": "Apache-2.0",
980 "dependencies": {
981 "bare-events": "^2.7.0"
982 }
983 },
984 "node_modules/fast-fifo": {
985 "version": "1.3.2",
986 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
987 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
988 "license": "MIT"
989 },
990 "node_modules/flat-tree": {
991 "version": "1.13.0",
992 "resolved": "https://registry.npmjs.org/flat-tree/-/flat-tree-1.13.0.tgz",
993 "integrity": "sha512-fT3HIuCPwHhFgJ20QYzDHgUG0zMmFg5cHvFiFo5h+QMSJ28TihsEVY0f8HGliuO+pOzmvjMx1odToeaEWkTnyQ==",
994 "license": "MIT"
995 },
996 "node_modules/fs-native-extensions": {
997 "version": "1.4.5",
998 "resolved": "https://registry.npmjs.org/fs-native-extensions/-/fs-native-extensions-1.4.5.tgz",
999 "integrity": "sha512-ekV0T//iDm4AvhOcuPaHpxub4DI7HvY5ucLJVDvi7T2J+NZkQ9S6MuvgP0yeQvoqNUaAGyLjVYb1905BF9bpmg==",
1000 "license": "Apache-2.0",
1001 "optional": true,
1002 "dependencies": {
1003 "require-addon": "^1.1.0",
1004 "which-runtime": "^1.2.0"
1005 }
1006 },
1007 "node_modules/globbie": {
1008 "version": "1.0.3",
1009 "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz",
1010 "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==",
1011 "dev": true,
1012 "license": "Apache-2.0",
1013 "dependencies": {
1014 "bare-fs": "^4.1.2",
1015 "bare-path": "^3.0.0",
1016 "bare-process": "^4.2.1",
1017 "picomatch": "^4.0.2"
1018 }
1019 },
1020 "node_modules/hyperbee": {
1021 "version": "2.27.3",
1022 "resolved": "https://registry.npmjs.org/hyperbee/-/hyperbee-2.27.3.tgz",
1023 "integrity": "sha512-PXURH2U4juUZyJRKHTrY5z1zX851pmI1Q0jfv5F/hCIErDt/ND8jOZuxc3hfOLM9f0W3qJEDTMlV5AJBkVPy8w==",
1024 "license": "MIT",
1025 "dependencies": {
1026 "b4a": "^1.6.0",
1027 "codecs": "^3.0.0",
1028 "debounceify": "^1.0.0",
1029 "hypercore-errors": "^1.0.0",
1030 "mutexify": "^1.4.0",
1031 "protocol-buffers-encodings": "^1.2.0",
1032 "rache": "^1.0.0",
1033 "ready-resource": "^1.0.0",
1034 "resolve-reject-promise": "^1.1.0",
1035 "safety-catch": "^1.0.2",
1036 "streamx": "^2.12.4",
1037 "unslab": "^1.2.0"
1038 }
1039 },
1040 "node_modules/hyperblobs": {
1041 "version": "2.8.0",
1042 "resolved": "https://registry.npmjs.org/hyperblobs/-/hyperblobs-2.8.0.tgz",
1043 "integrity": "sha512-s9txNdHAdDMZjOn7B5nPOKXcUe/IGnzgWYhczAQs3TxEAcXFoXTBAtBd0HjorLa0pfvcSgq+v61cW2Chr4LACA==",
1044 "license": "Apache-2.0",
1045 "dependencies": {
1046 "b4a": "^1.6.1",
1047 "bare-events": "^2.5.0",
1048 "hypercore-errors": "^1.1.1",
1049 "mutexify": "^1.4.0",
1050 "speedometer": "^1.1.0",
1051 "streamx": "^2.13.2"
1052 }
1053 },
1054 "node_modules/hypercore": {
1055 "version": "10.38.2",
1056 "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-10.38.2.tgz",
1057 "integrity": "sha512-2vx42g2RKdP/o2XzlxKfvgEaZ3+h/B855gCR8iscUDnxUoOZej7vXZ2buex0sweDUEW5aEfMxFR6o5hncio3Nw==",
1058 "license": "MIT",
1059 "dependencies": {
1060 "@hyperswarm/secret-stream": "^6.0.0",
1061 "b4a": "^1.1.0",
1062 "bare-events": "^2.2.0",
1063 "big-sparse-array": "^1.0.3",
1064 "compact-encoding": "^2.11.0",
1065 "crc-universal": "^1.0.2",
1066 "fast-fifo": "^1.3.0",
1067 "flat-tree": "^1.9.0",
1068 "hypercore-crypto": "^3.2.1",
1069 "hypercore-errors": "^1.2.0",
1070 "hypercore-id-encoding": "^1.2.0",
1071 "is-options": "^1.0.1",
1072 "protomux": "^3.5.0",
1073 "quickbit-universal": "^2.2.0",
1074 "random-access-file": "^4.0.0",
1075 "random-array-iterator": "^1.0.0",
1076 "safety-catch": "^1.0.1",
1077 "sodium-universal": "^4.0.0",
1078 "streamx": "^2.12.4",
1079 "unslab": "^1.3.0",
1080 "xache": "^1.1.0",
1081 "z32": "^1.0.0"
1082 }
1083 },
1084 "node_modules/hypercore-crypto": {
1085 "version": "3.6.1",
1086 "resolved": "https://registry.npmjs.org/hypercore-crypto/-/hypercore-crypto-3.6.1.tgz",
1087 "integrity": "sha512-ltIz2uDwy9pO/ZGTvqcjzyBkvt6O4cVm4r/nNxh0GFs/RbQtqP/i4wCvLEdmU7ptgtnw7fI67WYD1aHPuv4OVA==",
1088 "license": "MIT",
1089 "dependencies": {
1090 "b4a": "^1.6.6",
1091 "compact-encoding": "^2.15.0",
1092 "sodium-universal": "^5.0.0"
1093 }
1094 },
1095 "node_modules/hypercore-crypto/node_modules/sodium-native": {
1096 "version": "5.0.10",
1097 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
1098 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
1099 "license": "MIT",
1100 "dependencies": {
1101 "require-addon": "^1.1.0",
1102 "which-runtime": "^1.2.1"
1103 },
1104 "engines": {
1105 "bare": ">=1.16.0"
1106 }
1107 },
1108 "node_modules/hypercore-crypto/node_modules/sodium-universal": {
1109 "version": "5.0.1",
1110 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1111 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1112 "license": "MIT",
1113 "dependencies": {
1114 "sodium-native": "^5.0.1"
1115 },
1116 "peerDependencies": {
1117 "sodium-javascript": "~0.8.0"
1118 },
1119 "peerDependenciesMeta": {
1120 "sodium-javascript": {
1121 "optional": true
1122 }
1123 }
1124 },
1125 "node_modules/hypercore-errors": {
1126 "version": "1.5.0",
1127 "resolved": "https://registry.npmjs.org/hypercore-errors/-/hypercore-errors-1.5.0.tgz",
1128 "integrity": "sha512-5KQ/SuDxsvet+7qWA35Ay6zdD9WyAHQoyWHGcPUTbmJBd300gvNIJoi3oma7kp4TTCSzii6qYumNZe/s0j/saQ==",
1129 "license": "Apache-2.0",
1130 "dependencies": {
1131 "hypercore-id-encoding": "^1.3.0"
1132 }
1133 },
1134 "node_modules/hypercore-id-encoding": {
1135 "version": "1.3.0",
1136 "resolved": "https://registry.npmjs.org/hypercore-id-encoding/-/hypercore-id-encoding-1.3.0.tgz",
1137 "integrity": "sha512-W6sHdGo5h7LXEsoWfKf/KfuROZmZRQDlGqJF2EPHW+noCK66Vvr0+zE6cL0vqQi18s0kQPeN7Sq3QyR0Ytc2VQ==",
1138 "license": "Apache-2.0",
1139 "dependencies": {
1140 "b4a": "^1.5.3",
1141 "z32": "^1.0.0"
1142 }
1143 },
1144 "node_modules/hyperdht": {
1145 "version": "6.29.0",
1146 "resolved": "https://registry.npmjs.org/hyperdht/-/hyperdht-6.29.0.tgz",
1147 "integrity": "sha512-okIpelpIhRTBDCCe2CNWfLHbVSQ5DPbl1Npb1zet8SqrkCsYF27RHJ04IaYqiigHI9y0KZgjOHdZbejalbFVoQ==",
1148 "license": "MIT",
1149 "dependencies": {
1150 "@hyperswarm/secret-stream": "^6.6.2",
1151 "b4a": "^1.3.1",
1152 "bare-events": "^2.2.0",
1153 "blind-relay": "^1.3.0",
1154 "bogon": "^1.0.0",
1155 "compact-encoding": "^2.4.1",
1156 "compact-encoding-net": "^1.0.1",
1157 "dht-rpc": "^6.15.1",
1158 "hypercore-crypto": "^3.3.0",
1159 "hypercore-id-encoding": "^1.2.0",
1160 "noise-curve-ed": "^2.0.0",
1161 "noise-handshake": "^4.0.0",
1162 "record-cache": "^1.1.1",
1163 "safety-catch": "^1.0.1",
1164 "signal-promise": "^1.0.3",
1165 "sodium-universal": "^5.0.1",
1166 "streamx": "^2.16.1",
1167 "unslab": "^1.3.0",
1168 "xache": "^1.1.0"
1169 },
1170 "bin": {
1171 "hyperdht": "bin.js"
1172 }
1173 },
1174 "node_modules/hyperdht/node_modules/sodium-native": {
1175 "version": "5.0.10",
1176 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
1177 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
1178 "license": "MIT",
1179 "dependencies": {
1180 "require-addon": "^1.1.0",
1181 "which-runtime": "^1.2.1"
1182 },
1183 "engines": {
1184 "bare": ">=1.16.0"
1185 }
1186 },
1187 "node_modules/hyperdht/node_modules/sodium-universal": {
1188 "version": "5.0.1",
1189 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1190 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1191 "license": "MIT",
1192 "dependencies": {
1193 "sodium-native": "^5.0.1"
1194 },
1195 "peerDependencies": {
1196 "sodium-javascript": "~0.8.0"
1197 },
1198 "peerDependenciesMeta": {
1199 "sodium-javascript": {
1200 "optional": true
1201 }
1202 }
1203 },
1204 "node_modules/hyperdrive": {
1205 "version": "11.13.4",
1206 "resolved": "https://registry.npmjs.org/hyperdrive/-/hyperdrive-11.13.4.tgz",
1207 "integrity": "sha512-PN0gClSRx+QBbsLGnGqo2ei9TavJN8d6D3jv2yNMlu7zgXSnqR6QcGo8mgyKnrzgR336oNfvQ2C2Y6cY5klScg==",
1208 "license": "Apache-2.0",
1209 "dependencies": {
1210 "hyperbee": "^2.11.1",
1211 "hyperblobs": "^2.3.0",
1212 "hypercore": "^10.33.0",
1213 "hypercore-errors": "^1.0.0",
1214 "is-options": "^1.0.2",
1215 "mirror-drive": "^1.2.0",
1216 "ready-resource": "^1.0.0",
1217 "safety-catch": "^1.0.2",
1218 "speedometer": "^1.1.0",
1219 "streamx": "^2.12.4",
1220 "sub-encoder": "^2.1.1",
1221 "unix-path-resolve": "^1.0.2"
1222 }
1223 },
1224 "node_modules/hyperswarm": {
1225 "version": "4.16.0",
1226 "resolved": "https://registry.npmjs.org/hyperswarm/-/hyperswarm-4.16.0.tgz",
1227 "integrity": "sha512-f86RsI1qP/nbBTv4wYxOhu1JnWvGZgiC1w69aRFn0xGSavLMWjvbbVspVApOnRjiPKpYVVGACXdrvEOrL3rHdw==",
1228 "license": "MIT",
1229 "dependencies": {
1230 "b4a": "^1.3.1",
1231 "bare-events": "^2.2.0",
1232 "hyperdht": "^6.21.0",
1233 "safety-catch": "^1.0.2",
1234 "shuffled-priority-queue": "^2.1.0",
1235 "streamx": "^2.22.1",
1236 "unslab": "^1.3.0"
1237 }
1238 },
1239 "node_modules/is-options": {
1240 "version": "1.0.2",
1241 "resolved": "https://registry.npmjs.org/is-options/-/is-options-1.0.2.tgz",
1242 "integrity": "sha512-u+Ai74c8Q74aS8BuHwPdI1jptGOT1FQXgCq8/zv0xRuE+wRgSMEJLj8lVO8Zp9BeGb29BXY6AsNPinfqjkr7Fg==",
1243 "license": "MIT",
1244 "dependencies": {
1245 "b4a": "^1.1.1"
1246 }
1247 },
1248 "node_modules/kademlia-routing-table": {
1249 "version": "1.0.6",
1250 "resolved": "https://registry.npmjs.org/kademlia-routing-table/-/kademlia-routing-table-1.0.6.tgz",
1251 "integrity": "sha512-Ve6jwIlUCYvUzBnXnzVRHDZCFgXURW9gmF3r7n05kZs/2rNbLHXwGdcq0qIaSwdmJCvtosgR4JensnVU65hzNQ==",
1252 "license": "MIT",
1253 "dependencies": {
1254 "bare-events": "^2.2.0"
1255 }
1256 },
1257 "node_modules/mirror-drive": {
1258 "version": "1.12.0",
1259 "resolved": "https://registry.npmjs.org/mirror-drive/-/mirror-drive-1.12.0.tgz",
1260 "integrity": "sha512-ASoYsRul+fzGT8rzgEEiinKCLZYd/03nclngLim6Gwm97n8jPMHbhIXgBlnkcqeASKsCnTIm3w9hxR6OBbRW4Q==",
1261 "license": "Apache-2.0",
1262 "dependencies": {
1263 "bare-events": "^2.8.2",
1264 "binary-stream-equals": "^1.0.0",
1265 "same-data": "^1.0.0",
1266 "speedometer": "^1.1.0",
1267 "streamx": "^2.22.1",
1268 "unix-path-resolve": "^1.0.2"
1269 }
1270 },
1271 "node_modules/mutexify": {
1272 "version": "1.4.0",
1273 "resolved": "https://registry.npmjs.org/mutexify/-/mutexify-1.4.0.tgz",
1274 "integrity": "sha512-pbYSsOrSB/AKN5h/WzzLRMFgZhClWccf2XIB4RSMC8JbquiB0e0/SH5AIfdQMdyHmYtv4seU7yV/TvAwPLJ1Yg==",
1275 "license": "MIT",
1276 "dependencies": {
1277 "queue-tick": "^1.0.0"
1278 }
1279 },
1280 "node_modules/nanoassert": {
1281 "version": "2.0.0",
1282 "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz",
1283 "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==",
1284 "license": "ISC"
1285 },
1286 "node_modules/nat-sampler": {
1287 "version": "1.0.1",
1288 "resolved": "https://registry.npmjs.org/nat-sampler/-/nat-sampler-1.0.1.tgz",
1289 "integrity": "sha512-yQvyNN7xbqR8crTKk3U8gRgpcV1Az+vfCEijiHu9oHHsnIl8n3x+yXNHl42M6L3czGynAVoOT9TqBfS87gDdcw==",
1290 "license": "MIT"
1291 },
1292 "node_modules/noise-curve-ed": {
1293 "version": "2.1.0",
1294 "resolved": "https://registry.npmjs.org/noise-curve-ed/-/noise-curve-ed-2.1.0.tgz",
1295 "integrity": "sha512-zAzJx+VwZM3w6EA1hTmDhJfvAnCeBQn/1FAeZ0LtGxCcCtlAK/uJXQVF/eDVUOaAZ286lHlx77WJ+qj9SmsRRg==",
1296 "license": "ISC",
1297 "dependencies": {
1298 "b4a": "^1.1.0",
1299 "nanoassert": "^2.0.0",
1300 "sodium-universal": "^5.0.0"
1301 }
1302 },
1303 "node_modules/noise-curve-ed/node_modules/sodium-native": {
1304 "version": "5.0.10",
1305 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
1306 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
1307 "license": "MIT",
1308 "dependencies": {
1309 "require-addon": "^1.1.0",
1310 "which-runtime": "^1.2.1"
1311 },
1312 "engines": {
1313 "bare": ">=1.16.0"
1314 }
1315 },
1316 "node_modules/noise-curve-ed/node_modules/sodium-universal": {
1317 "version": "5.0.1",
1318 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1319 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1320 "license": "MIT",
1321 "dependencies": {
1322 "sodium-native": "^5.0.1"
1323 },
1324 "peerDependencies": {
1325 "sodium-javascript": "~0.8.0"
1326 },
1327 "peerDependenciesMeta": {
1328 "sodium-javascript": {
1329 "optional": true
1330 }
1331 }
1332 },
1333 "node_modules/noise-handshake": {
1334 "version": "4.2.0",
1335 "resolved": "https://registry.npmjs.org/noise-handshake/-/noise-handshake-4.2.0.tgz",
1336 "integrity": "sha512-9O/VTNX/E2/AToyMTTDU0J/4WhaXMTdqc2DHs9vf+snoZ0cenSBq0dNYTVV1snYYEkmo6QeRrYMxtqtoYnY+LA==",
1337 "license": "Apache-2.0",
1338 "dependencies": {
1339 "b4a": "^1.1.0",
1340 "nanoassert": "^2.0.0",
1341 "sodium-universal": "^5.0.0"
1342 }
1343 },
1344 "node_modules/noise-handshake/node_modules/sodium-native": {
1345 "version": "5.0.10",
1346 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
1347 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
1348 "license": "MIT",
1349 "dependencies": {
1350 "require-addon": "^1.1.0",
1351 "which-runtime": "^1.2.1"
1352 },
1353 "engines": {
1354 "bare": ">=1.16.0"
1355 }
1356 },
1357 "node_modules/noise-handshake/node_modules/sodium-universal": {
1358 "version": "5.0.1",
1359 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1360 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1361 "license": "MIT",
1362 "dependencies": {
1363 "sodium-native": "^5.0.1"
1364 },
1365 "peerDependencies": {
1366 "sodium-javascript": "~0.8.0"
1367 },
1368 "peerDependenciesMeta": {
1369 "sodium-javascript": {
1370 "optional": true
1371 }
1372 }
1373 },
1374 "node_modules/paparam": {
1375 "version": "1.10.0",
1376 "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.0.tgz",
1377 "integrity": "sha512-p36QQrwU3X6fj5d2JN20B4lcZI/O3fxMVUHc1xD7MNe5bcOf3jIgE0CIK5Zv/s5YAUGlobTJ/kKXrh1FJVrs3w==",
1378 "dev": true,
1379 "license": "Apache-2.0"
1380 },
1381 "node_modules/picomatch": {
1382 "version": "4.0.3",
1383 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
1384 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
1385 "dev": true,
1386 "license": "MIT",
1387 "engines": {
1388 "node": ">=12"
1389 },
1390 "funding": {
1391 "url": "https://github.com/sponsors/jonschlinkert"
1392 }
1393 },
1394 "node_modules/protocol-buffers-encodings": {
1395 "version": "1.2.0",
1396 "resolved": "https://registry.npmjs.org/protocol-buffers-encodings/-/protocol-buffers-encodings-1.2.0.tgz",
1397 "integrity": "sha512-daeNPuKh1NlLD1uDfbLpD+xyUTc07nEtfHwmBZmt/vH0B7VOM+JOCOpDcx9ZRpqHjAiIkGqyTDi+wfGSl17R9w==",
1398 "license": "MIT",
1399 "dependencies": {
1400 "b4a": "^1.6.0",
1401 "signed-varint": "^2.0.1",
1402 "varint": "5.0.0"
1403 }
1404 },
1405 "node_modules/protomux": {
1406 "version": "3.10.1",
1407 "resolved": "https://registry.npmjs.org/protomux/-/protomux-3.10.1.tgz",
1408 "integrity": "sha512-jgBqx8ZyaBWea/DFG4eOu1scOaeBwcnagiRC1XFVrjeGt7oAb0Pk5udPpBUpJ4DJBRjra50jD6YcZiQQTRqaaA==",
1409 "license": "MIT",
1410 "dependencies": {
1411 "b4a": "^1.3.1",
1412 "compact-encoding": "^2.5.1",
1413 "queue-tick": "^1.0.0",
1414 "safety-catch": "^1.0.1",
1415 "unslab": "^1.3.0"
1416 }
1417 },
1418 "node_modules/queue-tick": {
1419 "version": "1.0.1",
1420 "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
1421 "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
1422 "license": "MIT"
1423 },
1424 "node_modules/quickbit-native": {
1425 "version": "2.4.8",
1426 "resolved": "https://registry.npmjs.org/quickbit-native/-/quickbit-native-2.4.8.tgz",
1427 "integrity": "sha512-FcCcqI+nIAWGknqhtrYT5TSD7t/N+Xd8ctM+2PrIIBuwOi5hx0SxAvuPtzLIEMfT/2h9+fhBakUe2uALOHX6yw==",
1428 "license": "Apache-2.0",
1429 "optional": true,
1430 "dependencies": {
1431 "require-addon": "^1.1.0"
1432 }
1433 },
1434 "node_modules/quickbit-universal": {
1435 "version": "2.2.0",
1436 "resolved": "https://registry.npmjs.org/quickbit-universal/-/quickbit-universal-2.2.0.tgz",
1437 "integrity": "sha512-w02i1R8n7+6pEKTud8DfF8zbFY9o7RtPlUc3jWbtCkDKvhbx/AvV7oNnz4/TcmsPGpSJS+fq5Ud6RH6+YPvSGg==",
1438 "license": "ISC",
1439 "dependencies": {
1440 "b4a": "^1.6.0",
1441 "simdle-universal": "^1.1.0"
1442 },
1443 "optionalDependencies": {
1444 "quickbit-native": "^2.2.0"
1445 }
1446 },
1447 "node_modules/rache": {
1448 "version": "1.0.0",
1449 "resolved": "https://registry.npmjs.org/rache/-/rache-1.0.0.tgz",
1450 "integrity": "sha512-e0k0g0w/8jOCB+7YqCIlOa+OJ38k0wrYS4x18pMSmqOvLKoyhmMhmQyCcvfY6VaP8D75cqkEnlakXs+RYYLqNg==",
1451 "license": "Apache-2.0"
1452 },
1453 "node_modules/random-access-file": {
1454 "version": "4.1.2",
1455 "resolved": "https://registry.npmjs.org/random-access-file/-/random-access-file-4.1.2.tgz",
1456 "integrity": "sha512-GQM6R78DceZDcQod8KxlDFwXIiUvlvuy1EOzxTDsjuDjW5NlnlZi0MOk6iI4itAj/2vcvdqcEExYbVpC/dJcEw==",
1457 "license": "MIT",
1458 "dependencies": {
1459 "bare-fs": "^4.0.1",
1460 "bare-path": "^3.0.0",
1461 "random-access-storage": "^3.0.0"
1462 },
1463 "optionalDependencies": {
1464 "fs-native-extensions": "^1.3.1"
1465 }
1466 },
1467 "node_modules/random-access-storage": {
1468 "version": "3.0.2",
1469 "resolved": "https://registry.npmjs.org/random-access-storage/-/random-access-storage-3.0.2.tgz",
1470 "integrity": "sha512-Es9maUyWdJXWKckKy9s1+vT+DEgAt+PBb9lxPaake/0EDUsHehloKGv9v1zimS2V3gpFAcQXubvc1Rgci2sDPQ==",
1471 "license": "MIT",
1472 "dependencies": {
1473 "bare-events": "^2.2.0",
1474 "queue-tick": "^1.0.0"
1475 }
1476 },
1477 "node_modules/random-array-iterator": {
1478 "version": "1.0.0",
1479 "resolved": "https://registry.npmjs.org/random-array-iterator/-/random-array-iterator-1.0.0.tgz",
1480 "integrity": "sha512-u7xCM93XqKEvPTP6xZp2ehttcAemKnh73oKNf1FvzuVCfpt6dILDt1Kxl1LeBjm2iNIeR49VGFhy4Iz3yOun+Q==",
1481 "license": "MIT"
1482 },
1483 "node_modules/read-write-mutexify": {
1484 "version": "2.1.0",
1485 "resolved": "https://registry.npmjs.org/read-write-mutexify/-/read-write-mutexify-2.1.0.tgz",
1486 "integrity": "sha512-fDw/p5/acI1ytVY1UbxEDma/ej1yJH/n9NcjS9YNzcE6sPBPWdlru3ydRa/UBowUg4zqOvNMD5SOGYJrlQ6MzQ==",
1487 "license": "MIT"
1488 },
1489 "node_modules/ready-resource": {
1490 "version": "1.2.0",
1491 "resolved": "https://registry.npmjs.org/ready-resource/-/ready-resource-1.2.0.tgz",
1492 "integrity": "sha512-nfcco/8iAFV0M+2PYnmIc+/xY0iRb35d42HFHQ7AfjulbGEAFa+XWpByfwSyeVeiBoMLLFVMv1HixxNCqzSQ1g==",
1493 "license": "MIT",
1494 "dependencies": {
1495 "bare-events": "^2.2.0"
1496 }
1497 },
1498 "node_modules/record-cache": {
1499 "version": "1.2.0",
1500 "resolved": "https://registry.npmjs.org/record-cache/-/record-cache-1.2.0.tgz",
1501 "integrity": "sha512-kyy3HWCez2WrotaL3O4fTn0rsIdfRKOdQQcEJ9KpvmKmbffKVvwsloX063EgRUlpJIXHiDQFhJcTbZequ2uTZw==",
1502 "license": "MIT",
1503 "dependencies": {
1504 "b4a": "^1.3.1"
1505 }
1506 },
1507 "node_modules/require-addon": {
1508 "version": "1.2.0",
1509 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
1510 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
1511 "license": "Apache-2.0",
1512 "dependencies": {
1513 "bare-addon-resolve": "^1.3.0"
1514 },
1515 "engines": {
1516 "bare": ">=1.10.0"
1517 }
1518 },
1519 "node_modules/resolve-reject-promise": {
1520 "version": "1.1.0",
1521 "resolved": "https://registry.npmjs.org/resolve-reject-promise/-/resolve-reject-promise-1.1.0.tgz",
1522 "integrity": "sha512-LWsTOA91AqzBTjSGgX79Tc130pwcBK6xjpJEO+qRT5IKZ6bGnHKcc8QL3upUBcWuU8OTIDzKK2VNSwmmlqvAVg==",
1523 "license": "MIT"
1524 },
1525 "node_modules/safety-catch": {
1526 "version": "1.0.2",
1527 "resolved": "https://registry.npmjs.org/safety-catch/-/safety-catch-1.0.2.tgz",
1528 "integrity": "sha512-C1UYVZ4dtbBxEtvOcpjBaaD27nP8MlvyAQEp2fOTOEe6pfUpk1cDUxij6BR1jZup6rSyUTaBBplK7LanskrULA==",
1529 "license": "MIT"
1530 },
1531 "node_modules/same-data": {
1532 "version": "1.0.0",
1533 "resolved": "https://registry.npmjs.org/same-data/-/same-data-1.0.0.tgz",
1534 "integrity": "sha512-Eqn7N2yV+aKMlUHTRqUwYG1Iv0cJqjlvLKj/GoP5PozJn361QaOYX14+v87r7NqQUZC22noP/LfLrSQiPwAygw==",
1535 "license": "MIT"
1536 },
1537 "node_modules/same-object": {
1538 "version": "1.0.2",
1539 "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz",
1540 "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==",
1541 "dev": true,
1542 "license": "MIT"
1543 },
1544 "node_modules/shuffled-priority-queue": {
1545 "version": "2.1.0",
1546 "resolved": "https://registry.npmjs.org/shuffled-priority-queue/-/shuffled-priority-queue-2.1.0.tgz",
1547 "integrity": "sha512-xhdh7fHyMsr0m/w2kDfRJuBFRS96b9l8ZPNWGaQ+PMvnUnZ/Eh+gJJ9NsHBd7P9k0399WYlCLzsy18EaMfyadA==",
1548 "license": "MIT",
1549 "dependencies": {
1550 "unordered-set": "^2.0.1"
1551 }
1552 },
1553 "node_modules/signal-promise": {
1554 "version": "1.0.3",
1555 "resolved": "https://registry.npmjs.org/signal-promise/-/signal-promise-1.0.3.tgz",
1556 "integrity": "sha512-WBgv0UnIq2C+Aeh0/n+IRpP6967eIx9WpynTUoiW3isPpfe1zu2LJzyfXdo9Tgef8yR/sGjcMvoUXD7EYdiz+g==",
1557 "license": "MIT"
1558 },
1559 "node_modules/signed-varint": {
1560 "version": "2.0.1",
1561 "resolved": "https://registry.npmjs.org/signed-varint/-/signed-varint-2.0.1.tgz",
1562 "integrity": "sha512-abgDPg1106vuZZOvw7cFwdCABddfJRz5akcCcchzTbhyhYnsG31y4AlZEgp315T7W3nQq5P4xeOm186ZiPVFzw==",
1563 "license": "MIT",
1564 "dependencies": {
1565 "varint": "~5.0.0"
1566 }
1567 },
1568 "node_modules/simdle-native": {
1569 "version": "1.3.9",
1570 "resolved": "https://registry.npmjs.org/simdle-native/-/simdle-native-1.3.9.tgz",
1571 "integrity": "sha512-Isc8sP4OiiIU0mpslD4GHEnR0VQWvR/54WN7YtwEDkNdTJVWtpmvsSvsgRlw5BNGxdYXlVRegdnrSu10H/PhvA==",
1572 "license": "Apache-2.0",
1573 "optional": true,
1574 "dependencies": {
1575 "b4a": "^1.6.0",
1576 "require-addon": "^1.1.0"
1577 }
1578 },
1579 "node_modules/simdle-universal": {
1580 "version": "1.1.2",
1581 "resolved": "https://registry.npmjs.org/simdle-universal/-/simdle-universal-1.1.2.tgz",
1582 "integrity": "sha512-3n3w1bs+uwgHKQjt6arez83EywNlhZzYvNOhvAASTl/8KqNIcqr6aHyGt3JRlfuUC7iB0tomJRPlJ2cRGIpBzA==",
1583 "license": "ISC",
1584 "dependencies": {
1585 "b4a": "^1.6.0"
1586 },
1587 "optionalDependencies": {
1588 "simdle-native": "^1.1.1"
1589 }
1590 },
1591 "node_modules/sodium-native": {
1592 "version": "4.3.3",
1593 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz",
1594 "integrity": "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw==",
1595 "license": "MIT",
1596 "dependencies": {
1597 "require-addon": "^1.1.0"
1598 }
1599 },
1600 "node_modules/sodium-secretstream": {
1601 "version": "1.2.0",
1602 "resolved": "https://registry.npmjs.org/sodium-secretstream/-/sodium-secretstream-1.2.0.tgz",
1603 "integrity": "sha512-q/DbraNFXm1KfCiiZvapmz5UC3OlpirYFIvBK2MhGaOFSb3gRyk8OXTi17UI9SGfshQNCpsVvlopogbzZNyW6Q==",
1604 "license": "MIT",
1605 "dependencies": {
1606 "b4a": "^1.1.1",
1607 "sodium-universal": "^5.0.0"
1608 }
1609 },
1610 "node_modules/sodium-secretstream/node_modules/sodium-native": {
1611 "version": "5.0.10",
1612 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz",
1613 "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==",
1614 "license": "MIT",
1615 "dependencies": {
1616 "require-addon": "^1.1.0",
1617 "which-runtime": "^1.2.1"
1618 },
1619 "engines": {
1620 "bare": ">=1.16.0"
1621 }
1622 },
1623 "node_modules/sodium-secretstream/node_modules/sodium-universal": {
1624 "version": "5.0.1",
1625 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1626 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1627 "license": "MIT",
1628 "dependencies": {
1629 "sodium-native": "^5.0.1"
1630 },
1631 "peerDependencies": {
1632 "sodium-javascript": "~0.8.0"
1633 },
1634 "peerDependenciesMeta": {
1635 "sodium-javascript": {
1636 "optional": true
1637 }
1638 }
1639 },
1640 "node_modules/sodium-universal": {
1641 "version": "4.0.1",
1642 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-4.0.1.tgz",
1643 "integrity": "sha512-sNp13PrxYLaUFHTGoDKkSDFvoEu51bfzE12RwGlqU1fcrkpAOK0NvizaJzOWV0Omtk9me2+Pnbjcf/l0efxuGQ==",
1644 "license": "MIT",
1645 "dependencies": {
1646 "sodium-native": "^4.0.0"
1647 },
1648 "peerDependencies": {
1649 "sodium-javascript": "~0.8.0"
1650 },
1651 "peerDependenciesMeta": {
1652 "sodium-javascript": {
1653 "optional": true
1654 }
1655 }
1656 },
1657 "node_modules/speedometer": {
1658 "version": "1.1.0",
1659 "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-1.1.0.tgz",
1660 "integrity": "sha512-z/wAiTESw2XVPssY2XRcme4niTc4S5FkkJ4gknudtVoc33Zil8TdTxHy5torRcgqMqksJV2Yz8HQcvtbsnw0mQ==",
1661 "license": "MIT"
1662 },
1663 "node_modules/stackframe": {
1664 "version": "1.3.4",
1665 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
1666 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
1667 "dev": true,
1668 "license": "MIT"
1669 },
1670 "node_modules/streamx": {
1671 "version": "2.23.0",
1672 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
1673 "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
1674 "license": "MIT",
1675 "dependencies": {
1676 "events-universal": "^1.0.0",
1677 "fast-fifo": "^1.3.2",
1678 "text-decoder": "^1.1.0"
1679 }
1680 },
1681 "node_modules/sub-encoder": {
1682 "version": "2.1.3",
1683 "resolved": "https://registry.npmjs.org/sub-encoder/-/sub-encoder-2.1.3.tgz",
1684 "integrity": "sha512-Xxx04ygZo/1J3yHvaSA6VhDmiSaBQkw/PmO3YnnYFXle+tfOGToC6FcDpIfMztWZXJzuKG14b/57HMkiL58C6A==",
1685 "license": "Apache-2.0",
1686 "dependencies": {
1687 "b4a": "^1.6.0",
1688 "codecs": "^3.1.0"
1689 }
1690 },
1691 "node_modules/test-tmp": {
1692 "version": "1.4.0",
1693 "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz",
1694 "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==",
1695 "dev": true,
1696 "license": "MIT",
1697 "dependencies": {
1698 "bare-fs": "^4.0.1",
1699 "bare-os": "^3.3.0",
1700 "bare-path": "^3.0.0"
1701 }
1702 },
1703 "node_modules/text-decoder": {
1704 "version": "1.2.6",
1705 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.6.tgz",
1706 "integrity": "sha512-27FeW5GQFDfw0FpwMQhMagB7BztOOlmjcSRi97t2oplhKVTZtp0DZbSegSaXS5IIC6mxMvBG4AR1Sgc6BX3CQg==",
1707 "license": "Apache-2.0",
1708 "dependencies": {
1709 "b4a": "^1.6.4"
1710 }
1711 },
1712 "node_modules/time-ordered-set": {
1713 "version": "2.0.1",
1714 "resolved": "https://registry.npmjs.org/time-ordered-set/-/time-ordered-set-2.0.1.tgz",
1715 "integrity": "sha512-VJEKmgSN2UiOLB8BpN8Sh2b9LGMHTP5OPrQRpnKjvOheOyzk0mufbjzjKTIG2gO4A+Y+vDJ+0TcLbpUmMLsg8A==",
1716 "license": "MIT"
1717 },
1718 "node_modules/timeout-refresh": {
1719 "version": "2.0.1",
1720 "resolved": "https://registry.npmjs.org/timeout-refresh/-/timeout-refresh-2.0.1.tgz",
1721 "integrity": "sha512-SVqEcMZBsZF9mA78rjzCrYrUs37LMJk3ShZ851ygZYW1cMeIjs9mL57KO6Iv5mmjSQnOe/29/VAfGXo+oRCiVw==",
1722 "license": "MIT"
1723 },
1724 "node_modules/tmatch": {
1725 "version": "5.0.0",
1726 "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz",
1727 "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==",
1728 "dev": true,
1729 "license": "ISC",
1730 "engines": {
1731 "node": ">=8"
1732 }
1733 },
1734 "node_modules/udx-native": {
1735 "version": "1.19.2",
1736 "resolved": "https://registry.npmjs.org/udx-native/-/udx-native-1.19.2.tgz",
1737 "integrity": "sha512-RNYh+UhfryCsF5hE2ZOuIqcZ+qdipXK3UsarwxWJwsUQZFE3ybwz0mPjwb5ev1PMBcjFahWiepS/q0wwL51c2g==",
1738 "license": "Apache-2.0",
1739 "dependencies": {
1740 "b4a": "^1.5.0",
1741 "bare-events": "^2.2.0",
1742 "require-addon": "^1.1.0",
1743 "streamx": "^2.22.0"
1744 },
1745 "engines": {
1746 "bare": ">=1.17.4"
1747 }
1748 },
1749 "node_modules/unix-path-resolve": {
1750 "version": "1.0.2",
1751 "resolved": "https://registry.npmjs.org/unix-path-resolve/-/unix-path-resolve-1.0.2.tgz",
1752 "integrity": "sha512-kG4g5nobBBaMnH2XbrS4sLUXEpx4nY2J3C6KAlAUcnahG2HChxSPVKWYrqEq76iTo+cyMkLUjqxGaQR2tz097Q==",
1753 "license": "MIT"
1754 },
1755 "node_modules/unordered-set": {
1756 "version": "2.0.1",
1757 "resolved": "https://registry.npmjs.org/unordered-set/-/unordered-set-2.0.1.tgz",
1758 "integrity": "sha512-eUmNTPzdx+q/WvOHW0bgGYLWvWHNT3PTKEQLg0MAQhc0AHASHVHoP/9YytYd4RBVariqno/mEUhVZN98CmD7bg==",
1759 "license": "MIT"
1760 },
1761 "node_modules/unslab": {
1762 "version": "1.3.0",
1763 "resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz",
1764 "integrity": "sha512-YATkfKAFj47kTzmiQrWXMyRvaVrHsW6MEALa4bm+FhiA2YG4oira+Z3DXN6LrYOYn2Y8eO94Lwl9DOHjs1FpoQ==",
1765 "license": "Apache-2.0",
1766 "dependencies": {
1767 "b4a": "^1.6.6"
1768 }
1769 },
1770 "node_modules/v8-to-istanbul": {
1771 "version": "9.3.0",
1772 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
1773 "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
1774 "dev": true,
1775 "license": "ISC",
1776 "dependencies": {
1777 "@jridgewell/trace-mapping": "^0.3.12",
1778 "@types/istanbul-lib-coverage": "^2.0.1",
1779 "convert-source-map": "^2.0.0"
1780 },
1781 "engines": {
1782 "node": ">=10.12.0"
1783 }
1784 },
1785 "node_modules/varint": {
1786 "version": "5.0.0",
1787 "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz",
1788 "integrity": "sha512-gC13b/bWrqQoKY2EmROCZ+AR0jitc6DnDGaQ6Ls9QpKmuSgJB1eQ7H3KETtQm7qSdMWMKCmsshyCmUwMLh3OAA==",
1789 "license": "MIT"
1790 },
1791 "node_modules/which-runtime": {
1792 "version": "1.3.2",
1793 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
1794 "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
1795 "license": "Apache-2.0"
1796 },
1797 "node_modules/xache": {
1798 "version": "1.2.1",
1799 "resolved": "https://registry.npmjs.org/xache/-/xache-1.2.1.tgz",
1800 "integrity": "sha512-igRS6jPreJ54ABdzhh4mCDXcz+XMaWO2q1ABRV2yWYuk29jlp8VT7UBdCqNkX7rpYBbXsebVVKkwIuYZjyZNqA==",
1801 "license": "MIT"
1802 },
1803 "node_modules/z32": {
1804 "version": "1.1.0",
1805 "resolved": "https://registry.npmjs.org/z32/-/z32-1.1.0.tgz",
1806 "integrity": "sha512-1WUHy+VS6d0HPNspDxvLssBbeQjXMjSnpv0vH82vRAUfg847NmX3OXozp/hRP5jPhxBbrVzrgvAt+UsGNzRFQQ==",
1807 "license": "MIT",
1808 "dependencies": {
1809 "b4a": "^1.5.3"
1810 }
1811 }
1812 }
1813}
hyper-wiki/package.json +26 -0
……@@ -0,0 +1,26 @@
1{
2 "name": "hyper-wiki",
3 "version": "1.0.0",
4 "description": "P2P wiki — Hyperdrive-backed, versioned, collaborative",
5 "type": "module",
6 "main": "index.js",
7 "pear": {
8 "name": "hyper-wiki",
9 "type": "terminal"
10 },
11 "scripts": {
12 "test": "brittle test/*.test.js"
13 },
14 "dependencies": {
15 "hyperdrive": "^11.0.0",
16 "hyperbee": "^2.0.0",
17 "hypercore": "^10.0.0",
18 "corestore": "^6.0.0",
19 "hyperswarm": "^4.0.0",
20 "b4a": "^1.0.0"
21 },
22 "devDependencies": {
23 "brittle": "^3.0.0"
24 },
25 "license": "MIT"
26}
hyper-wiki/test/wiki.test.js +226 -0
……@@ -0,0 +1,226 @@
1import test from 'brittle'
2import { Wiki } from '../lib/wiki.js'
3import { tmpdir } from 'os'
4import { join } from 'path'
5import { randomBytes } from 'crypto'
6
7function tmpStorage () {
8 return join(tmpdir(), 'hyper-wiki-test-' + randomBytes(8).toString('hex'))
9}
10
11test('Wiki - create and read page', async (t) => {
12 const wiki = new Wiki(tmpStorage())
13 await wiki.ready()
14
15 const meta = await wiki.create('hello-world', '# Hello World\n\nThis is a test page.', {
16 title: 'Hello World',
17 author: 'alice'
18 })
19
20 t.is(meta.title, 'Hello World')
21 t.is(meta.author, 'alice')
22 t.is(meta.version, 1)
23 t.ok(meta.created > 0)
24
25 const page = await wiki.read('hello-world')
26 t.ok(page)
27 t.is(page.slug, 'hello-world')
28 t.is(page.content, '# Hello World\n\nThis is a test page.')
29 t.is(page.title, 'Hello World')
30 t.is(page.version, 1)
31
32 await wiki.close()
33})
34
35test('Wiki - read nonexistent returns null', async (t) => {
36 const wiki = new Wiki(tmpStorage())
37 await wiki.ready()
38
39 const page = await wiki.read('nonexistent')
40 t.is(page, null)
41
42 await wiki.close()
43})
44
45test('Wiki - update page increments version', async (t) => {
46 const wiki = new Wiki(tmpStorage())
47 await wiki.ready()
48
49 await wiki.create('test', 'Version 1', { title: 'Test Page' })
50 const meta = await wiki.update('test', 'Version 2')
51
52 t.is(meta.version, 2)
53 t.is(meta.title, 'Test Page') // title preserved
54
55 const page = await wiki.read('test')
56 t.is(page.content, 'Version 2')
57 t.is(page.version, 2)
58
59 await wiki.close()
60})
61
62test('Wiki - delete page', async (t) => {
63 const wiki = new Wiki(tmpStorage())
64 await wiki.ready()
65
66 await wiki.create('to-delete', 'content')
67 t.ok(await wiki.exists('to-delete'))
68
69 await wiki.remove('to-delete')
70 t.absent(await wiki.exists('to-delete'))
71 t.is(await wiki.read('to-delete'), null)
72
73 await wiki.close()
74})
75
76test('Wiki - list pages', async (t) => {
77 const wiki = new Wiki(tmpStorage())
78 await wiki.ready()
79
80 await wiki.create('page-a', 'Content A', { title: 'Page A' })
81 await wiki.create('page-b', 'Content B', { title: 'Page B' })
82 await wiki.create('page-c', 'Content C', { title: 'Page C' })
83
84 const pages = await wiki.list()
85 t.is(pages.length, 3)
86
87 // Check sorted by updated desc (page-c is most recent)
88 t.is(pages[0].slug, 'page-c')
89 t.is(pages[0].title, 'Page C')
90
91 await wiki.close()
92})
93
94test('Wiki - list empty returns empty array', async (t) => {
95 const wiki = new Wiki(tmpStorage())
96 await wiki.ready()
97
98 const pages = await wiki.list()
99 t.alike(pages, [])
100
101 await wiki.close()
102})
103
104test('Wiki - exists', async (t) => {
105 const wiki = new Wiki(tmpStorage())
106 await wiki.ready()
107
108 t.absent(await wiki.exists('nope'))
109 await wiki.create('yep', 'exists')
110 t.ok(await wiki.exists('yep'))
111
112 await wiki.close()
113})
114
115test('Wiki - search by content', async (t) => {
116 const wiki = new Wiki(tmpStorage())
117 await wiki.ready()
118
119 await wiki.create('hypercore-guide', '# Hypercore\n\nAppend-only log for P2P', { title: 'Hypercore Guide' })
120 await wiki.create('hyperbee-guide', '# Hyperbee\n\nDatabase on top of Hypercore', { title: 'Hyperbee Guide' })
121 await wiki.create('unrelated', '# Cooking\n\nHow to make pasta', { title: 'Cooking' })
122
123 const results = await wiki.search('hypercore')
124 t.ok(results.length >= 2, 'finds pages mentioning hypercore')
125 t.ok(results.some(r => r.slug === 'hypercore-guide'))
126 t.ok(results.some(r => r.slug === 'hyperbee-guide'))
127
128 await wiki.close()
129})
130
131test('Wiki - search by title', async (t) => {
132 const wiki = new Wiki(tmpStorage())
133 await wiki.ready()
134
135 await wiki.create('setup', 'Install instructions', { title: 'Setup Guide' })
136 await wiki.create('faq', 'Common questions', { title: 'FAQ' })
137
138 const results = await wiki.search('setup')
139 t.ok(results.length >= 1)
140 t.ok(results.some(r => r.slug === 'setup'))
141
142 await wiki.close()
143})
144
145test('Wiki - search no results', async (t) => {
146 const wiki = new Wiki(tmpStorage())
147 await wiki.ready()
148
149 await wiki.create('page1', 'some content')
150 const results = await wiki.search('nonexistent-term-xyz')
151 t.is(results.length, 0)
152
153 await wiki.close()
154})
155
156test('Wiki - multiple updates preserve created time', async (t) => {
157 const wiki = new Wiki(tmpStorage())
158 await wiki.ready()
159
160 const v1 = await wiki.create('evolving', 'v1')
161 const created = v1.created
162
163 await new Promise(r => setTimeout(r, 10))
164 const v2 = await wiki.update('evolving', 'v2')
165
166 t.is(v2.created, created, 'created time unchanged')
167 t.ok(v2.updated >= created, 'updated time is newer')
168 t.is(v2.version, 2)
169
170 await wiki.close()
171})
172
173test('Wiki - unicode content', async (t) => {
174 const wiki = new Wiki(tmpStorage())
175 await wiki.ready()
176
177 await wiki.create('unicode', '# 日本語ページ\n\n🎉 こんにちは世界', {
178 title: '日本語テスト',
179 author: 'ユーザー'
180 })
181
182 const page = await wiki.read('unicode')
183 t.is(page.title, '日本語テスト')
184 t.ok(page.content.includes('🎉'))
185 t.ok(page.content.includes('こんにちは'))
186
187 await wiki.close()
188})
189
190test('Wiki - large content', async (t) => {
191 const wiki = new Wiki(tmpStorage())
192 await wiki.ready()
193
194 const content = '# Big Page\n\n' + 'Lorem ipsum dolor sit amet. '.repeat(1000)
195 await wiki.create('big', content, { title: 'Big Page' })
196
197 const page = await wiki.read('big')
198 t.is(page.content, content)
199
200 await wiki.close()
201})
202
203test('Wiki - tags preserved across updates', async (t) => {
204 const wiki = new Wiki(tmpStorage())
205 await wiki.ready()
206
207 await wiki.create('tagged', 'content', {
208 title: 'Tagged Page',
209 tags: ['p2p', 'hypercore']
210 })
211
212 await wiki.update('tagged', 'updated content')
213 const page = await wiki.read('tagged')
214 t.alike(page.tags, ['p2p', 'hypercore'], 'tags preserved')
215
216 await wiki.close()
217})
218
219test('Wiki - has discoveryKey', async (t) => {
220 const wiki = new Wiki(tmpStorage())
221 await wiki.ready()
222
223 t.ok(wiki.key, 'has key')
224
225 await wiki.close()
226})
keet-social/README.md +124 -0
……@@ -0,0 +1,124 @@
1# keet-social — Godot 4 Plugin for Keet P2P Chat
2
3A pure GDScript addon that connects Godot 4 to [keet-daemon](https://github.com/user/keet-cli) via its HTTP JSON-RPC API. Chat, manage rooms, track identities, and aggregate feeds — all P2P, no servers.
4
5## Requirements
6
7- Godot 4.3+
8- keet-daemon running on `localhost:3117`
9
10## Installation
11
12Copy `addons/keet_social/` into your Godot project's `addons/` folder. Enable the plugin in Project > Project Settings > Plugins.
13
14## Components
15
16| Node | Purpose |
17|------|---------|
18| **KeetClient** | Core HTTP client. All other nodes reference this. |
19| **KeetRoom** | List, join, leave, create rooms. |
20| **KeetChat** | Send/receive messages with auto-polling. |
21| **KeetIdentity** | Get/set identity, lookup users. |
22| **KeetFeed** | Multi-room message timeline aggregator. |
23
24## Quick Start
25
261. Add a `KeetClient` node to your scene
272. Add a `KeetChat` node, set its `client` property to the KeetClient
283. Set `room_id` on the KeetChat node
294. Connect signals:
30
31```gdscript
32func _ready() -> void:
33 $KeetChat.message_received.connect(_on_message)
34 $KeetChat.send("Hello from Godot!")
35
36func _on_message(msg: Dictionary) -> void:
37 print("%s: %s" % [msg.get("name", "?"), msg.get("text", "")])
38```
39
40## API
41
42### KeetClient
43
44```gdscript
45# JSON-RPC call with callback
46client.rpc("rooms", {}, func(result, error): ...)
47
48# Async version (use with await)
49var response = await client.rpc_async("rooms")
50
51# Signals
52signal connected
53signal disconnected
54signal error_occurred(message: String)
55```
56
57### KeetChat
58
59```gdscript
60chat.watch("room_id_here") # Start watching a room
61chat.send("Hello!") # Send a message
62chat.load_history(50) # Load last 50 messages
63chat.search("keyword") # Search messages
64chat.react(message_id, "👍") # React to a message
65
66signal message_received(message: Dictionary)
67signal messages_loaded(messages: Array)
68signal message_sent
69```
70
71### KeetRoom
72
73```gdscript
74room.list_rooms() # List all rooms
75room.create_room("My Room") # Create a room
76room.join_room("invite_token") # Join by invite
77room.leave_room("room_id") # Leave a room
78room.create_invite("room_id") # Get invite token
79room.list_members("room_id") # List members
80
81signal rooms_loaded(rooms: Array)
82signal room_created(room_id: String)
83signal members_loaded(room_id: String, members: Array)
84```
85
86### KeetIdentity
87
88```gdscript
89identity.get_identity() # Get current identity
90identity.set_name("My Name") # Set display name
91identity.lookup_user("alice0") # Find user by username
92identity.check_username("bob0") # Check availability
93
94signal identity_loaded(identity: Dictionary)
95signal name_changed(name: String)
96```
97
98### KeetFeed
99
100```gdscript
101feed.add_room("room_id_1") # Watch a room
102feed.add_room("room_id_2") # Watch another
103feed.start() # Start polling all rooms
104
105signal new_entry(room_id: String, message: Dictionary)
106signal feed_updated(messages: Array) # Full timeline
107```
108
109## Demo
110
111Run `demo/chat_demo.tscn` for a working 2D chat client. Requires keet-daemon on port 3117.
112
113## Architecture
114
115```
116Godot App
117 └── KeetClient (HTTP JSON-RPC to localhost:3117)
118 ├── KeetRoom (room management)
119 ├── KeetChat (messaging + polling)
120 ├── KeetIdentity (profiles)
121 └── KeetFeed (multi-room timeline)
122```
123
124All communication goes through keet-daemon's HTTP API. No WebSocket, no native dependencies, no compilation. Pure GDScript.
keet-social/addons/keet_social/keet_chat.gd +182 -0
……@@ -0,0 +1,182 @@
1class_name KeetChat
2extends Node
3## Chat messaging for a single Keet room.
4## Send/receive messages with automatic polling for new messages.
5
6signal message_received(message: Dictionary)
7signal messages_loaded(messages: Array)
8signal message_sent
9signal error_occurred(message: String)
10
11@export var client: KeetClient
12@export var room_id: String
13@export var auto_poll: bool = true
14@export_range(0.5, 10.0) var poll_interval: float = 2.0
15@export_range(1, 100) var message_limit: int = 50
16
17var _poll_timer: Timer
18var _high_seq: int = -1
19var _known_ids: Dictionary = {}
20
21
22func _ready() -> void:
23 _poll_timer = Timer.new()
24 _poll_timer.wait_time = poll_interval
25 _poll_timer.timeout.connect(_poll_messages)
26 add_child(_poll_timer)
27 if auto_poll and room_id != "":
28 start_polling()
29
30
31## Start watching a room for new messages.
32func watch(rid: String) -> void:
33 room_id = rid
34 _high_seq = -1
35 _known_ids.clear()
36 start_polling()
37
38
39## Stop watching for messages.
40func unwatch() -> void:
41 stop_polling()
42 room_id = ""
43 _high_seq = -1
44 _known_ids.clear()
45
46
47## Send a text message to the current room.
48func send(text: String, callback: Callable = Callable()) -> void:
49 if room_id == "":
50 error_occurred.emit("No room_id set")
51 return
52 _require_client()
53 client.rpc("send", {"roomId": room_id, "text": text}, func(result, err):
54 if err != null:
55 error_occurred.emit("send: %s" % str(err))
56 return
57 message_sent.emit()
58 if callback.is_valid():
59 callback.call(result)
60 )
61
62
63## Load message history for the current room.
64func load_history(limit: int = 0, callback: Callable = Callable()) -> void:
65 if room_id == "":
66 error_occurred.emit("No room_id set")
67 return
68 _require_client()
69 var lim := limit if limit > 0 else message_limit
70 client.rpc("read", {"roomId": room_id, "limit": lim}, func(result, err):
71 if err != null:
72 error_occurred.emit("load_history: %s" % str(err))
73 return
74 var msgs: Array = result if result is Array else []
75 # Track seen messages
76 for msg: Variant in msgs:
77 if msg is Dictionary:
78 var mid: String = _msg_id(msg)
79 _known_ids[mid] = true
80 var seq: int = msg.get("seq", -1) as int
81 if seq > _high_seq:
82 _high_seq = seq
83 messages_loaded.emit(msgs)
84 if callback.is_valid():
85 callback.call(msgs)
86 )
87
88
89## Edit a message.
90func edit(message_id: Variant, text: String, callback: Callable = Callable()) -> void:
91 _require_client()
92 client.rpc("msg.edit", {"roomId": room_id, "messageId": message_id, "text": text}, func(result, err):
93 if err != null:
94 error_occurred.emit("edit: %s" % str(err))
95 return
96 if callback.is_valid():
97 callback.call(result)
98 )
99
100
101## Delete a message.
102func delete(message_id: Variant, callback: Callable = Callable()) -> void:
103 _require_client()
104 client.rpc("msg.delete", {"roomId": room_id, "messageId": message_id}, func(result, err):
105 if err != null:
106 error_occurred.emit("delete: %s" % str(err))
107 return
108 if callback.is_valid():
109 callback.call(result)
110 )
111
112
113## React to a message.
114func react(message_id: Variant, emoji: String, callback: Callable = Callable()) -> void:
115 _require_client()
116 client.rpc("react", {"roomId": room_id, "messageId": message_id, "emoji": emoji}, func(result, err):
117 if err != null:
118 error_occurred.emit("react: %s" % str(err))
119 return
120 if callback.is_valid():
121 callback.call(result)
122 )
123
124
125## Search messages in the current room.
126func search(query: String, callback: Callable = Callable()) -> void:
127 _require_client()
128 client.rpc("msg.search", {"roomId": room_id, "query": query}, func(result, err):
129 if err != null:
130 error_occurred.emit("search: %s" % str(err))
131 return
132 if callback.is_valid():
133 callback.call(result)
134 )
135
136
137func start_polling() -> void:
138 if not _poll_timer.is_stopped():
139 return
140 _poll_timer.start()
141 # Immediate first poll
142 _poll_messages()
143
144
145func stop_polling() -> void:
146 _poll_timer.stop()
147
148
149func _poll_messages() -> void:
150 if room_id == "" or client == null or not client.is_connected:
151 return
152 client.rpc("read", {"roomId": room_id, "limit": message_limit}, func(result, err):
153 if err != null:
154 return
155 if not result is Array:
156 return
157 for msg: Variant in result:
158 if not msg is Dictionary:
159 continue
160 var mid: String = _msg_id(msg)
161 if _known_ids.has(mid):
162 continue
163 _known_ids[mid] = true
164 var seq: int = msg.get("seq", -1) as int
165 if seq > _high_seq:
166 _high_seq = seq
167 message_received.emit(msg)
168 )
169
170
171func _msg_id(msg: Dictionary) -> String:
172 var id: Variant = msg.get("id", null)
173 if id is Dictionary:
174 return "%s:%d" % [id.get("deviceId", ""), id.get("seq", 0)]
175 if id is String:
176 return id
177 return "%s:%d" % [msg.get("memberId", ""), msg.get("seq", 0)]
178
179
180func _require_client() -> void:
181 if client == null:
182 push_error("KeetChat: client not set. Assign a KeetClient node.")
keet-social/addons/keet_social/keet_client.gd +150 -0
……@@ -0,0 +1,150 @@
1class_name KeetClient
2extends Node
3## Core HTTP client for keet-daemon (localhost:3117).
4## Provides JSON-RPC 2.0 calls and connection management.
5
6signal connected
7signal disconnected
8signal error_occurred(message: String)
9signal status_updated(status: Dictionary)
10
11@export var host: String = "127.0.0.1"
12@export var port: int = 3117
13@export var auto_connect: bool = true
14@export_range(1.0, 30.0) var health_interval: float = 10.0
15
16var is_connected: bool = false
17
18var _base_url: String
19var _health_timer: Timer
20var _pending_requests: int = 0
21
22
23func _ready() -> void:
24 _base_url = "http://%s:%d" % [host, port]
25 _health_timer = Timer.new()
26 _health_timer.wait_time = health_interval
27 _health_timer.timeout.connect(_check_health)
28 add_child(_health_timer)
29 if auto_connect:
30 check_connection()
31 _health_timer.start()
32
33
34## Make a JSON-RPC call to keet-daemon.
35## Returns result via callback(result, error).
36func rpc(method: String, params: Dictionary = {}, callback: Callable = Callable()) -> void:
37 var req := HTTPRequest.new()
38 req.timeout = 10.0
39 add_child(req)
40 var body := JSON.stringify({"method": method, "params": params})
41 req.request_completed.connect(_on_rpc_response.bind(callback, req))
42 _pending_requests += 1
43 var err := req.request(
44 _base_url + "/rpc",
45 ["Content-Type: application/json"],
46 HTTPClient.METHOD_POST,
47 body
48 )
49 if err != OK:
50 _pending_requests -= 1
51 req.queue_free()
52 if callback.is_valid():
53 callback.call(null, "Request failed: %d" % err)
54
55
56## Convenience: async RPC via await. Returns {"result": ..., "error": ...}.
57func rpc_async(method: String, params: Dictionary = {}) -> Dictionary:
58 var response := {"result": null, "error": null}
59 var done := false
60 rpc(method, params, func(result, err):
61 response.result = result
62 response.error = err
63 done = true
64 )
65 while not done:
66 await get_tree().process_frame
67 return response
68
69
70## GET a convenience endpoint (e.g. "/rpc/rooms", "/rpc/identity").
71func get_endpoint(path: String, callback: Callable = Callable()) -> void:
72 var req := HTTPRequest.new()
73 req.timeout = 10.0
74 add_child(req)
75 req.request_completed.connect(_on_get_response.bind(callback, req))
76 var err := req.request(_base_url + path)
77 if err != OK:
78 req.queue_free()
79 if callback.is_valid():
80 callback.call(null, "GET failed: %d" % err)
81
82
83## Check if keet-daemon is reachable.
84func check_connection() -> void:
85 rpc("status", {}, func(result, err):
86 var was_connected := is_connected
87 if err != null:
88 is_connected = false
89 if was_connected:
90 disconnected.emit()
91 else:
92 is_connected = true
93 if not was_connected:
94 connected.emit()
95 status_updated.emit(result if result is Dictionary else {})
96 )
97
98
99func _on_rpc_response(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray, callback: Callable, req: HTTPRequest) -> void:
100 _pending_requests -= 1
101 req.queue_free()
102
103 if result != HTTPRequest.RESULT_SUCCESS:
104 _handle_disconnect()
105 if callback.is_valid():
106 callback.call(null, "HTTP error: %d" % result)
107 return
108
109 if response_code < 200 or response_code >= 300:
110 if callback.is_valid():
111 callback.call(null, "HTTP %d" % response_code)
112 return
113
114 var text := body.get_string_from_utf8()
115 var data = JSON.parse_string(text)
116 if data == null:
117 if callback.is_valid():
118 callback.call(null, "Invalid JSON")
119 return
120
121 if not is_connected:
122 is_connected = true
123 connected.emit()
124
125 if callback.is_valid():
126 callback.call(data.get("result"), data.get("error"))
127
128
129func _on_get_response(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray, callback: Callable, req: HTTPRequest) -> void:
130 req.queue_free()
131
132 if result != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
133 if callback.is_valid():
134 callback.call(null, "GET failed")
135 return
136
137 var text := body.get_string_from_utf8()
138 var data = JSON.parse_string(text)
139 if callback.is_valid():
140 callback.call(data, null)
141
142
143func _check_health() -> void:
144 check_connection()
145
146
147func _handle_disconnect() -> void:
148 if is_connected:
149 is_connected = false
150 disconnected.emit()
keet-social/addons/keet_social/keet_feed.gd +114 -0
……@@ -0,0 +1,114 @@
1class_name KeetFeed
2extends Node
3## Multi-room message aggregator.
4## Polls multiple rooms and emits a unified timeline of messages.
5
6signal feed_updated(messages: Array)
7signal new_entry(room_id: String, message: Dictionary)
8signal error_occurred(message: String)
9
10@export var client: KeetClient
11@export var auto_poll: bool = false
12@export_range(1.0, 30.0) var poll_interval: float = 5.0
13@export_range(1, 50) var per_room_limit: int = 10
14
15var _watched_rooms: Array[String] = []
16var _poll_timer: Timer
17var _known_ids: Dictionary = {}
18var _timeline: Array = []
19
20
21func _ready() -> void:
22 _poll_timer = Timer.new()
23 _poll_timer.wait_time = poll_interval
24 _poll_timer.timeout.connect(_poll_all)
25 add_child(_poll_timer)
26 if auto_poll and _watched_rooms.size() > 0:
27 start()
28
29
30## Add a room to the feed.
31func add_room(room_id: String) -> void:
32 if room_id not in _watched_rooms:
33 _watched_rooms.append(room_id)
34
35
36## Remove a room from the feed.
37func remove_room(room_id: String) -> void:
38 _watched_rooms.erase(room_id)
39
40
41## Set all watched rooms at once.
42func set_rooms(room_ids: Array[String]) -> void:
43 _watched_rooms = room_ids
44
45
46## Start polling all watched rooms.
47func start() -> void:
48 if _poll_timer.is_stopped():
49 _poll_timer.start()
50 _poll_all()
51
52
53## Stop polling.
54func stop() -> void:
55 _poll_timer.stop()
56
57
58## Get the current timeline (sorted by timestamp, newest first).
59func get_timeline() -> Array:
60 return _timeline
61
62
63## Refresh the feed immediately.
64func refresh() -> void:
65 _poll_all()
66
67
68func _poll_all() -> void:
69 if client == null or not client.is_connected:
70 return
71 for rid: String in _watched_rooms:
72 _poll_room(rid)
73
74
75func _poll_room(room_id: String) -> void:
76 client.rpc("read", {"roomId": room_id, "limit": per_room_limit}, func(result, err):
77 if err != null:
78 return
79 if not result is Array:
80 return
81 var has_new := false
82 for msg: Variant in result:
83 if not msg is Dictionary:
84 continue
85 var mid: String = _msg_id(room_id, msg)
86 if _known_ids.has(mid):
87 continue
88 _known_ids[mid] = true
89 var entry: Dictionary = {
90 "room_id": room_id,
91 "message": msg,
92 "timestamp": msg.get("timestamp", 0)
93 }
94 _timeline.append(entry)
95 has_new = true
96 new_entry.emit(room_id, msg)
97 if has_new:
98 _timeline.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
99 return a.get("timestamp", 0) > b.get("timestamp", 0)
100 )
101 # Cap timeline size
102 if _timeline.size() > 200:
103 _timeline.resize(200)
104 feed_updated.emit(_timeline)
105 )
106
107
108func _msg_id(room_id: String, msg: Dictionary) -> String:
109 var id: Variant = msg.get("id", null)
110 if id is Dictionary:
111 return "%s/%s:%d" % [room_id, id.get("deviceId", ""), id.get("seq", 0)]
112 if id is String:
113 return "%s/%s" % [room_id, id]
114 return "%s/%s:%d" % [room_id, msg.get("memberId", ""), msg.get("seq", 0)]
keet-social/addons/keet_social/keet_identity.gd +95 -0
……@@ -0,0 +1,95 @@
1class_name KeetIdentity
2extends Node
3## Identity management for keet-daemon.
4## Get/set display name, lookup users, manage profile.
5
6signal identity_loaded(identity: Dictionary)
7signal name_changed(name: String)
8signal member_found(member: Dictionary)
9signal error_occurred(message: String)
10
11@export var client: KeetClient
12
13
14## Get the current identity.
15func get_identity(callback: Callable = Callable()) -> void:
16 _require_client()
17 client.rpc("identity", {}, func(result, err):
18 if err != null:
19 error_occurred.emit("get_identity: %s" % str(err))
20 return
21 var ident: Dictionary = result if result is Dictionary else {}
22 identity_loaded.emit(ident)
23 if callback.is_valid():
24 callback.call(ident)
25 )
26
27
28## Set display name.
29func set_name(display_name: String, callback: Callable = Callable()) -> void:
30 _require_client()
31 client.rpc("identity.setName", {"name": display_name}, func(result, err):
32 if err != null:
33 error_occurred.emit("set_name: %s" % str(err))
34 return
35 name_changed.emit(display_name)
36 if callback.is_valid():
37 callback.call(result)
38 )
39
40
41## Check if a username is available.
42func check_username(username: String, callback: Callable = Callable()) -> void:
43 _require_client()
44 client.rpc("identity.checkName", {"username": username}, func(result, err):
45 if err != null:
46 error_occurred.emit("check_username: %s" % str(err))
47 return
48 if callback.is_valid():
49 callback.call(result)
50 )
51
52
53## Lookup a user by username.
54func lookup_user(username: String, callback: Callable = Callable()) -> void:
55 _require_client()
56 client.rpc("identity.lookup", {"username": username}, func(result, err):
57 if err != null:
58 error_occurred.emit("lookup_user: %s" % str(err))
59 return
60 var member: Dictionary = result if result is Dictionary else {}
61 member_found.emit(member)
62 if callback.is_valid():
63 callback.call(member)
64 )
65
66
67## Lookup a member by ID within a room.
68func lookup_member(room_id: String, member_id: String, callback: Callable = Callable()) -> void:
69 _require_client()
70 client.rpc("identity.lookupMember", {"roomId": room_id, "memberId": member_id}, func(result, err):
71 if err != null:
72 error_occurred.emit("lookup_member: %s" % str(err))
73 return
74 var member: Dictionary = result if result is Dictionary else {}
75 member_found.emit(member)
76 if callback.is_valid():
77 callback.call(member)
78 )
79
80
81## Get daemon status (uptime, rooms, identity summary).
82func get_status(callback: Callable = Callable()) -> void:
83 _require_client()
84 client.rpc("status", {}, func(result, err):
85 if err != null:
86 error_occurred.emit("get_status: %s" % str(err))
87 return
88 if callback.is_valid():
89 callback.call(result)
90 )
91
92
93func _require_client() -> void:
94 if client == null:
95 push_error("KeetIdentity: client not set. Assign a KeetClient node.")
keet-social/addons/keet_social/keet_room.gd +130 -0
……@@ -0,0 +1,130 @@
1class_name KeetRoom
2extends Node
3## Room management for keet-daemon.
4## List, join, leave, create, and configure rooms.
5
6signal rooms_loaded(rooms: Array)
7signal room_joined(room_id: String)
8signal room_left(room_id: String)
9signal room_created(room_id: String)
10signal members_loaded(room_id: String, members: Array)
11signal invite_created(room_id: String, invite: String)
12signal error_occurred(message: String)
13
14@export var client: KeetClient
15
16
17## List all rooms.
18func list_rooms(callback: Callable = Callable()) -> void:
19 _require_client()
20 client.rpc("rooms", {}, func(result, err):
21 if err != null:
22 error_occurred.emit("list_rooms: %s" % str(err))
23 return
24 var rooms: Array = result if result is Array else []
25 rooms_loaded.emit(rooms)
26 if callback.is_valid():
27 callback.call(rooms)
28 )
29
30
31## Get info for a specific room.
32func get_room(room_id: String, callback: Callable = Callable()) -> void:
33 _require_client()
34 client.rpc("room", {"roomId": room_id}, func(result, err):
35 if err != null:
36 error_occurred.emit("get_room: %s" % str(err))
37 return
38 if callback.is_valid():
39 callback.call(result)
40 )
41
42
43## Create a new room.
44func create_room(title: String, callback: Callable = Callable()) -> void:
45 _require_client()
46 client.rpc("room.create", {"title": title}, func(result, err):
47 if err != null:
48 error_occurred.emit("create_room: %s" % str(err))
49 return
50 var rid: String = result.get("roomId", "") if result is Dictionary else ""
51 room_created.emit(rid)
52 if callback.is_valid():
53 callback.call(result)
54 )
55
56
57## Join a room by invite link or token.
58func join_room(invite: String, callback: Callable = Callable()) -> void:
59 _require_client()
60 client.rpc("room.join", {"invite": invite}, func(result, err):
61 if err != null:
62 error_occurred.emit("join_room: %s" % str(err))
63 return
64 var rid: String = result.get("roomId", "") if result is Dictionary else ""
65 room_joined.emit(rid)
66 if callback.is_valid():
67 callback.call(result)
68 )
69
70
71## Leave a room.
72func leave_room(room_id: String, callback: Callable = Callable()) -> void:
73 _require_client()
74 client.rpc("room.leave", {"roomId": room_id}, func(result, err):
75 if err != null:
76 error_occurred.emit("leave_room: %s" % str(err))
77 return
78 room_left.emit(room_id)
79 if callback.is_valid():
80 callback.call(result)
81 )
82
83
84## Get an invite token for a room.
85func create_invite(room_id: String, callback: Callable = Callable()) -> void:
86 _require_client()
87 client.rpc("room.invite", {"roomId": room_id}, func(result, err):
88 if err != null:
89 error_occurred.emit("create_invite: %s" % str(err))
90 return
91 var token: String = ""
92 if result is Dictionary:
93 token = result.get("token", result.get("invite", ""))
94 elif result is String:
95 token = result
96 invite_created.emit(room_id, token)
97 if callback.is_valid():
98 callback.call(token)
99 )
100
101
102## List members of a room.
103func list_members(room_id: String, callback: Callable = Callable()) -> void:
104 _require_client()
105 client.rpc("members", {"roomId": room_id}, func(result, err):
106 if err != null:
107 error_occurred.emit("list_members: %s" % str(err))
108 return
109 var members: Array = result if result is Array else []
110 members_loaded.emit(room_id, members)
111 if callback.is_valid():
112 callback.call(members)
113 )
114
115
116## Set room configuration (title, description, etc).
117func set_config(room_id: String, key: String, value: Variant, callback: Callable = Callable()) -> void:
118 _require_client()
119 client.rpc("room.config.set", {"roomId": room_id, "key": key, "value": value}, func(result, err):
120 if err != null:
121 error_occurred.emit("set_config: %s" % str(err))
122 return
123 if callback.is_valid():
124 callback.call(result)
125 )
126
127
128func _require_client() -> void:
129 if client == null:
130 push_error("KeetRoom: client not set. Assign a KeetClient node.")
keet-social/addons/keet_social/plugin.cfg +7 -0
……@@ -0,0 +1,7 @@
1[plugin]
2
3name="Keet Social"
4description="P2P social integration via keet-daemon. Chat, rooms, identity, and feeds."
5author="hyper-social"
6version="0.1.0"
7script="plugin.gd"
keet-social/addons/keet_social/plugin.gd +18 -0
……@@ -0,0 +1,18 @@
1@tool
2extends EditorPlugin
3
4
5func _enter_tree() -> void:
6 add_custom_type("KeetClient", "Node", preload("keet_client.gd"), null)
7 add_custom_type("KeetRoom", "Node", preload("keet_room.gd"), null)
8 add_custom_type("KeetChat", "Node", preload("keet_chat.gd"), null)
9 add_custom_type("KeetIdentity", "Node", preload("keet_identity.gd"), null)
10 add_custom_type("KeetFeed", "Node", preload("keet_feed.gd"), null)
11
12
13func _exit_tree() -> void:
14 remove_custom_type("KeetClient")
15 remove_custom_type("KeetRoom")
16 remove_custom_type("KeetChat")
17 remove_custom_type("KeetIdentity")
18 remove_custom_type("KeetFeed")
keet-social/demo/chat_demo.gd +115 -0
……@@ -0,0 +1,115 @@
1extends Control
2## 2D chat demo using keet-social plugin.
3## Connects to keet-daemon, lists rooms, and enables chatting.
4
5@onready var client: KeetClient = $KeetClient
6@onready var room_manager: KeetRoom = $KeetRoom
7@onready var chat: KeetChat = $KeetChat
8@onready var identity: KeetIdentity = $KeetIdentity
9
10@onready var status_label: Label = $UI/TopBar/StatusLabel
11@onready var room_list: ItemList = $UI/HSplit/RoomPanel/RoomList
12@onready var chat_log: RichTextLabel = $UI/HSplit/ChatPanel/VBox/ChatLog
13@onready var input_field: LineEdit = $UI/HSplit/ChatPanel/VBox/HBox/InputField
14@onready var send_button: Button = $UI/HSplit/ChatPanel/VBox/HBox/SendButton
15@onready var room_title: Label = $UI/HSplit/ChatPanel/RoomTitle
16
17var _rooms: Array = []
18var _current_room: String = ""
19var _identity_name: String = "unknown"
20
21
22func _ready() -> void:
23 # Wire up client
24 client.connected.connect(_on_connected)
25 client.disconnected.connect(_on_disconnected)
26
27 # Wire up room manager
28 room_manager.client = client
29 room_manager.rooms_loaded.connect(_on_rooms_loaded)
30
31 # Wire up chat
32 chat.client = client
33 chat.message_received.connect(_on_message_received)
34 chat.messages_loaded.connect(_on_history_loaded)
35
36 # Wire up identity
37 identity.client = client
38
39 # Wire up UI
40 send_button.pressed.connect(_on_send)
41 input_field.text_submitted.connect(_on_text_submitted)
42 room_list.item_selected.connect(_on_room_selected)
43
44 status_label.text = "Connecting..."
45 room_title.text = "Select a room"
46
47
48func _on_connected() -> void:
49 status_label.text = "Connected"
50 identity.get_identity(func(ident: Dictionary):
51 _identity_name = ident.get("displayName", ident.get("name", "unknown"))
52 status_label.text = "Connected as %s" % _identity_name
53 )
54 room_manager.list_rooms()
55
56
57func _on_disconnected() -> void:
58 status_label.text = "Disconnected - retrying..."
59 chat.stop_polling()
60
61
62func _on_rooms_loaded(rooms: Array) -> void:
63 _rooms = rooms
64 room_list.clear()
65 for room: Variant in rooms:
66 if room is Dictionary:
67 room_list.add_item(room.get("title", room.get("roomId", "?")))
68
69
70func _on_room_selected(index: int) -> void:
71 if index < 0 or index >= _rooms.size():
72 return
73 var room: Dictionary = _rooms[index]
74 _current_room = room.get("roomId", "")
75 room_title.text = room.get("title", _current_room)
76 chat_log.clear()
77 chat.watch(_current_room)
78 chat.load_history()
79
80
81func _on_history_loaded(messages: Array) -> void:
82 chat_log.clear()
83 for msg: Variant in messages:
84 if msg is Dictionary:
85 _append_message(msg)
86
87
88func _on_message_received(msg: Dictionary) -> void:
89 _append_message(msg)
90
91
92func _on_send() -> void:
93 _send_message()
94
95
96func _on_text_submitted(_text: String) -> void:
97 _send_message()
98
99
100func _send_message() -> void:
101 var text := input_field.text.strip_edges()
102 if text == "" or _current_room == "":
103 return
104 input_field.text = ""
105 chat.send(text)
106
107
108func _append_message(msg: Dictionary) -> void:
109 var name: String = msg.get("name", msg.get("memberId", "?"))
110 var text: String = msg.get("text", "")
111 var is_system: bool = msg.get("system", false)
112 if is_system:
113 chat_log.append_text("[color=gray]--- %s ---[/color]\n" % text)
114 else:
115 chat_log.append_text("[b]%s:[/b] %s\n" % [name, text])
keet-social/demo/chat_demo.tscn +106 -0
……@@ -0,0 +1,106 @@
1[gd_scene load_steps=7 format=3 uid="uid://demo_chat"]
2
3[ext_resource type="Script" path="res://demo/chat_demo.gd" id="1"]
4[ext_resource type="Script" path="res://addons/keet_social/keet_client.gd" id="2"]
5[ext_resource type="Script" path="res://addons/keet_social/keet_room.gd" id="3"]
6[ext_resource type="Script" path="res://addons/keet_social/keet_chat.gd" id="4"]
7[ext_resource type="Script" path="res://addons/keet_social/keet_identity.gd" id="5"]
8[ext_resource type="Script" path="res://addons/keet_social/keet_feed.gd" id="6"]
9
10[node name="ChatDemo" type="Control"]
11layout_mode = 3
12anchors_preset = 15
13anchor_right = 1.0
14anchor_bottom = 1.0
15grow_horizontal = 2
16grow_vertical = 2
17script = ExtResource("1")
18
19[node name="KeetClient" type="Node" parent="."]
20script = ExtResource("2")
21
22[node name="KeetRoom" type="Node" parent="."]
23script = ExtResource("3")
24
25[node name="KeetChat" type="Node" parent="."]
26script = ExtResource("4")
27
28[node name="KeetIdentity" type="Node" parent="."]
29script = ExtResource("5")
30
31[node name="KeetFeed" type="Node" parent="."]
32script = ExtResource("6")
33
34[node name="UI" type="VBoxContainer" parent="."]
35layout_mode = 1
36anchors_preset = 15
37anchor_right = 1.0
38anchor_bottom = 1.0
39grow_horizontal = 2
40grow_vertical = 2
41
42[node name="TopBar" type="HBoxContainer" parent="UI"]
43layout_mode = 2
44custom_minimum_size = Vector2(0, 40)
45
46[node name="TitleLabel" type="Label" parent="UI/TopBar"]
47layout_mode = 2
48text = "Keet Social"
49
50[node name="HSeparator" type="HSeparator" parent="UI/TopBar"]
51layout_mode = 2
52size_flags_horizontal = 3
53
54[node name="StatusLabel" type="Label" parent="UI/TopBar"]
55layout_mode = 2
56text = "Disconnected"
57horizontal_alignment = 2
58
59[node name="HSplit" type="HSplitContainer" parent="UI"]
60layout_mode = 2
61size_flags_vertical = 3
62split_offset = 200
63
64[node name="RoomPanel" type="VBoxContainer" parent="UI/HSplit"]
65layout_mode = 2
66custom_minimum_size = Vector2(180, 0)
67
68[node name="RoomLabel" type="Label" parent="UI/HSplit/RoomPanel"]
69layout_mode = 2
70text = "Rooms"
71
72[node name="RoomList" type="ItemList" parent="UI/HSplit/RoomPanel"]
73layout_mode = 2
74size_flags_vertical = 3
75
76[node name="ChatPanel" type="VBoxContainer" parent="UI/HSplit"]
77layout_mode = 2
78size_flags_horizontal = 3
79
80[node name="RoomTitle" type="Label" parent="UI/HSplit/ChatPanel"]
81layout_mode = 2
82text = "Select a room"
83
84[node name="VBox" type="VBoxContainer" parent="UI/HSplit/ChatPanel"]
85layout_mode = 2
86size_flags_vertical = 3
87
88[node name="ChatLog" type="RichTextLabel" parent="UI/HSplit/ChatPanel/VBox"]
89layout_mode = 2
90size_flags_vertical = 3
91bbcode_enabled = true
92scroll_following = true
93
94[node name="HBox" type="HBoxContainer" parent="UI/HSplit/ChatPanel/VBox"]
95layout_mode = 2
96custom_minimum_size = Vector2(0, 36)
97
98[node name="InputField" type="LineEdit" parent="UI/HSplit/ChatPanel/VBox/HBox"]
99layout_mode = 2
100size_flags_horizontal = 3
101placeholder_text = "Type a message..."
102
103[node name="SendButton" type="Button" parent="UI/HSplit/ChatPanel/VBox/HBox"]
104layout_mode = 2
105custom_minimum_size = Vector2(80, 0)
106text = "Send"
keet-social/project.godot +13 -0
……@@ -0,0 +1,13 @@
1; Engine configuration file.
2; It's best edited using the editor UI and not directly,
3; but it can also be edited via text for source control.
4
5[application]
6
7config/name="Keet Social Demo"
8run/main_scene="res://demo/chat_demo.tscn"
9config/features=PackedStringArray("4.3")
10
11[editor_plugins]
12
13enabled=PackedStringArray("res://addons/keet_social/plugin.cfg")