p2-3
This commit is contained in:
@@ -5,6 +5,9 @@ node_modules/
|
||||
out/
|
||||
dist/
|
||||
deploy/
|
||||
# vendored at postinstall
|
||||
build/shims/events-polyfill.cjs
|
||||
build/squid-imports.json
|
||||
*.AppImage
|
||||
*.dmg
|
||||
*.snap
|
||||
|
||||
@@ -59,7 +59,10 @@ $BARE bin.mjs host demo --port 25565 # print fj1. invite; local Squid
|
||||
$BARE bin.mjs join 'fj1.…' --port 25566 # guest tunnel
|
||||
|
||||
# Minecraft Java → 127.0.0.1:<port shown>
|
||||
# Chat: type lines while host/join is running; /peers
|
||||
|
||||
npm test
|
||||
npm run make:standalone # single Bare executable → out/flying-jib-<host>/
|
||||
```
|
||||
|
||||
See [developer_docs/SETUP.md](./developer_docs/SETUP.md) and [living_docs/CURRENT_STATUS.md](./living_docs/CURRENT_STATUS.md).
|
||||
|
||||
@@ -11,14 +11,15 @@ const { ensureDir } = require('./lib/worlds')
|
||||
const { WorldTunnelHost, WorldTunnelClient } = require('./lib/world-tunnel')
|
||||
const { loadOrCreateTunnelKeys } = require('./lib/tunnel-keys')
|
||||
const { encodeInvite, decodeInvite, decodeKey } = require('./lib/invite')
|
||||
const { PeerSession } = require('./lib/peer-session')
|
||||
|
||||
/**
|
||||
* Flying Jib Bare application shell.
|
||||
* - Optional pear-runtime worker for OTA (hello-pear-bare pattern)
|
||||
* - Squid + HyperDHT tunnels run in this process under Bare (ADR-0013)
|
||||
* - Squid + HyperDHT tunnels + Protomux peer session in this process (ADR-0013)
|
||||
*/
|
||||
module.exports = class App extends ReadyResource {
|
||||
constructor({ dir, appPath, updates, version, upgrade, name }) {
|
||||
constructor({ dir, appPath, updates, version, upgrade, name, displayName }) {
|
||||
super()
|
||||
this.dir = dir
|
||||
this.appPath = appPath
|
||||
@@ -26,11 +27,13 @@ module.exports = class App extends ReadyResource {
|
||||
this.version = version
|
||||
this.upgrade = upgrade
|
||||
this.name = name
|
||||
this.displayName = displayName || 'player'
|
||||
this.IPC = null
|
||||
this.pipe = null
|
||||
this.squid = null
|
||||
this.tunnelHost = null
|
||||
this.tunnelClient = null
|
||||
this.peerSession = null
|
||||
this.activeWorld = null
|
||||
this._shuttingDown = false
|
||||
}
|
||||
@@ -63,6 +66,20 @@ module.exports = class App extends ReadyResource {
|
||||
}
|
||||
|
||||
async _close() {
|
||||
await this._teardownSession()
|
||||
const pipe = this.pipe
|
||||
const IPC = this.IPC
|
||||
this.pipe = null
|
||||
this.IPC = null
|
||||
pipe?.destroy()
|
||||
IPC?.destroy()
|
||||
}
|
||||
|
||||
async _teardownSession() {
|
||||
if (this.peerSession) {
|
||||
await this.peerSession.close().catch(() => {})
|
||||
this.peerSession = null
|
||||
}
|
||||
if (this.tunnelClient) {
|
||||
await this.tunnelClient.close().catch(() => {})
|
||||
this.tunnelClient = null
|
||||
@@ -76,12 +93,6 @@ module.exports = class App extends ReadyResource {
|
||||
this.squid = null
|
||||
}
|
||||
this.activeWorld = null
|
||||
const pipe = this.pipe
|
||||
const IPC = this.IPC
|
||||
this.pipe = null
|
||||
this.IPC = null
|
||||
pipe?.destroy()
|
||||
IPC?.destroy()
|
||||
}
|
||||
|
||||
_onWorkerMessage(data) {
|
||||
@@ -106,6 +117,25 @@ module.exports = class App extends ReadyResource {
|
||||
if (this.pipe) this.pipe.write(message)
|
||||
}
|
||||
|
||||
async _startPeerSession(worldPublicKey) {
|
||||
if (this.peerSession) {
|
||||
await this.peerSession.close().catch(() => {})
|
||||
this.peerSession = null
|
||||
}
|
||||
this.peerSession = new PeerSession({
|
||||
worldPublicKey,
|
||||
displayName: this.displayName
|
||||
})
|
||||
this.peerSession.on('chat', (m) => this.emit('chat', m))
|
||||
this.peerSession.on('presence', (m) => this.emit('presence', m))
|
||||
this.peerSession.on('peers', (list) => this.emit('peers', list))
|
||||
this.peerSession.on('peer-join', (m) => this.emit('peer-join', m))
|
||||
this.peerSession.on('peer-leave', (m) => this.emit('peer-leave', m))
|
||||
this.peerSession.on('error', (err) => this.emit('error', err))
|
||||
await this.peerSession.ready()
|
||||
this.emit('message', `Peer session ready (id ${this.peerSession.peerId})`)
|
||||
}
|
||||
|
||||
listWorlds() {
|
||||
return listWorlds(this.dir)
|
||||
}
|
||||
@@ -145,10 +175,11 @@ module.exports = class App extends ReadyResource {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Squid + HyperDHT host tunnel; return status + invite string.
|
||||
* @param {{ world: string, port?: number, version?: string }} opts
|
||||
* Start Squid + HyperDHT host tunnel + peer chat/presence.
|
||||
* @param {{ world: string, port?: number, version?: string, displayName?: string }} opts
|
||||
*/
|
||||
async hostWorld(opts) {
|
||||
if (opts.displayName) this.displayName = opts.displayName
|
||||
const squidStatus = await this.startWorld(opts)
|
||||
const keys = loadOrCreateTunnelKeys(this.dir, this.activeWorld.id)
|
||||
|
||||
@@ -158,11 +189,13 @@ module.exports = class App extends ReadyResource {
|
||||
cap: keys.cap
|
||||
})
|
||||
this.tunnelHost.on('connection', () => {
|
||||
this.emit('message', 'Remote peer connected via tunnel')
|
||||
this.emit('message', 'Remote peer connected via MC tunnel')
|
||||
})
|
||||
this.tunnelHost.on('error', (err) => this.emit('error', err))
|
||||
await this.tunnelHost.ready()
|
||||
|
||||
await this._startPeerSession(keys.publicKey)
|
||||
|
||||
const invite = encodeInvite({
|
||||
type: 'private-world',
|
||||
worldKey: keys.publicKeyZ32,
|
||||
@@ -175,18 +208,20 @@ module.exports = class App extends ReadyResource {
|
||||
return {
|
||||
squid: squidStatus,
|
||||
tunnel: this.tunnelHost.status,
|
||||
peers: this.peerSession ? this.peerSession.peerList : [],
|
||||
invite
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Join a private world via fj1. invite (client tunnel only; no local Squid).
|
||||
* @param {{ invite: string, localPort?: number }} opts
|
||||
* Join a private world via fj1. invite (tunnel + peer session).
|
||||
* @param {{ invite: string, localPort?: number, displayName?: string }} opts
|
||||
*/
|
||||
async joinWorld(opts) {
|
||||
if (this.tunnelClient) {
|
||||
throw new Error('Already joined a remote world; stop first')
|
||||
}
|
||||
if (opts.displayName) this.displayName = opts.displayName
|
||||
const inv = decodeInvite(opts.invite)
|
||||
const publicKey = decodeKey(inv.worldKey)
|
||||
const cap = decodeKey(inv.cap)
|
||||
@@ -199,40 +234,35 @@ module.exports = class App extends ReadyResource {
|
||||
this.tunnelClient.on('error', (err) => this.emit('error', err))
|
||||
await this.tunnelClient.ready()
|
||||
|
||||
await this._startPeerSession(publicKey)
|
||||
|
||||
return {
|
||||
invite: inv,
|
||||
tunnel: this.tunnelClient.status
|
||||
tunnel: this.tunnelClient.status,
|
||||
peers: this.peerSession ? this.peerSession.peerList : []
|
||||
}
|
||||
}
|
||||
|
||||
sendChat(text) {
|
||||
if (!this.peerSession) throw new Error('No peer session (host or join first)')
|
||||
this.peerSession.sendChat(text)
|
||||
}
|
||||
|
||||
async stopWorld() {
|
||||
let stopped = false
|
||||
if (this.tunnelClient) {
|
||||
await this.tunnelClient.close().catch(() => {})
|
||||
this.tunnelClient = null
|
||||
stopped = true
|
||||
}
|
||||
if (this.tunnelHost) {
|
||||
await this.tunnelHost.close().catch(() => {})
|
||||
this.tunnelHost = null
|
||||
stopped = true
|
||||
}
|
||||
if (this.squid) {
|
||||
await this.squid.close().catch(() => {})
|
||||
this.squid = null
|
||||
stopped = true
|
||||
}
|
||||
this.activeWorld = null
|
||||
return stopped
|
||||
await this._teardownSession()
|
||||
return true
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return {
|
||||
storage: this.dir,
|
||||
world: this.activeWorld,
|
||||
displayName: this.displayName,
|
||||
squid: this.squid ? this.squid.status : { running: false },
|
||||
tunnelHost: this.tunnelHost ? this.tunnelHost.status : null,
|
||||
tunnelClient: this.tunnelClient ? this.tunnelClient.status : null
|
||||
tunnelClient: this.tunnelClient ? this.tunnelClient.status : null,
|
||||
peers: this.peerSession ? this.peerSession.peerList : [],
|
||||
peerId: this.peerSession ? this.peerSession.peerId : null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,13 +14,17 @@ import { createRequire } from 'module'
|
||||
import { command, flag, arg, summary, header, footer } from 'paparam'
|
||||
import path from 'path'
|
||||
import process from 'process'
|
||||
import { fileURLToPath } from 'url'
|
||||
import pkg from './package.json' with { type: 'json' }
|
||||
import App from './app.js'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
// createRequire needs a file: URL in Node; under bare-pack standalone, import.meta.url
|
||||
// may be app.bundle — fall back to a dummy path for require() of CJS shims.
|
||||
let require
|
||||
try {
|
||||
require = createRequire(import.meta.url)
|
||||
} catch {
|
||||
require = createRequire('file:///flying-jib/bin.mjs')
|
||||
}
|
||||
|
||||
const appName = pkg.productName || pkg.name
|
||||
const isBare = typeof Bare !== 'undefined'
|
||||
@@ -48,17 +52,72 @@ function exit(code) {
|
||||
else process.exit(code)
|
||||
}
|
||||
|
||||
function makeApp(storage, updates = false) {
|
||||
function makeApp(storage, updates = false, displayName) {
|
||||
return new App({
|
||||
dir: storage,
|
||||
appPath: isDev ? null : argv0,
|
||||
updates,
|
||||
version: pkg.version,
|
||||
upgrade: pkg.upgrade,
|
||||
name: appName
|
||||
name: appName,
|
||||
displayName: displayName || 'player'
|
||||
})
|
||||
}
|
||||
|
||||
/** Wire chat/presence logging + optional stdin chat lines for host/join */
|
||||
function attachPeerUi(app) {
|
||||
app.on('chat', (m) => {
|
||||
const tag = m.local ? 'you' : m.from
|
||||
console.log(`[chat] <${tag}> ${m.text}`)
|
||||
})
|
||||
app.on('peer-join', (m) => {
|
||||
console.log(`[peers] joined ${m.peerId.slice(0, 12)}…`)
|
||||
})
|
||||
app.on('peer-leave', (m) => {
|
||||
console.log(`[peers] left ${m.peerId.slice(0, 12)}…`)
|
||||
})
|
||||
app.on('peers', (list) => {
|
||||
if (list.length) {
|
||||
console.log(`[peers] online: ${list.map((p) => p.name).join(', ')}`)
|
||||
}
|
||||
})
|
||||
|
||||
// Type lines to chat (host/join sessions)
|
||||
if (process.stdin && process.stdin.isTTY && typeof process.stdin.on === 'function') {
|
||||
try {
|
||||
process.stdin.setEncoding('utf8')
|
||||
process.stdin.resume()
|
||||
let buf = ''
|
||||
process.stdin.on('data', (chunk) => {
|
||||
buf += chunk
|
||||
let i
|
||||
while ((i = buf.indexOf('\n')) >= 0) {
|
||||
const line = buf.slice(0, i).replace(/\r$/, '').trim()
|
||||
buf = buf.slice(i + 1)
|
||||
if (!line) continue
|
||||
if (line === '/peers') {
|
||||
const st = app.getStatus()
|
||||
console.log(
|
||||
'[peers]',
|
||||
st.peers.length
|
||||
? st.peers.map((p) => `${p.name}(${p.id.slice(0, 8)})`).join(', ')
|
||||
: '(none)'
|
||||
)
|
||||
continue
|
||||
}
|
||||
try {
|
||||
app.sendChat(line)
|
||||
} catch (err) {
|
||||
console.error('[chat]', err.message)
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
// non-interactive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Attach signal handlers once; re-entrant safe via app.exit */
|
||||
function attachShutdown(app) {
|
||||
let stopping = false
|
||||
@@ -182,25 +241,29 @@ const hostCmd = command(
|
||||
arg('<name>', 'world name / id'),
|
||||
flag('--port|-p [port]', 'local Minecraft port (default 25565)'),
|
||||
flag('--version [ver]', 'override Minecraft version'),
|
||||
flag('--name|-n [display]', 'chat display name'),
|
||||
async (cmd) => {
|
||||
const storage = resolveStorage(root.flags.storage)
|
||||
const app = makeApp(storage, false)
|
||||
const app = makeApp(storage, false, cmd.flags.name)
|
||||
app.on('message', (m) => console.log(m))
|
||||
app.on('error', (err) => console.error('[error]', err))
|
||||
attachShutdown(app)
|
||||
attachPeerUi(app)
|
||||
|
||||
await app.ready()
|
||||
try {
|
||||
const result = await app.hostWorld({
|
||||
world: cmd.args.name,
|
||||
port: cmd.flags.port ? Number(cmd.flags.port) : 25565,
|
||||
version: cmd.flags.version
|
||||
version: cmd.flags.version,
|
||||
displayName: cmd.flags.name
|
||||
})
|
||||
console.log('')
|
||||
console.log(`${appName} hosting private world (P2P)`)
|
||||
console.log(` world: ${cmd.args.name}`)
|
||||
console.log(` local: ${result.squid.host}:${result.squid.port}`)
|
||||
console.log(` tunnel: HyperDHT host`)
|
||||
console.log(` chat: type lines + Enter; /peers for list`)
|
||||
console.log('')
|
||||
console.log('Invite (treat as secret — fj1. capability):')
|
||||
console.log(result.invite)
|
||||
@@ -226,18 +289,21 @@ const joinCmd = command(
|
||||
summary('Join a private world from an fj1. invite'),
|
||||
arg('<invite>', 'fj1. invite string'),
|
||||
flag('--port|-p [port]', 'local bind port (default: ephemeral)'),
|
||||
flag('--name|-n [display]', 'chat display name'),
|
||||
async (cmd) => {
|
||||
const storage = resolveStorage(root.flags.storage)
|
||||
const app = makeApp(storage, false)
|
||||
const app = makeApp(storage, false, cmd.flags.name)
|
||||
app.on('message', (m) => console.log(m))
|
||||
app.on('error', (err) => console.error('[error]', err))
|
||||
attachShutdown(app)
|
||||
attachPeerUi(app)
|
||||
|
||||
await app.ready()
|
||||
try {
|
||||
const result = await app.joinWorld({
|
||||
invite: cmd.args.invite,
|
||||
localPort: cmd.flags.port ? Number(cmd.flags.port) : 0
|
||||
localPort: cmd.flags.port ? Number(cmd.flags.port) : 0,
|
||||
displayName: cmd.flags.name
|
||||
})
|
||||
const port = result.tunnel.localPort
|
||||
console.log('')
|
||||
@@ -245,6 +311,7 @@ const joinCmd = command(
|
||||
console.log(` name: ${result.invite.name || '(unnamed)'}`)
|
||||
console.log(` version: ${result.invite.mcVersion || 'unknown'}`)
|
||||
console.log(` local: 127.0.0.1:${port}`)
|
||||
console.log(` chat: type lines + Enter; /peers for list`)
|
||||
console.log('')
|
||||
console.log('Connect Java Edition to:')
|
||||
console.log(` 127.0.0.1:${port}`)
|
||||
@@ -297,6 +364,3 @@ if (root.flags.version) {
|
||||
console.log(`${appName} v${pkg.version}`)
|
||||
exit(0)
|
||||
}
|
||||
|
||||
void __dirname
|
||||
void __filename
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Flying Squid / readable-stream use `EventEmitter.call(this)` (util.inherits).
|
||||
* bare-events is an ES class (throws without `new`).
|
||||
*
|
||||
* Load the classic npm `events` package by absolute path so we do not recurse
|
||||
* through package.json / bare-node-runtime "events" import maps.
|
||||
* Callable EventEmitter for Flying Squid / readable-stream (util.inherits).
|
||||
* Vendored polyfill is inlined so bare-pack standalone does not need nested requires.
|
||||
*/
|
||||
const path = require('path')
|
||||
const eventsJs = path.join(__dirname, '..', '..', 'node_modules', 'events', 'events.js')
|
||||
module.exports = require(eventsJs)
|
||||
module.exports = require('./events-polyfill.cjs')
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
'use strict'; module.exports = {}
|
||||
@@ -0,0 +1 @@
|
||||
'use strict'; module.exports = {}
|
||||
@@ -0,0 +1 @@
|
||||
'use strict'; module.exports = {}
|
||||
@@ -0,0 +1 @@
|
||||
'use strict'; module.exports = {}
|
||||
@@ -0,0 +1 @@
|
||||
'use strict'; module.exports = {}
|
||||
@@ -0,0 +1 @@
|
||||
'use strict'; module.exports = {}
|
||||
@@ -0,0 +1,2 @@
|
||||
'use strict'
|
||||
module.exports = {}
|
||||
@@ -0,0 +1,2 @@
|
||||
'use strict'
|
||||
module.exports = false
|
||||
@@ -54,9 +54,20 @@ Optional packaged forms (`.AppImage`, `.pkg`, `.msix`) via bare-build package mo
|
||||
|
||||
## Flying Squid in the bundle
|
||||
|
||||
- Include Prismarine graph with **`bare-node-runtime`** + project **`build/squid-imports.json`** (generated postinstall)
|
||||
- **Note:** plain `bare-build` currently fails resolving flying-squid’s browser-only `esbuild-import-glob(...)` specifier. Prefer peardock-style **`bare-pack` with imports** then embed into bare-runtime (see peardock `scripts/bare-standalone.cjs`). Stub at `build/stubs/esbuild-import-glob.cjs` is prepared for that path.
|
||||
- Natives: only target host prebuilds (prune multi-arch bloat)
|
||||
Use the peardock-style packer (not plain `bare-build` CLI):
|
||||
|
||||
```sh
|
||||
npm run make:standalone # current host
|
||||
npm run make:standalone:all # all 64-bit hosts
|
||||
# → out/flying-jib-<host>/flying-jib[.exe]
|
||||
```
|
||||
|
||||
Script: `scripts/bare-standalone.cjs`
|
||||
- `bare-pack` + `build/squid-imports.json` + stubs
|
||||
- Postinstall patches: static Squid plugins, log.js, engines, events polyfill
|
||||
|
||||
Verified: standalone binary starts Squid on `127.0.0.1` with empty PATH (no system Node).
|
||||
Size today ~500MB — optimize later (prune natives / strip).
|
||||
|
||||
|
||||
## Pear deployment layers
|
||||
|
||||
@@ -99,12 +99,14 @@ Stream framing: Hyperswarm/HyperDHT secretstream (already message-oriented for P
|
||||
|
||||
| Protocol name | Phase | Purpose |
|
||||
|---------------|-------|---------|
|
||||
| `flying-jib/presence` | 3 | Peer id, display name, world/region, heartbeat |
|
||||
| `flying-jib/chat` | 3 | Side-channel chat messages |
|
||||
| `flying-jib/control` | 2–3 | Session hello, errors, version negotiate |
|
||||
| `flying-jib/presence` | 3 | Peer id, display name, heartbeat (**implemented**) |
|
||||
| `flying-jib/chat` | 3 | Side-channel chat messages (**implemented**) |
|
||||
| `flying-jib/control` | 3 | Control messages (channel open; payload TBD) |
|
||||
| `flying-jib/migrate` | 4 | Border handoff state |
|
||||
| `flying-jib/registry` | 4 | Optional live region gossip (Autobase is source of truth) |
|
||||
|
||||
**Topic:** `hash(worldPublicKey || 'flying-jib/control/v1')` via Hyperswarm (separate from MC TCP tunnel).
|
||||
|
||||
### Presence message (logical)
|
||||
|
||||
```json
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Side-channel peer session for a private world (Phase 3).
|
||||
* Hyperswarm topic derived from world public key; Protomux channels:
|
||||
* flying-jib/presence
|
||||
* flying-jib/chat
|
||||
* flying-jib/control
|
||||
*
|
||||
* Separate from the MC TCP tunnel (ADR-0006 data plane).
|
||||
*/
|
||||
|
||||
const ReadyResource = require('ready-resource')
|
||||
const Hyperswarm = require('hyperswarm')
|
||||
const Protomux = require('protomux')
|
||||
const c = require('compact-encoding')
|
||||
const b4a = require('b4a')
|
||||
const crypto = require('hypercore-crypto')
|
||||
|
||||
const PROTO_PRESENCE = 'flying-jib/presence'
|
||||
const PROTO_CHAT = 'flying-jib/chat'
|
||||
const PROTO_CONTROL = 'flying-jib/control'
|
||||
|
||||
const presenceEncoding = c.json
|
||||
const chatEncoding = c.json
|
||||
const controlEncoding = c.json
|
||||
|
||||
/**
|
||||
* 32-byte topic for private-world control plane.
|
||||
* @param {Buffer} worldPublicKey
|
||||
*/
|
||||
function controlTopic(worldPublicKey) {
|
||||
return crypto.hash(b4a.concat([b4a.from(worldPublicKey), b4a.from('flying-jib/control/v1')]))
|
||||
}
|
||||
|
||||
class PeerSession extends ReadyResource {
|
||||
/**
|
||||
* @param {object} opts
|
||||
* @param {Buffer} opts.worldPublicKey
|
||||
* @param {string} [opts.displayName]
|
||||
* @param {Buffer} [opts.keyPairSeed] optional stable Noise key seed
|
||||
*/
|
||||
constructor(opts) {
|
||||
super()
|
||||
if (!opts || !opts.worldPublicKey) throw new Error('PeerSession requires worldPublicKey')
|
||||
this.worldPublicKey = b4a.from(opts.worldPublicKey)
|
||||
this.displayName = opts.displayName || 'player'
|
||||
this.keyPairSeed = opts.keyPairSeed || null
|
||||
this.swarm = null
|
||||
this.topic = controlTopic(this.worldPublicKey)
|
||||
this.peers = new Map() // remotePublicKey hex -> { name, lastSeen, mux }
|
||||
this._heartbeat = null
|
||||
this._peerId = null
|
||||
}
|
||||
|
||||
get peerId() {
|
||||
return this._peerId
|
||||
}
|
||||
|
||||
get peerList() {
|
||||
const list = []
|
||||
for (const [id, p] of this.peers) {
|
||||
list.push({ id, name: p.name, lastSeen: p.lastSeen })
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
async _open() {
|
||||
const keyPair = this.keyPairSeed
|
||||
? require('hyperdht').keyPair(this.keyPairSeed)
|
||||
: require('hyperdht').keyPair()
|
||||
this._peerId = b4a.toString(keyPair.publicKey, 'hex').slice(0, 16)
|
||||
|
||||
this.swarm = new Hyperswarm({ keyPair })
|
||||
this.swarm.on('connection', (conn, info) => this._onConnection(conn, info))
|
||||
|
||||
this.swarm.join(this.topic, { server: true, client: true })
|
||||
await this.swarm.flush()
|
||||
|
||||
this._heartbeat = setInterval(() => this._broadcastPresence(), 5000)
|
||||
if (this._heartbeat.unref) this._heartbeat.unref()
|
||||
this._broadcastPresence()
|
||||
}
|
||||
|
||||
_onConnection(conn, info) {
|
||||
const remoteId = b4a.toString(conn.remotePublicKey, 'hex')
|
||||
const mux = new Protomux(conn)
|
||||
|
||||
const presence = mux.createChannel({
|
||||
protocol: PROTO_PRESENCE,
|
||||
onopen: () => {},
|
||||
onclose: () => {}
|
||||
})
|
||||
const chat = mux.createChannel({
|
||||
protocol: PROTO_CHAT,
|
||||
onopen: () => {},
|
||||
onclose: () => {}
|
||||
})
|
||||
const control = mux.createChannel({
|
||||
protocol: PROTO_CONTROL,
|
||||
onopen: () => {},
|
||||
onclose: () => {}
|
||||
})
|
||||
|
||||
const entry = {
|
||||
name: remoteId.slice(0, 8),
|
||||
lastSeen: Date.now(),
|
||||
mux,
|
||||
_sendPresence: null,
|
||||
_sendChat: null
|
||||
}
|
||||
this.peers.set(remoteId, entry)
|
||||
|
||||
const presenceMsg = presence.addMessage({
|
||||
encoding: presenceEncoding,
|
||||
onmessage: (m) => {
|
||||
const p = this.peers.get(remoteId) || entry
|
||||
p.name = m.name || p.name
|
||||
p.lastSeen = Date.now()
|
||||
this.peers.set(remoteId, p)
|
||||
this.emit('presence', { peerId: remoteId, ...m })
|
||||
this.emit('peers', this.peerList)
|
||||
}
|
||||
})
|
||||
|
||||
const chatMsg = chat.addMessage({
|
||||
encoding: chatEncoding,
|
||||
onmessage: (m) => {
|
||||
this.emit('chat', {
|
||||
from: m.from || remoteId.slice(0, 8),
|
||||
peerId: remoteId,
|
||||
text: String(m.text || ''),
|
||||
ts: m.ts || Date.now()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
control.addMessage({
|
||||
encoding: controlEncoding,
|
||||
onmessage: (m) => {
|
||||
this.emit('control', { peerId: remoteId, ...m })
|
||||
}
|
||||
})
|
||||
|
||||
presence.open()
|
||||
chat.open()
|
||||
control.open()
|
||||
|
||||
entry._sendPresence = (payload) => {
|
||||
try {
|
||||
presenceMsg.send(payload)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
entry._sendChat = (payload) => {
|
||||
try {
|
||||
chatMsg.send(payload)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
// Introduce ourselves
|
||||
entry._sendPresence({
|
||||
peerId: this._peerId,
|
||||
name: this.displayName,
|
||||
ts: Date.now()
|
||||
})
|
||||
|
||||
conn.on('close', () => {
|
||||
this.peers.delete(remoteId)
|
||||
this.emit('peer-leave', { peerId: remoteId })
|
||||
this.emit('peers', this.peerList)
|
||||
})
|
||||
conn.on('error', () => {
|
||||
/* closed via close */
|
||||
})
|
||||
|
||||
this.emit('peer-join', { peerId: remoteId })
|
||||
}
|
||||
|
||||
_broadcastPresence() {
|
||||
const payload = {
|
||||
peerId: this._peerId,
|
||||
name: this.displayName,
|
||||
ts: Date.now()
|
||||
}
|
||||
for (const p of this.peers.values()) {
|
||||
if (p._sendPresence) p._sendPresence(payload)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
*/
|
||||
sendChat(text) {
|
||||
const payload = {
|
||||
from: this.displayName,
|
||||
text: String(text).slice(0, 2000),
|
||||
ts: Date.now()
|
||||
}
|
||||
for (const p of this.peers.values()) {
|
||||
if (p._sendChat) p._sendChat(payload)
|
||||
}
|
||||
// Echo locally so UI/CLI sees own messages consistently
|
||||
this.emit('chat', { ...payload, peerId: this._peerId, local: true })
|
||||
}
|
||||
|
||||
setDisplayName(name) {
|
||||
this.displayName = String(name || 'player').slice(0, 32)
|
||||
this._broadcastPresence()
|
||||
}
|
||||
|
||||
async _close() {
|
||||
if (this._heartbeat) {
|
||||
clearInterval(this._heartbeat)
|
||||
this._heartbeat = null
|
||||
}
|
||||
if (this.swarm) {
|
||||
try {
|
||||
await this.swarm.destroy()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
this.swarm = null
|
||||
}
|
||||
this.peers.clear()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PeerSession,
|
||||
controlTopic,
|
||||
PROTO_PRESENCE,
|
||||
PROTO_CHAT,
|
||||
PROTO_CONTROL
|
||||
}
|
||||
@@ -11,6 +11,12 @@ and this project aims to follow [Semantic Versioning](https://semver.org/).
|
||||
|
||||
### Added
|
||||
|
||||
- Phase 3 peer communication (CLI):
|
||||
- Hyperswarm/Protomux presence + chat for private worlds
|
||||
- Stdin chat on `host`/`join`; `/peers` command
|
||||
- Standalone packaging:
|
||||
- `npm run make:standalone` → single Bare executable (no system Node)
|
||||
- Flying Squid postinstall patches for static plugin load
|
||||
- Phase 2 private world sharing (core):
|
||||
- CLI: `host` (share), `join` (client tunnel)
|
||||
- HyperDHT world tunnel with capability header
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# Flying Jib — Current Status
|
||||
|
||||
**Snapshot date:** 2026-07-30
|
||||
**Phase:** 1 largely done · **Phase 2 core working** (host/join tunnel)
|
||||
**Phase:** 1–3 core working · standalone binary ships Squid without system Node
|
||||
**Product:** Flying Jib — Bare/Pear P2P Minecraft mesh
|
||||
|
||||
---
|
||||
|
||||
## One-line summary
|
||||
|
||||
**Bare CLI hosts Flying Squid on loopback and can share private worlds via HyperDHT + `fj1.` invites.** Guests join with a local tunnel port; Java Edition always uses localhost.
|
||||
**Single Bare executable** runs Flying Squid on loopback, shares worlds via HyperDHT + `fj1.` invites, and side-channel chat/presence over Hyperswarm/Protomux — **no system Node.js**.
|
||||
|
||||
---
|
||||
|
||||
@@ -16,13 +16,12 @@
|
||||
|
||||
| Area | Status |
|
||||
|------|--------|
|
||||
| Bare app CLI (`create` / `list` / `start` / `host` / `join` / `status`) | Working |
|
||||
| Flying Squid on Bare + Node (dev) | Loopback listen verified |
|
||||
| `fj-bind-guard` | OK |
|
||||
| Unit tests (bind-guard, worlds, invite, tunnel) | **10/10 pass** |
|
||||
| HyperDHT world tunnel host↔client | Integration test + CLI smoke |
|
||||
| `fj1.` invite mint/parse | Working |
|
||||
| Graceful shutdown (SIGINT re-entrancy guard) | Improved |
|
||||
| CLI (`create` / `list` / `start` / `host` / `join` / `status`) | Working |
|
||||
| Flying Squid on Bare + standalone binary | Loopback listen verified **without Node on PATH** |
|
||||
| HyperDHT world tunnel + `fj1.` invites | Working |
|
||||
| Peer chat + presence (Protomux) | Working (tests + CLI stdin chat) |
|
||||
| `npm run make:standalone` | Produces `out/flying-jib-<host>/flying-jib` (~500MB) |
|
||||
| Unit/integration tests | **12/12 pass** |
|
||||
|
||||
---
|
||||
|
||||
@@ -30,32 +29,34 @@
|
||||
|
||||
| Area | Status |
|
||||
|------|--------|
|
||||
| `bare-build --standalone` release artifact | In progress / unproven |
|
||||
| Presence / chat Protomux channels | Phase 3 |
|
||||
| Binary size optimization | ~500MB — prune later |
|
||||
| Mesh enrollment / borders | Phase 4 |
|
||||
| GUI | Not started |
|
||||
| Production Pear OTA key | Placeholder |
|
||||
| Full Java multiplayer playtest on two machines | Manual |
|
||||
| Two-machine Java playtest | Manual |
|
||||
|
||||
---
|
||||
|
||||
## How to try (two terminals)
|
||||
## How to try
|
||||
|
||||
```sh
|
||||
# Dev
|
||||
BARE=./node_modules/bare-runtime/bin/bare
|
||||
$BARE bin.mjs --storage /tmp/fj-host create demo
|
||||
$BARE bin.mjs --storage /tmp/fj-host host demo --port 25565
|
||||
# copy fj1.… invite
|
||||
$BARE bin.mjs host demo --port 25565 --name Alice
|
||||
$BARE bin.mjs join 'fj1.…' --port 25566 --name Bob
|
||||
# type chat lines; /peers
|
||||
|
||||
$BARE bin.mjs --storage /tmp/fj-guest join 'fj1.…' --port 25566
|
||||
# Java → 127.0.0.1:25565 (host) or :25566 (guest)
|
||||
# Standalone (no Node)
|
||||
npm run make:standalone
|
||||
./out/flying-jib-darwin-arm64/flying-jib create demo
|
||||
./out/flying-jib-darwin-arm64/flying-jib start demo --port 25565
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next actions
|
||||
|
||||
1. Manual Java Edition multiplayer through tunnel
|
||||
2. Finish bare-build standalone CI path
|
||||
3. Phase 3: presence + chat over Protomux
|
||||
4. Hardening: tunnel connection limits UX, invite rotate
|
||||
1. Two-machine Java Edition playtest
|
||||
2. Shrink standalone binary / CI matrix for all hosts
|
||||
3. Phase 4 mesh registry + border migration
|
||||
|
||||
|
||||
@@ -5,6 +5,29 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-30 — Phase 3 chat/presence + standalone binary
|
||||
|
||||
### Wins
|
||||
|
||||
- `lib/peer-session.js` — Hyperswarm + Protomux (`flying-jib/presence|chat|control`)
|
||||
- CLI host/join: stdin chat, `/peers`, `--name` display name
|
||||
- Peer chat integration test
|
||||
- **`scripts/bare-standalone.cjs`** peardock-style pack; **standalone binary runs Squid on 127.0.0.1 with PATH without Node**
|
||||
- Postinstall patches: static Squid plugins, log.js bare-pack fixes, events polyfill vendor
|
||||
|
||||
### Notes
|
||||
|
||||
- Standalone ~500MB; size optimization later
|
||||
- Dev path `bare-runtime/bin/bare bin.mjs` remains preferred for iteration
|
||||
|
||||
### Next
|
||||
|
||||
- Multi-host make:standalone:all in CI
|
||||
- Phase 4 mesh
|
||||
- Java multiplayer playtest
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-30 — Phase 2 core: HyperDHT tunnel + fj1. invites
|
||||
|
||||
### Wins
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Not Started |
|
||||
| **Status** | In Progress (CLI core done) |
|
||||
| **Owner** | TBD |
|
||||
| **Target** | TBD |
|
||||
| **ADRs** | 0003 (channels) |
|
||||
@@ -133,9 +133,11 @@
|
||||
|
||||
### Acceptance
|
||||
|
||||
- [ ] Live peer list for private world session
|
||||
- [ ] Side-channel chat works when peers are connected
|
||||
- [ ] Reconnect / heartbeat documented and implemented
|
||||
- [x] Live peer list for private world session (`/peers`, presence events)
|
||||
- [x] Side-channel chat works when peers are connected (stdin on host/join)
|
||||
- [x] Heartbeat presence every 5s
|
||||
- [ ] GUI peer list
|
||||
- [ ] Bridge to in-game Squid chat (optional)
|
||||
|
||||
### Documentation required to complete
|
||||
|
||||
|
||||
Generated
+155
-5
@@ -10,6 +10,7 @@
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"autobase": "^7.28.1",
|
||||
"b4a": "^1.6.7",
|
||||
"bare-assert": "^1.1.0",
|
||||
"bare-buffer": "^3.3.1",
|
||||
@@ -36,16 +37,19 @@
|
||||
"bare-url": "^2.1.0",
|
||||
"bare-utils": "^1.0.0",
|
||||
"bare-zlib": "^1.3.0",
|
||||
"compact-encoding": "^3.3.0",
|
||||
"corestore": "^7.9.2",
|
||||
"events": "^3.3.0",
|
||||
"flying-squid": "^1.12.0",
|
||||
"framed-stream": "^1.0.1",
|
||||
"graceful-goodbye": "^1.3.3",
|
||||
"hyperbee": "^2.27.3",
|
||||
"hypercore-crypto": "^3.4.2",
|
||||
"hyperdht": "^6.20.0",
|
||||
"hyperswarm": "^4.17.0",
|
||||
"paparam": "^1.10.1",
|
||||
"pear-runtime": "^1.2.0",
|
||||
"protomux": "^3.11.0",
|
||||
"ready-resource": "^1.2.0",
|
||||
"which-runtime": "^1.4.0",
|
||||
"z32": "^1.1.0"
|
||||
@@ -55,6 +59,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"bare-build": "^1.0.2",
|
||||
"bare-bundle-id": "^1.0.0",
|
||||
"bare-module-traverse": "^1.0.0",
|
||||
"bare-pack": "^2.2.1",
|
||||
"bare-runtime": "1.29.4",
|
||||
"brittle": "^3.19.0",
|
||||
"prettier": "^3.6.2",
|
||||
@@ -248,6 +255,36 @@
|
||||
"integrity": "sha512-6i37w/+EhlWlGUJff3T/Q8u1RGmP5wgbiwYnOnbOqvtrPxT63/sYFyP9RcpxtxGymtfA075IvmOnL7ycNOWl3w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/autobase": {
|
||||
"version": "7.28.1",
|
||||
"resolved": "https://registry.npmjs.org/autobase/-/autobase-7.28.1.tgz",
|
||||
"integrity": "sha512-I8SHcJE/ru5Nun6NpqwRjenB8aK01b9z1qNeyx4XuG+BxeFUpXk0ZT2u3/nG+GZnObUTFMCKOo2fbAv7ipcaLw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"b4a": "^1.6.1",
|
||||
"bare-events": "^2.2.0",
|
||||
"compact-encoding": "^3.0.0",
|
||||
"core-coupler": "^2.0.0",
|
||||
"debounceify": "^1.0.0",
|
||||
"encryption-encoding": "^1.0.3",
|
||||
"hyperbee": "^2.22.0",
|
||||
"hypercore": "^11.27.12",
|
||||
"hypercore-crypto": "^3.4.0",
|
||||
"hypercore-id-encoding": "^1.2.0",
|
||||
"hyperschema": "^1.12.1",
|
||||
"index-encoder": "^3.3.2",
|
||||
"nanoassert": "^2.0.0",
|
||||
"protomux-wakeup": "^2.0.0",
|
||||
"ready-resource": "^1.0.0",
|
||||
"resolve-reject-promise": "^1.1.0",
|
||||
"safety-catch": "^1.0.2",
|
||||
"scope-lock": "^1.2.4",
|
||||
"signal-promise": "^1.0.3",
|
||||
"sodium-universal": "^5.0.1",
|
||||
"sub-encoder": "^2.1.1",
|
||||
"tiny-buffer-map": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/b4a": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
|
||||
@@ -614,6 +651,31 @@
|
||||
"require-asset": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-build/node_modules/bare-module-traverse": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/bare-module-traverse/-/bare-module-traverse-2.4.4.tgz",
|
||||
"integrity": "sha512-zK4bDxqeD15Tvu/C5B1Mi/epl9PavjIhU3SVh5RhDb4Hpet6applPVX8Q9rinu7nT80yzgSSlBn0exzJaNXv0A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-addon-resolve": "^1.5.0",
|
||||
"bare-mime": "^1.0.0",
|
||||
"bare-module-lexer": "^1.6.0",
|
||||
"bare-module-resolve": "^1.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*",
|
||||
"bare-url": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
},
|
||||
"bare-url": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-bundle": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
|
||||
@@ -1108,14 +1170,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bare-module-traverse": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/bare-module-traverse/-/bare-module-traverse-2.4.4.tgz",
|
||||
"integrity": "sha512-zK4bDxqeD15Tvu/C5B1Mi/epl9PavjIhU3SVh5RhDb4Hpet6applPVX8Q9rinu7nT80yzgSSlBn0exzJaNXv0A==",
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/bare-module-traverse/-/bare-module-traverse-1.8.3.tgz",
|
||||
"integrity": "sha512-Iykt1899FOo0lpejEwAUK50fYIzQ18uiJFqybgZvIIqnZSV0/0GhJp+3w3Vx5hTVeTIROF8UmTUB5jHNs65PAg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-addon-resolve": "^1.5.0",
|
||||
"bare-mime": "^1.0.0",
|
||||
"bare-module-lexer": "^1.6.0",
|
||||
"bare-module-lexer": "^1.4.0",
|
||||
"bare-module-resolve": "^1.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -1297,6 +1359,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-pack/node_modules/bare-module-traverse": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/bare-module-traverse/-/bare-module-traverse-2.4.4.tgz",
|
||||
"integrity": "sha512-zK4bDxqeD15Tvu/C5B1Mi/epl9PavjIhU3SVh5RhDb4Hpet6applPVX8Q9rinu7nT80yzgSSlBn0exzJaNXv0A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-addon-resolve": "^1.5.0",
|
||||
"bare-mime": "^1.0.0",
|
||||
"bare-module-lexer": "^1.6.0",
|
||||
"bare-module-resolve": "^1.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*",
|
||||
"bare-url": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
},
|
||||
"bare-url": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-path": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz",
|
||||
@@ -1956,6 +2043,30 @@
|
||||
"bare-url": "^2.4.2"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-thread/node_modules/bare-module-traverse": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/bare-module-traverse/-/bare-module-traverse-2.4.4.tgz",
|
||||
"integrity": "sha512-zK4bDxqeD15Tvu/C5B1Mi/epl9PavjIhU3SVh5RhDb4Hpet6applPVX8Q9rinu7nT80yzgSSlBn0exzJaNXv0A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-addon-resolve": "^1.5.0",
|
||||
"bare-mime": "^1.0.0",
|
||||
"bare-module-lexer": "^1.6.0",
|
||||
"bare-module-resolve": "^1.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*",
|
||||
"bare-url": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
},
|
||||
"bare-url": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-timers": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-timers/-/bare-timers-1.5.2.tgz",
|
||||
@@ -2462,6 +2573,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-coupler": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/core-coupler/-/core-coupler-2.0.0.tgz",
|
||||
"integrity": "sha512-FJuEvsdCMwx0Wu+gFQ49rGCi8LCXh8kizHsCQwkdgPZFEFiF0z2HDvyIs+fPt5wMIfU2UVFDuN+dtpfbIxJE6g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safety-catch": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/corestore": {
|
||||
"version": "7.12.0",
|
||||
"resolved": "https://registry.npmjs.org/corestore/-/corestore-7.12.0.tgz",
|
||||
@@ -2580,6 +2700,15 @@
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encryption-encoding": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/encryption-encoding/-/encryption-encoding-1.0.3.tgz",
|
||||
"integrity": "sha512-+SlKCeULnNnwBF2rGrJlSLYjwITwfjO8PLSHzue4yt+2DsVJUU8GakRkH4+mTaa3FzpxVjpTy1kKR2gYGctNWg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"hyperschema": "^1.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/endian-toggle": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/endian-toggle/-/endian-toggle-0.0.0.tgz",
|
||||
@@ -3804,6 +3933,18 @@
|
||||
"unslab": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/protomux-wakeup": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/protomux-wakeup/-/protomux-wakeup-2.9.0.tgz",
|
||||
"integrity": "sha512-K93WhS9qIRL8WAQPU2V3PxdpQ1oRlQCFyCyIYInoq/iRkcuO1BegRERWo7SS1Tcme14VKGWOr/IivjjYnHan3Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"b4a": "^1.6.7",
|
||||
"hypercore-crypto": "^3.5.0",
|
||||
"hyperschema": "^1.10.4",
|
||||
"protomux": "^3.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -4356,6 +4497,15 @@
|
||||
"integrity": "sha512-IaM+/bh71URhhzlH1qNKNylamK4nxaby2q0qxolWL4Typ3+sIcZHxLe/Mj35b7GOHat/RKpp2vbRepjV6sD+Rw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-buffer-map": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tiny-buffer-map/-/tiny-buffer-map-1.1.1.tgz",
|
||||
"integrity": "sha512-C1eDw6ks9CmkDbWVCPHobuixPTkxGa7IDERlaVk98dv4tOUdz42o3haHBr0uhNxbj0gczBTVIyS2uQsu+1vc2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"b4a": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tmatch": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz",
|
||||
|
||||
+11
-2
@@ -8,13 +8,15 @@
|
||||
"bin": "bin.mjs",
|
||||
"upgrade": "pear://<YOUR_KEY_HERE>",
|
||||
"scripts": {
|
||||
"postinstall": "node scripts/patch-engines-for-bare.js && node scripts/generate-squid-imports.js",
|
||||
"postinstall": "node scripts/patch-engines-for-bare.js && node scripts/patch-flying-squid-plugins.js && node scripts/patch-flying-squid-log.js && node scripts/generate-squid-imports.js",
|
||||
"start": "node_modules/bare-runtime/bin/bare bin.mjs",
|
||||
"start:node": "node bin.mjs",
|
||||
"test": "brittle-node test/*.test.js",
|
||||
"test:bare": "brittle-bare test/*.test.js",
|
||||
"docs:check": "node -e \"const fs=require('fs');const paths=['living_docs/ROADMAP.md','living_docs/CURRENT_STATUS.md','agent/RULES.md','agent/SECURITY.md','developer_docs/ARCHITECTURE.md','user_docs/README.md','lib/squid-manager.js','bin.mjs'];for (const p of paths){if(!fs.existsSync(p)){console.error('missing',p);process.exit(1)}};console.log('docs+app scaffold ok')\"",
|
||||
"make": "node scripts/make.js",
|
||||
"make:standalone": "node scripts/bare-standalone.cjs",
|
||||
"make:standalone:all": "node scripts/bare-standalone.cjs --host all",
|
||||
"make:darwin-arm64": "bare-build --name flying-jib --standalone --host darwin-arm64 --out ./out/darwin-arm64 bin.mjs",
|
||||
"make:darwin-x64": "bare-build --name flying-jib --standalone --host darwin-x64 --out ./out/darwin-x64 bin.mjs",
|
||||
"make:linux-arm64": "bare-build --name flying-jib --standalone --host linux-arm64 --out ./out/linux-arm64 bin.mjs",
|
||||
@@ -142,6 +144,8 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"autobase": "^7.28.1",
|
||||
"b4a": "^1.6.7",
|
||||
"bare-assert": "^1.1.0",
|
||||
"bare-buffer": "^3.3.1",
|
||||
"bare-console": "^6.0.1",
|
||||
@@ -167,23 +171,28 @@
|
||||
"bare-url": "^2.1.0",
|
||||
"bare-utils": "^1.0.0",
|
||||
"bare-zlib": "^1.3.0",
|
||||
"compact-encoding": "^3.3.0",
|
||||
"corestore": "^7.9.2",
|
||||
"events": "^3.3.0",
|
||||
"b4a": "^1.6.7",
|
||||
"flying-squid": "^1.12.0",
|
||||
"framed-stream": "^1.0.1",
|
||||
"graceful-goodbye": "^1.3.3",
|
||||
"hyperbee": "^2.27.3",
|
||||
"hypercore-crypto": "^3.4.2",
|
||||
"hyperdht": "^6.20.0",
|
||||
"hyperswarm": "^4.17.0",
|
||||
"paparam": "^1.10.1",
|
||||
"pear-runtime": "^1.2.0",
|
||||
"protomux": "^3.11.0",
|
||||
"ready-resource": "^1.2.0",
|
||||
"which-runtime": "^1.4.0",
|
||||
"z32": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bare-build": "^1.0.2",
|
||||
"bare-bundle-id": "^1.0.0",
|
||||
"bare-module-traverse": "^1.0.0",
|
||||
"bare-pack": "^2.2.1",
|
||||
"bare-runtime": "1.29.4",
|
||||
"brittle": "^3.19.0",
|
||||
"prettier": "^3.6.2",
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Pack Flying Jib as a Bare standalone executable (peardock pattern).
|
||||
*
|
||||
* bare-build CLI does not accept --imports; this script uses bare-pack with
|
||||
* build/squid-imports.json + package.json imports, then embeds into bare-runtime.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/bare-standalone.cjs
|
||||
* node scripts/bare-standalone.cjs --host darwin-arm64
|
||||
* node scripts/bare-standalone.cjs --host all
|
||||
*/
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const { pathToFileURL } = require('url')
|
||||
const pack = require('bare-pack')
|
||||
const { readModule, listPrefix } = require('bare-pack/fs')
|
||||
const traverse = require('bare-module-traverse')
|
||||
const id = require('bare-bundle-id')
|
||||
|
||||
const root = path.resolve(__dirname, '..')
|
||||
const pkg = require(path.join(root, 'package.json'))
|
||||
|
||||
const ALL_HOSTS = [
|
||||
'darwin-arm64',
|
||||
'darwin-x64',
|
||||
'linux-arm64',
|
||||
'linux-x64',
|
||||
'win32-arm64',
|
||||
'win32-x64'
|
||||
]
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = { hosts: [], outRoot: path.join(root, 'out') }
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i]
|
||||
if (a === '--host') {
|
||||
const h = argv[++i]
|
||||
if (h === 'all') out.hosts.push(...ALL_HOSTS)
|
||||
else out.hosts.push(h)
|
||||
} else if (a === '--out') out.outRoot = path.resolve(argv[++i])
|
||||
else if (a === '--help' || a === '-h') out.help = true
|
||||
}
|
||||
if (!out.hosts.length) out.hosts.push(`${process.platform}-${process.arch}`)
|
||||
return out
|
||||
}
|
||||
|
||||
function fileURL(rel) {
|
||||
return pathToFileURL(path.join(root, rel)).href
|
||||
}
|
||||
|
||||
function buildImportsMap() {
|
||||
// Prefer generated squid-imports (bnr + shims)
|
||||
const squidImportsPath = path.join(root, 'build', 'squid-imports.json')
|
||||
let map = {}
|
||||
if (fs.existsSync(squidImportsPath)) {
|
||||
map = JSON.parse(fs.readFileSync(squidImportsPath, 'utf8'))
|
||||
} else {
|
||||
try {
|
||||
map = { ...require('bare-node-runtime/imports') }
|
||||
} catch {
|
||||
map = {}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge package.json imports (relative bare paths need absolutizing)
|
||||
const pkgImports = pkg.imports || {}
|
||||
for (const [spec, target] of Object.entries(pkgImports)) {
|
||||
if (typeof target === 'string') {
|
||||
map[spec] = target
|
||||
continue
|
||||
}
|
||||
if (target && typeof target === 'object') {
|
||||
map[spec] = { ...target }
|
||||
}
|
||||
}
|
||||
|
||||
for (const [spec, target] of Object.entries(map)) {
|
||||
if (!target || typeof target !== 'object') continue
|
||||
if (typeof target.bare === 'string' && target.bare.startsWith('./')) {
|
||||
map[spec] = {
|
||||
...target,
|
||||
bare: fileURL(target.bare.replace(/^\.\//, ''))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit stubs for optional / browser-only / missing optional deps
|
||||
const stubs = {
|
||||
'esbuild-import-glob(path:.,skipFiles:index.js,external.js)':
|
||||
'build/stubs/esbuild-import-glob.cjs',
|
||||
'supports-color': 'build/stubs/supports-color.cjs',
|
||||
'cpu-features': 'build/stubs/supports-color.cjs',
|
||||
'encoding': 'build/stubs/auto-encoding.cjs',
|
||||
'bufferutil': 'build/stubs/auto-bufferutil.cjs',
|
||||
'utf-8-validate': 'build/stubs/auto-utf-8-validate.cjs',
|
||||
'canvas': 'build/stubs/auto-canvas.cjs',
|
||||
'sqlite3': 'build/stubs/auto-sqlite3.cjs',
|
||||
'better-sqlite3': 'build/stubs/auto-better-sqlite3.cjs'
|
||||
}
|
||||
for (const [spec, rel] of Object.entries(stubs)) {
|
||||
map[spec] = { bare: fileURL(rel), default: fileURL(rel) }
|
||||
}
|
||||
|
||||
return map
|
||||
}
|
||||
|
||||
function platformForHost(host) {
|
||||
const bareBuildRoot = path.dirname(require.resolve('bare-build/package'))
|
||||
const load = (name) => require(path.join(bareBuildRoot, 'lib', 'platform', name))
|
||||
if (host.startsWith('darwin') || host.startsWith('ios')) return load('apple')
|
||||
if (host.startsWith('linux')) return load('linux')
|
||||
if (host.startsWith('win32')) return load('windows')
|
||||
if (host.startsWith('android')) return load('android')
|
||||
throw new Error(`Unknown host '${host}'`)
|
||||
}
|
||||
|
||||
function walkFind(dir, pred) {
|
||||
const stack = [dir]
|
||||
while (stack.length) {
|
||||
const d = stack.pop()
|
||||
let entries
|
||||
try {
|
||||
entries = fs.readdirSync(d, { withFileTypes: true })
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
for (const ent of entries) {
|
||||
const p = path.join(d, ent.name)
|
||||
if (ent.isDirectory()) stack.push(p)
|
||||
else if (pred(p)) return p
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async function buildOne(host, outRoot) {
|
||||
const name = pkg.productName ? 'flying-jib' : pkg.name
|
||||
const outDir = path.join(outRoot, `${name}-${host}`)
|
||||
fs.rmSync(outDir, { recursive: true, force: true })
|
||||
fs.mkdirSync(outDir, { recursive: true })
|
||||
|
||||
// Ensure postinstall artifacts
|
||||
require('child_process').execSync('node scripts/generate-squid-imports.js', {
|
||||
cwd: root,
|
||||
stdio: 'inherit'
|
||||
})
|
||||
require('child_process').execSync('node scripts/patch-engines-for-bare.js', {
|
||||
cwd: root,
|
||||
stdio: 'inherit'
|
||||
})
|
||||
|
||||
const entryPath = path.join(root, 'bin.mjs')
|
||||
const imports = buildImportsMap()
|
||||
console.log(`[bare-standalone] packing ${name} for ${host}…`)
|
||||
|
||||
let entry = await pack(
|
||||
pathToFileURL(entryPath),
|
||||
{
|
||||
hosts: [host],
|
||||
linked: false,
|
||||
resolve: traverse.resolve.bare,
|
||||
imports
|
||||
},
|
||||
readModule,
|
||||
listPrefix
|
||||
)
|
||||
|
||||
const baseURL = pathToFileURL(root + path.sep)
|
||||
entry = entry.unmount(baseURL)
|
||||
entry.id = id(entry).toString('hex')
|
||||
|
||||
const platform = platformForHost(host)
|
||||
const opts = {
|
||||
name,
|
||||
version: pkg.version || '0.0.0',
|
||||
description: pkg.description || 'Flying Jib',
|
||||
author: pkg.author || '',
|
||||
identifier: 'dev.flyingjib.app',
|
||||
hosts: [host],
|
||||
out: outDir,
|
||||
standalone: true,
|
||||
package: false,
|
||||
base: root
|
||||
}
|
||||
|
||||
console.log(`[bare-standalone] embedding bare-runtime for ${host}…`)
|
||||
for await (const resource of platform(root, entry, null, opts)) {
|
||||
if (resource && resource.path) {
|
||||
console.log(`[bare-standalone] resource ${resource.path}`)
|
||||
}
|
||||
}
|
||||
|
||||
const binName = host.startsWith('win32') ? `${name}.exe` : name
|
||||
let binary = path.join(outDir, binName)
|
||||
if (!fs.existsSync(binary)) {
|
||||
const found = walkFind(outDir, (f) => {
|
||||
const base = path.basename(f)
|
||||
return base === name || base === `${name}.exe` || base === 'flying-jib'
|
||||
})
|
||||
if (found) binary = found
|
||||
}
|
||||
|
||||
if (fs.existsSync(binary)) {
|
||||
try {
|
||||
fs.chmodSync(binary, 0o755)
|
||||
} catch {
|
||||
/* win */
|
||||
}
|
||||
const flat = path.join(outDir, path.basename(binary))
|
||||
if (path.resolve(binary) !== path.resolve(flat)) {
|
||||
fs.copyFileSync(binary, flat)
|
||||
binary = flat
|
||||
}
|
||||
console.log(`[bare-standalone] wrote ${binary}`)
|
||||
} else {
|
||||
console.warn(`[bare-standalone] WARN: binary not found under ${outDir}`)
|
||||
try {
|
||||
console.warn(
|
||||
' contents:',
|
||||
fs.readdirSync(outDir, { recursive: true }).slice(0, 40).join(', ')
|
||||
)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(outDir, 'build-info.json'),
|
||||
JSON.stringify(
|
||||
{
|
||||
host,
|
||||
name,
|
||||
version: pkg.version,
|
||||
builtAt: new Date().toISOString(),
|
||||
entry: 'bin.mjs',
|
||||
bundleId: entry.id
|
||||
},
|
||||
null,
|
||||
2
|
||||
) + '\n'
|
||||
)
|
||||
|
||||
return outDir
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const opts = parseArgs(process.argv.slice(2))
|
||||
if (opts.help) {
|
||||
console.log(`Usage: node scripts/bare-standalone.cjs [--host <host>|all] [--out dir]
|
||||
Hosts: ${ALL_HOSTS.join(', ')}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const results = []
|
||||
for (const host of opts.hosts) {
|
||||
results.push(await buildOne(host, opts.outRoot))
|
||||
}
|
||||
console.log('[bare-standalone] done:', results.join(', '))
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -12,6 +12,15 @@ const { pathToFileURL } = require('url')
|
||||
|
||||
const root = path.resolve(__dirname, '..')
|
||||
const bnr = require('bare-node-runtime/imports')
|
||||
|
||||
// Keep events polyfill vendored for bare-pack (no nested node_modules require)
|
||||
const eventsSrc = path.join(root, 'node_modules', 'events', 'events.js')
|
||||
const eventsPoly = path.join(root, 'build', 'shims', 'events-polyfill.cjs')
|
||||
if (fs.existsSync(eventsSrc)) {
|
||||
fs.mkdirSync(path.dirname(eventsPoly), { recursive: true })
|
||||
fs.copyFileSync(eventsSrc, eventsPoly)
|
||||
}
|
||||
|
||||
const eventsShim = pathToFileURL(path.join(root, 'build/shims/node-events.cjs')).href
|
||||
const readlineShim = pathToFileURL(path.join(root, 'build/shims/node-readline.cjs')).href
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Neutralize flying-squid log plugin dynamic import('exit-hook') which bare-pack
|
||||
* does not follow, and soft-fail readline TTY helpers on Bare.
|
||||
*/
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const logPath = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'node_modules',
|
||||
'flying-squid',
|
||||
'src',
|
||||
'lib',
|
||||
'plugins',
|
||||
'log.js'
|
||||
)
|
||||
|
||||
if (!fs.existsSync(logPath)) {
|
||||
console.warn('[patch-flying-squid-log] skip (not installed)')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
let src = fs.readFileSync(logPath, 'utf8')
|
||||
if (src.includes('FLYING_JIB_PATCH: exit-hook removed')) {
|
||||
console.log('[patch-flying-squid-log] already patched')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Replace whole isInNode block's exit-hook import
|
||||
src = src.replace(
|
||||
/if \(isInNode\) \{[\s\S]*?readline = require\('readline'\)/,
|
||||
`if (isInNode) {
|
||||
/* FLYING_JIB_PATCH: exit-hook removed for bare-pack */
|
||||
readline = require('readline')`
|
||||
)
|
||||
|
||||
// Soften createInterface / prompt
|
||||
if (!src.includes('FLYING_JIB_PATCH: readline soft')) {
|
||||
src = src.replace(
|
||||
/rl = readline\.createInterface\(\{[\s\S]*?\}\)\s*\n\s*rl\.setPrompt\('> '\)\s*\n\s*rl\.prompt\(true\)/,
|
||||
`try { /* FLYING_JIB_PATCH: readline soft */
|
||||
rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
})
|
||||
if (rl && typeof rl.setPrompt === 'function') {
|
||||
rl.setPrompt('> ')
|
||||
rl.prompt(true)
|
||||
}
|
||||
} catch {
|
||||
rl = undefined
|
||||
}`
|
||||
)
|
||||
}
|
||||
|
||||
src = src.replace(
|
||||
'readline?.cursorTo(process.stdout, 0)',
|
||||
'(readline && typeof readline.cursorTo === "function" ? readline.cursorTo(process.stdout, 0) : undefined)'
|
||||
)
|
||||
|
||||
fs.writeFileSync(logPath, src)
|
||||
console.log('[patch-flying-squid-log] patched')
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Replace flying-squid dynamic requireindex plugin loading with static requires
|
||||
* so bare-pack can include the full plugin graph in standalone binaries.
|
||||
*/
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const root = path.resolve(__dirname, '..')
|
||||
const pluginsDir = path.join(root, 'node_modules', 'flying-squid', 'src', 'lib', 'plugins')
|
||||
const indexPath = path.join(pluginsDir, 'index.js')
|
||||
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
console.warn('[patch-flying-squid-plugins] flying-squid not installed; skip')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const files = fs
|
||||
.readdirSync(pluginsDir)
|
||||
.filter((f) => f.endsWith('.js') && f !== 'index.js')
|
||||
.sort()
|
||||
|
||||
const requires = files
|
||||
.map((f) => {
|
||||
const key = './' + f
|
||||
return ` require(${JSON.stringify(key)})`
|
||||
})
|
||||
.join(',\n')
|
||||
|
||||
const out = `module.exports.builtinPlugins = null
|
||||
|
||||
// Flying Jib postinstall: static plugin list for bare-pack (no readdir at runtime).
|
||||
module.exports.initPlugins = () => {
|
||||
if (module.exports.builtinPlugins) return
|
||||
module.exports.builtinPlugins = [
|
||||
${requires}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
fs.writeFileSync(indexPath, out)
|
||||
console.log(`[patch-flying-squid-plugins] wrote static list (${files.length} plugins)`)
|
||||
@@ -0,0 +1,53 @@
|
||||
'use strict'
|
||||
|
||||
const test = require('brittle')
|
||||
const crypto = require('hypercore-crypto')
|
||||
const { PeerSession, controlTopic } = require('../lib/peer-session')
|
||||
|
||||
test('controlTopic is stable 32 bytes', (t) => {
|
||||
const k = crypto.randomBytes(32)
|
||||
const a = controlTopic(k)
|
||||
const b = controlTopic(k)
|
||||
t.is(a.byteLength, 32)
|
||||
t.alike(a, b)
|
||||
})
|
||||
|
||||
test('two PeerSessions exchange chat', { timeout: 45000 }, async (t) => {
|
||||
const worldKey = crypto.randomBytes(32)
|
||||
|
||||
const a = new PeerSession({
|
||||
worldPublicKey: worldKey,
|
||||
displayName: 'Alice',
|
||||
keyPairSeed: crypto.randomBytes(32)
|
||||
})
|
||||
const b = new PeerSession({
|
||||
worldPublicKey: worldKey,
|
||||
displayName: 'Bob',
|
||||
keyPairSeed: crypto.randomBytes(32)
|
||||
})
|
||||
|
||||
await a.ready()
|
||||
await b.ready()
|
||||
|
||||
const got = new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error('chat timeout')), 30000)
|
||||
b.on('chat', (msg) => {
|
||||
if (msg.local) return
|
||||
if (msg.text === 'hello-from-alice') {
|
||||
clearTimeout(timer)
|
||||
resolve(msg)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Wait for swarm to link
|
||||
await new Promise((r) => setTimeout(r, 3000))
|
||||
a.sendChat('hello-from-alice')
|
||||
|
||||
const msg = await got
|
||||
t.is(msg.from, 'Alice')
|
||||
t.is(msg.text, 'hello-from-alice')
|
||||
|
||||
await a.close()
|
||||
await b.close()
|
||||
})
|
||||
+5
-1
@@ -30,7 +30,11 @@ Not in the current design. Flying Squid targets **Java Edition** protocol.
|
||||
|
||||
## Is chat end-to-end encrypted?
|
||||
|
||||
Peer links use Noise encryption (Hyperswarm/HyperDHT). Side-channel chat rides those links. In-game Minecraft chat is whatever Squid implements on the host world.
|
||||
Peer links use Noise encryption (Hyperswarm/HyperDHT). Side-channel chat (CLI: type while hosting/joined) rides a separate Hyperswarm topic for that world. In-game Minecraft chat is local to the host Squid instance.
|
||||
|
||||
## Do I need Node.js for the released app?
|
||||
|
||||
**No.** Use the standalone binary from `make:standalone`, or install via Pear when published.
|
||||
|
||||
## Will this work on school/work Wi‑Fi?
|
||||
|
||||
|
||||
Reference in New Issue
Block a user