Modernize
CI / Build & Test (push) Successful in 3m5s

This commit is contained in:
Raven Scott
2026-07-26 21:58:45 -04:00
parent 0d49580650
commit 914bc7e404
24 changed files with 4067 additions and 966 deletions
+197
View File
@@ -0,0 +1,197 @@
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
ci:
name: Build & Test
runs-on: ssh
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_TOKEN }}
- name: Check toolchain versions
run: |
node --version
npm --version
bare --version 2>/dev/null || echo "bare not in PATH (ok)"
- name: Install root dependencies
run: npm install
- name: Install native-host dependencies
run: npm install
working-directory: native-host
- name: Lint — syntax check extension
run: |
node --check extension/background.js
node --check extension/content.js
node --check extension/api.js
node --check extension/framed-stream.js
node --check extension/options.js
node --check extension/dashboard.js
node --check extension/defaults.js
- name: Lint — syntax check native host
run: |
node --check native-host/host.js
node --check native-host/messenger.js
node --check native-host/hyperdb-minimal-definition.js
- name: Lint — syntax check build scripts
run: |
node --check scripts/build-distributable.js
node --check scripts/pack-extension.js
node --check scripts/build-host.js
node --check scripts/build-hrpc.js
node --check scripts/bundle-protomux.js
- name: Build (host launcher, protomux, hrpc)
run: npm run build
- name: Pack extension
run: npm run pack
- name: Build all platform binaries
run: node scripts/build-distributable.js --all --package
- name: Smoke test — run native binary for current platform
run: |
echo "=== releases/ layout ==="
find releases/ -type f | sort
echo ""
MACHINE=$(uname -m)
echo "uname -m: $MACHINE"
BIN=""
for candidate in \
"releases/${MACHINE}/bridge-swarm-host" \
"releases/x86_64/bridge-swarm-host" \
"releases/aarch64/bridge-swarm-host" \
"releases/arm64/bridge-swarm-host" \
"releases/bridge-swarm-host"; do
if [ -f "$candidate" ]; then
BIN="$candidate"
break
fi
done
if [ -z "$BIN" ]; then
echo "No runnable binary found — skipping smoke test"
exit 0
fi
echo "Testing binary: $BIN"
chmod +x "$BIN"
OUTPUT=$(timeout 6 "$BIN" </dev/null 2>&1 || true)
echo "$OUTPUT"
if echo "$OUTPUT" | grep -q "\[bridge-swarm-host\] ready"; then
echo "PASS: binary started successfully"
else
echo "FAIL: binary did not print ready signal"
exit 1
fi
echo "PASS: smoke test complete"
- name: Generate checksums
run: |
cd releases
find . -name "*.zip" -o -name "*.xpi" | sort | xargs sha256sum > SHA256SUMS.txt
cat SHA256SUMS.txt
- name: List artifacts
run: |
echo "=== All release artifacts ==="
find releases/ -type f | sort
echo ""
find releases/ -type f -exec ls -lh {} \; | awk '{print $5, $9}'
- name: Publish rolling release
if: github.event_name == 'push'
run: |
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
TAG="latest-main"
TITLE="Latest build (main @ ${SHORT_SHA})"
COMMIT_MSG=$(echo "${{ github.event.head_commit.message }}" | sed '/^Made-with:/d' | sed '/^$/d' | head -1)
BODY="Automated build from main branch.\n\n**Commit:** ${{ github.sha }}\n**Message:** ${COMMIT_MSG}\n\nThis release is updated on every push to main and always contains the latest artifacts."
API="${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
AUTH="Authorization: token ${{ secrets.RELEASE_TOKEN }}"
REPO_URL=$(git remote get-url origin | sed 's|https://|https://x-token:${{ secrets.RELEASE_TOKEN }}@|')
git remote set-url origin "${REPO_URL}"
git config user.email "ci@bridgeswarm"
git config user.name "CI"
git tag -f "${TAG}" "${{ github.sha }}"
git push origin "refs/tags/${TAG}" --force
echo "Tag ${TAG} force-pushed to ${{ github.sha }}"
EXISTING=$(curl -s -H "$AUTH" "${API}/releases/tags/${TAG}")
RELEASE_ID=$(echo "$EXISTING" | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{const r=JSON.parse(d);console.log(r.id||'')}catch{console.log('')}})")
echo "Existing release ID: $RELEASE_ID"
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "null" ] && [ "$RELEASE_ID" != "" ]; then
curl -s -X PATCH \
-H "$AUTH" -H "Content-Type: application/json" \
"${API}/releases/${RELEASE_ID}" \
-d "{
\"name\": \"${TITLE}\",
\"body\": \"${BODY}\",
\"prerelease\": true,
\"target_commitish\": \"${{ github.sha }}\"
}"
echo "Updated release ${RELEASE_ID}"
ASSETS=$(curl -s -H "$AUTH" "${API}/releases/${RELEASE_ID}/assets")
echo "$ASSETS" | node -e "
let d='';
process.stdin.on('data',c=>d+=c).on('end',()=>{
try {
const assets = JSON.parse(d);
if (Array.isArray(assets)) assets.forEach(a => console.log(a.id));
} catch(_) {}
})" | while read ASSET_ID; do
[ -z "$ASSET_ID" ] && continue
echo "Deleting asset $ASSET_ID..."
curl -s -X DELETE -H "$AUTH" "${API}/releases/${RELEASE_ID}/assets/${ASSET_ID}"
done
else
RELEASE_ID=$(curl -s -X POST \
-H "$AUTH" -H "Content-Type: application/json" \
"${API}/releases" \
-d "{
\"tag_name\": \"${TAG}\",
\"name\": \"${TITLE}\",
\"body\": \"${BODY}\",
\"prerelease\": true,
\"target_commitish\": \"${{ github.sha }}\"
}" | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{console.log(JSON.parse(d).id)}catch{console.log('')}})")
echo "Created release ${RELEASE_ID}"
fi
for FILE in releases/*.zip releases/*.xpi releases/SHA256SUMS.txt; do
[ -f "$FILE" ] || continue
NAME=$(basename "$FILE")
echo "Uploading $NAME..."
curl -s -X POST \
-H "$AUTH" \
-H "Content-Type: application/octet-stream" \
"${API}/releases/${RELEASE_ID}/assets?name=${NAME}" \
--data-binary "@${FILE}"
echo ""
done
echo "Done — release ${TAG} updated to ${{ github.sha }}"
+98
View File
@@ -0,0 +1,98 @@
name: Release
on:
push:
tags:
- 'v*.*.*'
jobs:
release:
name: Build & Publish Release
runs-on: ssh
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check toolchain versions
run: node --version && npm --version
- name: Install root dependencies
run: npm install
- name: Install native-host dependencies
run: npm install
working-directory: native-host
- name: Build (host launcher, protomux, hrpc)
run: npm run build
- name: Build all platform binaries
run: node scripts/build-distributable.js --all --package
- name: Package extension (zip + xpi)
run: npm run pack
- name: Smoke test — run binary for current platform
run: |
echo "=== releases/ layout ==="
find releases/ -type f | sort
echo ""
MACHINE=$(uname -m)
echo "uname -m: $MACHINE"
BIN=""
for candidate in \
"releases/${MACHINE}/bridge-swarm-host" \
"releases/x86_64/bridge-swarm-host" \
"releases/aarch64/bridge-swarm-host" \
"releases/arm64/bridge-swarm-host" \
"releases/bridge-swarm-host"; do
if [ -f "$candidate" ]; then
BIN="$candidate"
break
fi
done
if [ -z "$BIN" ]; then
echo "No runnable binary found — skipping smoke test"
exit 0
fi
echo "Testing binary: $BIN"
chmod +x "$BIN"
OUTPUT=$(timeout 6 "$BIN" </dev/null 2>&1 || true)
echo "$OUTPUT"
if echo "$OUTPUT" | grep -q "\[bridge-swarm-host\] ready"; then
echo "PASS: binary started successfully"
else
echo "FAIL: binary did not print ready signal"
exit 1
fi
- name: List release artifacts
run: |
echo "=== releases/ ==="
find releases/ -type f | sort
echo ""
find releases/ -type f -exec ls -lh {} \; | awk '{print $5, $9}'
- name: Generate checksums
run: |
cd releases
find . -name "*.zip" -o -name "*.xpi" | sort | xargs sha256sum > SHA256SUMS.txt
cat SHA256SUMS.txt
- name: Publish release
uses: gitea.com/actions/gitea-release-action@latest
with:
token: ${{ secrets.RELEASE_TOKEN }}
tag_name: ${{ gitea.ref_name }}
name: BridgeSwarm ${{ gitea.ref_name }}
files: |-
releases/*.zip
releases/*.xpi
releases/SHA256SUMS.txt
sha256sum: false
+6
View File
@@ -1,6 +1,12 @@
# Dependencies
node_modules/
# Release artifacts (built by CI / npm run build:dist:package)
releases/
# Mirrored HRPC/hyperschema output for bare-pack (generated by npm run build:hrpc)
native-host/spec/
# Native host: generated launchers (paths are machine-specific)
native-host/bridge-swarm-host
native-host/bridge-swarm-host.bat
+44 -31
View File
@@ -40,44 +40,48 @@ P2P: [Browser] ◄──────► [Browser]
## Quick Start
### Installation
### Easy install (recommended)
Downloads prebuilt native-host binaries and the extension from the rolling Gitea release [`latest-main`](https://git.ssh.surf/snxraven/BridgeSwarm/releases/tag/latest-main). No Node.js or git clone required.
**macOS / Linux:**
```bash
# Clone and install
git clone https://github.com/anomalyco/BridgeSwarm.git
cd BridgeSwarm
# Run the installer (macOS/Linux)
./scripts/install.sh
# Or Windows
.\scripts\install.ps1
curl -fsSL https://git.ssh.surf/snxraven/BridgeSwarm/raw/branch/main/scripts/web-installer.sh | bash
```
### Quick Install (No Clone)
Run directly from the web — downloads BridgeSwarm, builds, and packages the extension:
```bash
curl -fsSL https://ssh.surf/bridgeswarm/install.sh -o install.sh && bash install.sh
**Windows (PowerShell):**
```powershell
irm https://git.ssh.surf/snxraven/BridgeSwarm/raw/branch/main/scripts/install.ps1 | iex
```
Or from GitLab:
```bash
curl -fsSL https://git.ssh.surf/snxraven/BridgeSwarm/src/branch/main/scripts/web-installer.sh -o install.sh && bash install.sh
```
This installs the native host to `~/.bridgeswarm/`, saves the extension package to `~/Downloads/`, and opens the browser for you to load it.
This installs the host to `~/.bridgeswarm/` (or `%LOCALAPPDATA%\bridgeswarm\` on Windows) and saves `BridgeSwarm-*.zip` / `.xpi` to `~/Downloads`.
### Load the Extension
1. Open Chrome/Edge and go to `chrome://extensions`
**Chrome / Edge:**
1. Open `chrome://extensions`
2. Enable **Developer mode**
3. Click **Load unpacked**
4. Select the `extension/` folder
3. Drag & drop `~/Downloads/BridgeSwarm-1.0.0.zip` onto the page (or Load unpacked after extracting)
4. Extension ID should be `fmcenppcipeikpnpopolicnllljclmmi`
5. Restart the browser
**Firefox:** temporary via `about:debugging` → Load Temporary Add-on, or permanent on Nightly/Dev Edition via Install Add-on From File (`.xpi`).
### Develop from source
```bash
git clone https://git.ssh.surf/snxraven/BridgeSwarm.git
cd BridgeSwarm
npm run setup # or ./scripts/install-from-source.sh
# Load unpacked: extension/
```
Build release artifacts locally:
```bash
npm run pack # extension zip + xpi
npm run build:dist:package # all-platform host zips (needs bare-build; best on CI)
```
### Your First P2P App
```javascript
@@ -291,7 +295,7 @@ For comprehensive documentation, see:
## Troubleshooting
### "Native host has exited"
The wrapper needs full paths to `node` and `bare`. Re-run `./scripts/install.sh`.
The launcher runs Bare via Node (`node …/node_modules/bare/bin/bare …/index.mjs`). Re-run `./scripts/install.sh` (or `npm install` in `native-host/` and `npm run build:host`) so the local `bare` dependency and launcher paths are correct.
### "Access to the specified native messaging host is forbidden"
Extension ID mismatch. Run `./scripts/update-native-manifest-extension-id.sh YOUR_EXTENSION_ID`.
@@ -310,11 +314,19 @@ Run `npm run build:hrpc` to generate the HRPC spec.
```bash
npm run build:protomux # Rebuild Protomux bundle
npm run build:hrpc # Rebuild HRPC spec
npm run build # Both
npm run pack # Package extension
npm run build:hrpc # Rebuild HRPC spec (+ mirror into native-host/spec)
npm run build # host launcher + protomux + hrpc
npm run pack # Package extension → releases/BridgeSwarm-*.zip|.xpi
npm run build:dist # Standalone host binary (current platform)
npm run build:dist:package # All platforms + zip archives (CI)
```
### Releases (Gitea)
- Push to `main` → CI builds artifacts and updates rolling prerelease tag `latest-main`
- Push tag `v*.*.*` → versioned Gitea release
- Requires repo secret `RELEASE_TOKEN` and self-hosted runner label `ssh` (same as holesail-browser)
### File Layout
```
@@ -350,7 +362,8 @@ BridgeSwarm/
- Chrome 88+, Edge, Firefox 79+
- Desktop only (native messaging not available on mobile)
- Node.js required for native host
- **Release install:** prebuilt Bare host binaries (darwin/linux/win32) — no Node required for end users
- **From-source / CI:** Node.js for tooling; Bare `>=1.29.4` via the `bare` npm dependency
## License
+214 -24
View File
@@ -1,7 +1,11 @@
var BridgeSwarmProtomux = (() => {
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => function __require() {
try {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
} catch (e) {
throw mod = 0, e;
}
};
// node_modules/b4a/lib/ascii.js
@@ -14,7 +18,7 @@ var BridgeSwarmProtomux = (() => {
const len = buffer.byteLength;
let result = "";
for (let i = 0; i < len; i++) {
result += String.fromCharCode(buffer[i]);
result += String.fromCharCode(buffer[i] & 127);
}
return result;
}
@@ -114,7 +118,7 @@ var BridgeSwarmProtomux = (() => {
const a = hexValue(string.charCodeAt(i * 2));
const b = hexValue(string.charCodeAt(i * 2 + 1));
if (a === void 0 || b === void 0) {
return buffer.subarray(0, i);
return i;
}
buffer[i] = a << 4 | b;
}
@@ -133,6 +137,35 @@ var BridgeSwarmProtomux = (() => {
}
});
// node_modules/b4a/lib/latin1.js
var require_latin1 = __commonJS({
"node_modules/b4a/lib/latin1.js"(exports, module) {
function byteLength(string) {
return string.length;
}
function toString(buffer) {
const len = buffer.byteLength;
let result = "";
for (let i = 0; i < len; i++) {
result += String.fromCharCode(buffer[i]);
}
return result;
}
function write(buffer, string) {
const len = buffer.byteLength;
for (let i = 0; i < len; i++) {
buffer[i] = string.charCodeAt(i);
}
return len;
}
module.exports = {
byteLength,
toString,
write
};
}
});
// node_modules/b4a/lib/utf8.js
var require_utf8 = __commonJS({
"node_modules/b4a/lib/utf8.js"(exports, module) {
@@ -215,6 +248,7 @@ var BridgeSwarmProtomux = (() => {
while (i < string.length) {
const code = string.codePointAt(i);
if (code <= 127) {
if (j + 1 > len) break;
buffer[j++] = code;
i++;
continue;
@@ -231,6 +265,7 @@ var BridgeSwarmProtomux = (() => {
count = 18;
bits = 240;
}
if (j + count / 6 + 1 > len) break;
buffer[j++] = bits | code >> count;
count -= 6;
while (count >= 0) {
@@ -239,7 +274,7 @@ var BridgeSwarmProtomux = (() => {
}
i += code >= 65536 ? 2 : 1;
}
return len;
return j;
};
}
module.exports = {
@@ -291,6 +326,7 @@ var BridgeSwarmProtomux = (() => {
var ascii = require_ascii();
var base64 = require_base64();
var hex = require_hex();
var latin1 = require_latin1();
var utf8 = require_utf8();
var utf16le = require_utf16le();
var LE = new Uint8Array(Uint16Array.of(255).buffer)[0] === 255;
@@ -302,6 +338,9 @@ var BridgeSwarmProtomux = (() => {
return base64;
case "hex":
return hex;
case "binary":
case "latin1":
return latin1;
case "utf8":
case "utf-8":
case void 0:
@@ -543,15 +582,17 @@ var BridgeSwarmProtomux = (() => {
}
function swap16(buffer) {
const len = buffer.byteLength;
if (len % 2 !== 0)
if (len % 2 !== 0) {
throw new RangeError("Buffer size must be a multiple of 16-bits");
}
for (let i = 0; i < len; i += 2) swap(buffer, i, i + 1);
return buffer;
}
function swap32(buffer) {
const len = buffer.byteLength;
if (len % 4 !== 0)
if (len % 4 !== 0) {
throw new RangeError("Buffer size must be a multiple of 32-bits");
}
for (let i = 0; i < len; i += 4) {
swap(buffer, i, i + 3);
swap(buffer, i + 1, i + 2);
@@ -560,8 +601,9 @@ var BridgeSwarmProtomux = (() => {
}
function swap64(buffer) {
const len = buffer.byteLength;
if (len % 8 !== 0)
if (len % 8 !== 0) {
throw new RangeError("Buffer size must be a multiple of 64-bits");
}
for (let i = 0; i < len; i += 8) {
swap(buffer, i, i + 7);
swap(buffer, i + 1, i + 6);
@@ -585,7 +627,7 @@ var BridgeSwarmProtomux = (() => {
}
return codecFor(encoding).toString(buffer);
}
function write(buffer, string, offset, length, encoding) {
function write(buffer, string, offset = 0, length = buffer.byteLength, encoding) {
if (arguments.length === 2) return utf8.write(buffer, string);
if (typeof offset === "string") {
encoding = offset;
@@ -750,16 +792,13 @@ var BridgeSwarmProtomux = (() => {
};
var buffer = exports.buffer = {
preencode(state, b) {
if (b) uint8array.preencode(state, b);
else state.end++;
uint8array.preencode(state, b);
},
encode(state, b) {
if (b) uint8array.encode(state, b);
else state.buffer[state.start++] = 0;
uint8array.encode(state, b);
},
decode(state) {
const b = state.buffer.subarray(state.start);
if (b.byteLength === 0) return null;
state.start = state.end;
return b;
}
@@ -1065,6 +1104,22 @@ var BridgeSwarmProtomux = (() => {
return state.buffer[state.start++] + state.buffer[state.start++] * 256 + state.buffer[state.start++] * 65536 + state.buffer[state.start++] * 16777216;
}
};
var uint32be = exports.uint32be = {
preencode(state, n) {
state.end += 4;
},
encode(state, n) {
validateUint(n);
state.buffer[state.start++] = n >>> 24;
state.buffer[state.start++] = n >>> 16;
state.buffer[state.start++] = n >>> 8;
state.buffer[state.start++] = n;
},
decode(state) {
if (state.end - state.start < 4) throw new Error("Out of bounds");
return state.buffer[state.start++] * 16777216 + state.buffer[state.start++] * 65536 + state.buffer[state.start++] * 256 + state.buffer[state.start++];
}
};
var uint40 = exports.uint40 = {
preencode(state, n) {
state.end += 5;
@@ -1107,7 +1162,9 @@ var BridgeSwarmProtomux = (() => {
},
decode(state) {
if (state.end - state.start < 7) throw new Error("Out of bounds");
return uint24.decode(state) + 16777216 * uint32.decode(state);
return validateSafeUint(
uint24.decode(state) + 16777216 * uint32.decode(state)
);
}
};
var uint64 = exports.uint64 = {
@@ -1122,7 +1179,26 @@ var BridgeSwarmProtomux = (() => {
},
decode(state) {
if (state.end - state.start < 8) throw new Error("Out of bounds");
return uint32.decode(state) + 4294967296 * uint32.decode(state);
return validateSafeUint(
uint32.decode(state) + 4294967296 * uint32.decode(state)
);
}
};
exports.uint64be = {
preencode(state, n) {
state.end += 8;
},
encode(state, n) {
validateUint(n);
const r = Math.floor(n / 4294967296);
uint32be.encode(state, r);
uint32be.encode(state, n);
},
decode(state) {
if (state.end - state.start < 8) throw new Error("Out of bounds");
return validateSafeUint(
4294967296 * uint32be.decode(state) + uint32be.decode(state)
);
}
};
var int = exports.int = zigZagInt(uint);
@@ -1249,6 +1325,19 @@ var BridgeSwarmProtomux = (() => {
}
};
var buffer = exports.buffer = {
preencode(state, b) {
uint8array.preencode(state, b);
},
encode(state, b) {
uint8array.encode(state, b);
},
decode(state) {
const len = uint.decode(state);
if (state.end - state.start < len) throw new Error("Out of bounds");
return state.buffer.subarray(state.start, state.start += len);
}
};
exports.optionalBuffer = {
preencode(state, b) {
if (b) uint8array.preencode(state, b);
else state.end++;
@@ -1696,6 +1785,35 @@ var BridgeSwarmProtomux = (() => {
};
}
};
var record = exports.record = function(keyEncoding, valueEncoding) {
return {
preencode(state, v) {
const keys = Object.keys(v);
uint.preencode(state, keys.length);
for (const k of keys) {
keyEncoding.preencode(state, k);
valueEncoding.preencode(state, v[k]);
}
},
encode(state, v) {
const keys = Object.keys(v);
uint.encode(state, keys.length);
for (const k of keys) {
keyEncoding.encode(state, k);
valueEncoding.encode(state, v[k]);
}
},
decode(state) {
const out = /* @__PURE__ */ Object.create(null);
const keys = uint.decode(state);
for (let i = 0; i < keys; i++) {
out[keyEncoding.decode(state)] = valueEncoding.decode(state);
}
return out;
}
};
};
exports.stringRecord = record(utf8, utf8);
function getType(o) {
if (o === null || o === void 0) return 0;
if (typeof o === "boolean") return 1;
@@ -1823,9 +1941,20 @@ var BridgeSwarmProtomux = (() => {
function zigZagEncodeBigInt(n) {
return n < 0n ? 2n * -n - 1n : n === 0n ? 0n : 2n * n;
}
function validateSafeUint(n) {
if (n > Number.MAX_SAFE_INTEGER)
throw new Error(
"uint is greater than the maximum safe integer, use biguint/bigint"
);
return n;
}
function validateUint(n) {
if (n >= 0 === false)
throw new Error("uint must be positive");
if (n > Number.MAX_SAFE_INTEGER)
throw new Error(
"integer is greater than the maximum safe integer, use biguint/bigint"
);
}
}
});
@@ -1843,7 +1972,7 @@ var BridgeSwarmProtomux = (() => {
module.exports = safetyCatch;
function isActuallyUncaught(err) {
if (!err) return false;
return err instanceof TypeError || err instanceof SyntaxError || err instanceof ReferenceError || err instanceof EvalError || err instanceof RangeError || err instanceof URIError || err.code === "ERR_ASSERTION";
return err instanceof TypeError || err instanceof SyntaxError || err instanceof ReferenceError || err instanceof EvalError || err instanceof RangeError || err instanceof URIError || err.code === "ERR_ASSERTION" || err.name === "AssertionError";
}
function throwErrorNT(err) {
queueMicrotask(() => {
@@ -1970,14 +2099,14 @@ var BridgeSwarmProtomux = (() => {
const state = { buffer: null, start: 2, end: 2 };
c.uint.preencode(state, this._localId);
c.string.preencode(state, this.protocol);
c.buffer.preencode(state, this.id);
c.optionalBuffer.preencode(state, this.id);
if (this._handshake) this._handshake.preencode(state, handshake);
state.buffer = this._mux._alloc(state.end);
state.buffer[0] = 0;
state.buffer[1] = 1;
c.uint.encode(state, this._localId);
c.string.encode(state, this.protocol);
c.buffer.encode(state, this.id);
c.optionalBuffer.encode(state, this.id);
if (this._handshake) this._handshake.encode(state, handshake);
this._mux._write0(state.buffer);
}
@@ -1990,17 +2119,25 @@ var BridgeSwarmProtomux = (() => {
}
_fullyOpenSoon() {
this._mux._remote[this._remoteId - 1].session = this;
queueTick(this._fullyOpen.bind(this));
queueTick(this._fullyOpenOrDestroy.bind(this));
}
_fullyOpenOrDestroy() {
try {
this._fullyOpen();
} catch (err) {
this._mux._safeDestroyBound(err);
}
}
_fullyOpen() {
if (this.opened === true || this.closed === true) return;
const remote = this._mux._remote[this._remoteId - 1];
this.opened = true;
this.handshake = this._handshake ? this._handshake.decode(remote.state) : null;
this._track(this.onopen(this.handshake, this));
remote.session = this;
remote.state = null;
if (remote.pending !== null) this._drain(remote);
if (this._mux._destroying === true) return;
this.opened = true;
this._resolveOpen(true);
}
_resolveOpen(opened) {
@@ -2020,6 +2157,7 @@ var BridgeSwarmProtomux = (() => {
const p = remote.pending[i];
this._mux._buffered -= byteSize(p.state);
this._recv(p.type, p.state);
if (this._mux._destroying === true) return;
}
remote.pending = null;
this._mux._resumeMaybe();
@@ -2155,6 +2293,7 @@ var BridgeSwarmProtomux = (() => {
this._batchState = null;
this._infos = /* @__PURE__ */ new Map();
this._notify = /* @__PURE__ */ new Map();
this._destroying = false;
this.stream.on("data", this._ondata.bind(this));
this.stream.on("drain", this._ondrain.bind(this));
this.stream.on("end", this._onend.bind(this));
@@ -2207,18 +2346,56 @@ var BridgeSwarmProtomux = (() => {
const info = this._infos.get(key);
return info ? info.opened > 0 : false;
}
createChannel({ userData = null, protocol, aliases = [], id = null, unique = true, handshake = null, messages = [], onopen = noop, onclose = noop, ondestroy = noop, ondrain = noop }) {
createChannel({
userData = null,
protocol,
aliases = [],
id = null,
unique = true,
handshake = null,
messages = [],
onopen = noop,
onclose = noop,
ondestroy = noop,
ondrain = noop
}) {
if (this.stream.destroyed) return null;
const info = this._get(protocol, id, aliases);
if (unique && info.opened > 0) return null;
if (info.incoming.length === 0) {
return new Channel(this, info, userData, protocol, aliases, id, handshake, messages, onopen, onclose, ondestroy, ondrain);
return new Channel(
this,
info,
userData,
protocol,
aliases,
id,
handshake,
messages,
onopen,
onclose,
ondestroy,
ondrain
);
}
this._remoteBacklog--;
const remoteId = info.incoming.shift();
const r = this._remote[remoteId - 1];
if (r === null) return null;
const session = new Channel(this, info, userData, protocol, aliases, id, handshake, messages, onopen, onclose, ondestroy, ondrain);
const session = new Channel(
this,
info,
userData,
protocol,
aliases,
id,
handshake,
messages,
onopen,
onclose,
ondestroy,
ondrain
);
session._remoteId = remoteId;
session._fullyOpenSoon();
return session;
@@ -2257,7 +2434,17 @@ var BridgeSwarmProtomux = (() => {
const key = toKey(protocol, id);
let info = this._infos.get(key);
if (info) return info;
info = { key, protocol, aliases: [], id, pairing: 0, opened: 0, incoming: [], outgoing: [], lastChannel: null };
info = {
key,
protocol,
aliases: [],
id,
pairing: 0,
opened: 0,
incoming: [],
outgoing: [],
lastChannel: null
};
this._infos.set(key, info);
for (const alias of aliases) {
const key2 = toKey(alias, id);
@@ -2362,7 +2549,7 @@ var BridgeSwarmProtomux = (() => {
_onopensession(state) {
const remoteId = c.uint.decode(state);
const protocol = c.string.decode(state);
const id = unslab(c.buffer.decode(state));
const id = unslab(c.optionalBuffer.decode(state));
if (remoteId === 0) {
this._rejectSession(0);
return null;
@@ -2454,13 +2641,16 @@ var BridgeSwarmProtomux = (() => {
this.drained = this.stream.write(buffer);
}
destroy(err) {
this._destroying = true;
this.stream.destroy(err);
}
_safeDestroy(err) {
safetyCatch(err);
this._destroying = true;
this.stream.destroy(err);
}
_shutdown() {
this._destroying = true;
for (const s of this._local) {
if (s !== null) s._close(true);
}
+4 -3
View File
@@ -15,7 +15,6 @@ const path = require('bare-path');
const fs = require('bare-fs');
const { Duplex } = require('bare-stream');
const c = require('compact-encoding');
const def = require(path.join(path.dirname(require.resolve('hyperdb')), 'lib', 'definition.js'));
const minimalDefinition = require('./hyperdb-minimal-definition.js');
const b4a = require('b4a');
@@ -47,10 +46,11 @@ function log(...args) {
}
// Load generated HRPC lazily so a missing/broken spec does not crash the host at startup
// Prefer native-host/spec so bare-pack includes HRPC in distributable builds.
let HRPC = null;
let hrpcLoadError = null;
try {
HRPC = require(path.join(__dirname, '..', 'spec', 'hrpc'));
HRPC = require('./spec/hrpc');
} catch (err) {
hrpcLoadError = err;
}
@@ -135,7 +135,8 @@ async function getDefaultHyperdb() {
const store = getCorestore();
const hyperdbCore = store.get({ name: 'hyperdb' });
await hyperdbCore.ready();
defaultHyperdb = HyperDB.bee(hyperdbCore, def.compat(minimalDefinition), { autoUpdate: true });
// HyperDB.bee applies def.compat() internally (hyperdb 6+)
defaultHyperdb = HyperDB.bee(hyperdbCore, minimalDefinition, { autoUpdate: true });
await defaultHyperdb.ready();
}
return defaultHyperdb;
+2 -1
View File
@@ -1,6 +1,7 @@
/**
* Minimal hand-written Hyperdb definition: one collection "records" with
* primary key id (string) and value (string). Compatible with def.compat() and BeeEngine.
* primary key id (string) and value (string).
* HyperDB.bee() applies def.compat() for this shape (hyperdb 6+ / compact-encoding 3).
*/
const c = require('compact-encoding');
const b4a = require('b4a');
+38 -18
View File
@@ -1,28 +1,50 @@
/**
* BridgeSwarm native messaging host entrypoint.
* Reads length-prefixed JSON from stdin, writes to stdout (Chrome/Firefox protocol).
* Requires bare-process for process.stdin/stdout.
*
* bare-process/global must be the very first import so that `process` is
* available as a global before any other module runs (required for bare-pack
* standalone binaries).
*
* All local imports are static so bare-pack can pre-resolve the full module graph.
*
* macOS install: when run with --extract-addons, loads native addons so the
* Bare runtime extracts them; the installer then ad-hoc signs those files.
*/
import { createRequire } from 'bare-module';
import 'bare-process/global';
import _messenger from './messenger.js';
import _host from './host.js';
function logErr(msg) {
if (typeof process !== 'undefined' && process.stderr) {
try {
process.stderr.write(`[bridge-swarm-host] ${msg}\n`);
}
} catch (_) {}
}
try {
const require = createRequire(import.meta.url);
logErr('loading bare-process...');
require('bare-process/global');
if (process.argv.includes('--extract-addons')) {
(async () => {
const addonPackages = [
'bare-fs',
'bare-pipe',
'bare-module',
'udx-native',
'sodium-native',
'rocksdb-native',
];
for (const name of addonPackages) {
try {
await import(name);
} catch (_) {}
}
process.exit(0);
})();
} else {
try {
const { createMessenger } = _messenger;
const { handleMessage, cleanup } = _host;
logErr('loading messenger...');
const { createMessenger } = require('./messenger.js');
logErr('loading host (hyperswarm)...');
const { handleMessage, cleanup } = require('./host.js');
const input = process.stdin;
const output = process.stdout;
@@ -39,10 +61,7 @@ try {
input,
output,
onMessage(msg) {
handleMessage(
(response) => messenger.send(response),
msg
).catch((err) => {
handleMessage((response) => messenger.send(response), msg).catch((err) => {
logErr(err.stack || err.message);
const id = msg && msg.id;
if (id) {
@@ -66,8 +85,9 @@ try {
process.on('SIGINT', shutdown);
logErr('ready');
} catch (err) {
} catch (err) {
logErr(`startup error: ${err.message}`);
if (err.stack) logErr(err.stack);
process.exitCode = 1;
}
}
+756 -233
View File
File diff suppressed because it is too large Load Diff
+18 -16
View File
@@ -5,28 +5,30 @@
"type": "commonjs",
"main": "index.mjs",
"scripts": {
"start": "bare index.mjs",
"start": "node ./node_modules/bare/bin/bare index.mjs",
"test": "echo \"Run from extension; no standalone test\" && exit 0"
},
"dependencies": {
"autobase": "^7.25.0",
"b4a": "^1.6.7",
"bare-module": "^6.1.3",
"bare-process": "^4.1.2",
"bare-stream": "^2.6.5",
"compact-encoding": "^2.18.0",
"corestore": "^7.0.0",
"autobase": "^7.28.1",
"b4a": "^1.8.1",
"bare": "^1.30.3",
"bare-fs": "^4.7.4",
"bare-module": "^6.4.0",
"bare-path": "^3.1.1",
"bare-process": "^4.5.1",
"bare-stream": "^2.13.3",
"compact-encoding": "^3.3.0",
"corestore": "^7.11.1",
"hrpc": "^4.3.0",
"hyperbee": "^2.27.0",
"hypercore": "^11.0.0",
"hyperdb": "^5.0.0",
"hyperdrive": "^13.0.0",
"hyperschema": "^1.19.0",
"hyperbee": "^2.27.3",
"hypercore": "^11.35.0",
"hyperdb": "^6.7.0",
"hyperdrive": "^13.3.3",
"hyperschema": "^1.21.0",
"hyperswarm": "^4.17.0",
"bare-path": "^1.0.0",
"protomux": "^3.10.0"
"protomux": "^3.11.0"
},
"engines": {
"bare": ">=1.0.0"
"bare": ">=1.29.4"
}
}
+1106 -209
View File
File diff suppressed because it is too large Load Diff
+13 -6
View File
@@ -9,17 +9,24 @@
"build:protomux": "node scripts/bundle-protomux.js",
"build:hrpc": "node scripts/build-hrpc.js",
"build": "npm run build:host && npm run build:protomux && npm run build:hrpc",
"pack": "node scripts/pack-extension.js"
"pack": "node scripts/pack-extension.js",
"build:dist": "node scripts/build-distributable.js",
"build:dist:all": "node scripts/build-distributable.js --all",
"build:dist:mac": "node scripts/build-distributable.js --host darwin-arm64 --host darwin-x64",
"build:dist:linux": "node scripts/build-distributable.js --host linux-arm64 --host linux-x64",
"build:dist:win": "node scripts/build-distributable.js --host win32-x64",
"build:dist:package": "node scripts/build-distributable.js --all --package"
},
"devDependencies": {
"archiver": "^7.0.1",
"esbuild": "^0.24.0",
"bare-build": "^1.0.2",
"esbuild": "^0.28.1",
"hrpc": "^4.3.0",
"hyperschema": "^1.19.0"
"hyperschema": "^1.21.0"
},
"dependencies": {
"b4a": "^1.6.0",
"compact-encoding": "^2.18.0",
"protomux": "^3.10.0"
"b4a": "^1.8.1",
"compact-encoding": "^3.3.0",
"protomux": "^3.11.0"
}
}
+421
View File
@@ -0,0 +1,421 @@
#!/usr/bin/env node
/**
* Build standalone distributable binaries for the BridgeSwarm native host.
*
* Uses bare-pack + bare-build (same approach as holesail-browser) to produce
* self-contained executables with no Node/npm required on the end-user machine.
*
* Usage:
* node scripts/build-distributable.js # current host only
* node scripts/build-distributable.js --all # all platforms
* node scripts/build-distributable.js --host darwin-arm64 --host linux-x64
* node scripts/build-distributable.js --package # also create .zip archives
*
* Output under releases/:
* bridge-swarm-host-darwin-arm64.zip, -darwin-x64.zip,
* -linux-arm64.zip, -linux-x64.zip, -win32-x64.zip
*/
const path = require('path');
const fs = require('fs');
const os = require('os');
const { execSync } = require('child_process');
const { pathToFileURL } = require('url');
const ROOT = path.join(__dirname, '..');
const NATIVE_HOST_DIR = path.join(ROOT, 'native-host');
const RELEASES_DIR = path.join(ROOT, 'releases');
const ENTRY = path.join(NATIVE_HOST_DIR, 'index.mjs');
const HOST_NAME = 'bridge-swarm-host';
const ALL_HOSTS = [
'darwin-arm64',
'darwin-x64',
'linux-arm64',
'linux-x64',
'win32-x64',
];
const BUILTINS = [];
function getCurrentHost() {
const platform = os.platform();
const arch = os.arch() === 'arm64' ? 'arm64' : 'x64';
return `${platform}-${arch}`;
}
function parseArgs() {
const args = process.argv.slice(2);
const hosts = [];
let all = false;
let doPackage = false;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--all') all = true;
else if (args[i] === '--package') doPackage = true;
else if (args[i] === '--host' && args[i + 1]) hosts.push(args[++i]);
}
if (all) return { hosts: ALL_HOSTS, doPackage };
if (hosts.length > 0) return { hosts, doPackage };
return { hosts: [getCurrentHost()], doPackage };
}
function getPlatformModule(host) {
const bareBuildDir = path.dirname(require.resolve('bare-build'));
switch (host) {
case 'darwin-arm64':
case 'darwin-x64':
return require(path.join(bareBuildDir, 'lib/platform/apple'));
case 'linux-arm64':
case 'linux-x64':
return require(path.join(bareBuildDir, 'lib/platform/linux'));
case 'win32-x64':
case 'win32-arm64':
return require(path.join(bareBuildDir, 'lib/platform/windows'));
default:
throw new Error(`Unknown host '${host}'`);
}
}
function normalizeBundleKeysToWindows(bundle) {
const next = new bundle.constructor();
next._id = bundle._id;
const keyMap = {};
for (const key of bundle.keys()) {
const newKey = key.replace(/\//g, '\\');
keyMap[key] = newKey;
const content = bundle.read(key);
const mode = bundle.mode(key);
const opts = { mode };
if (key === bundle.main) opts.main = true;
if (bundle.addons && bundle.addons.includes(key)) opts.addon = true;
if (bundle.assets && bundle.assets.includes(key)) opts.asset = true;
const res = bundle.resolutions && bundle.resolutions[key];
if (res) opts.imports = transformResolutionKeys(res, keyMap);
next.write(newKey, content, opts);
}
for (const [alias, key] of Object.entries(bundle.imports || {})) {
next._imports[alias] = keyMap[key] ?? key.replace(/\//g, '\\');
}
return next;
}
function transformResolutionKeys(obj, keyMap) {
if (typeof obj === 'string') return keyMap[obj] ?? obj.replace(/\//g, '\\');
if (obj && typeof obj === 'object' && !Buffer.isBuffer(obj)) {
const out = {};
for (const [k, v] of Object.entries(obj)) {
out[k] = transformResolutionKeys(v, keyMap);
}
return out;
}
return obj;
}
function patchBareBuildSignForLinux() {
if (os.platform() === 'darwin') return;
try {
execSync('which codesign', { stdio: 'ignore' });
return;
} catch (_) {}
const bareBuildDir = path.dirname(require.resolve('bare-build'));
const signPath = path.join(bareBuildDir, 'lib/platform/apple/sign.js');
if (!fs.existsSync(signPath)) return;
const current = fs.readFileSync(signPath, 'utf8');
if (current.includes('PATCHED_NO_CODESIGN')) return;
fs.writeFileSync(
signPath,
`// PATCHED_NO_CODESIGN: codesign not available on this platform (Linux cross-build)
module.exports = async function sign() {}
`
);
console.log(' Patched bare-build/apple/sign.js → no-op (codesign not available on Linux)');
}
/**
* Ensure .json entries are valid and add runtime.bundle pathname variants
* (needed so the embedded Bare runtime can resolve package.json files).
*/
function patchBundle(bundle) {
const bundleKeys = typeof bundle.keys === 'function' ? [...bundle.keys()] : Object.keys(bundle.files || {});
function resolveKey(pathSuffix) {
const normalized = pathSuffix.replace(/^\/+/, '').replace(/\\/g, '/');
const withSlash = '/' + normalized;
if (bundleKeys.includes(withSlash)) return withSlash;
if (bundleKeys.includes(normalized)) return normalized;
return withSlash;
}
function resolveJsonDiskPath(keyNorm) {
const inNativeHost = path.join(NATIVE_HOST_DIR, keyNorm);
if (fs.existsSync(inNativeHost)) return inNativeHost;
if (keyNorm.startsWith('node_modules' + path.sep) || keyNorm.startsWith('node_modules/')) {
const inRoot = path.join(ROOT, keyNorm.replace(/\//g, path.sep));
if (fs.existsSync(inRoot)) return inRoot;
}
return null;
}
let jsonFixed = 0;
let keysToProcess = typeof bundle.keys === 'function' ? [...bundle.keys()] : Object.keys(bundle.files || {});
for (const key of keysToProcess) {
if (!key.endsWith('.json')) continue;
let content = bundle.read(key);
if (!content || content.length === 0) {
const altKey = key.startsWith('/') ? key.slice(1) : '/' + key.replace(/^\/+/, '');
content = bundle.read(altKey);
if (content && content.length > 0) bundle.write(key, content);
}
const isEmpty = !content || content.length === 0;
const invalidJson =
content &&
content.length > 0 &&
(() => {
try {
JSON.parse(content.toString());
return false;
} catch (_) {
return true;
}
})();
if (isEmpty || invalidJson) {
const keyNorm = key.replace(/^\/+/, '').replace(/\//g, path.sep);
const diskPath = resolveJsonDiskPath(keyNorm);
if (diskPath) {
content = fs.readFileSync(diskPath);
bundle.write(key, content);
jsonFixed++;
}
}
if (content && content.length > 0) {
const keyNoLead = key.replace(/^\/+/, '');
const prefixSlash = 'runtime.bundle/' + keyNoLead;
const prefixLead = '/runtime.bundle/' + keyNoLead;
if (prefixSlash !== key) bundle.write(prefixSlash, content);
if (prefixLead !== key) bundle.write(prefixLead, content);
}
}
// Touch resolveKey so unused-lint tooling doesn't complain if tree-shaken later
void resolveKey;
if (jsonFixed > 0) console.log(` Patched ${jsonFixed} empty/invalid .json entries`);
console.log(' Added runtime.bundle pathname variants for .json entries');
}
function getHostFromBuiltPath(filePath, platformHosts = null) {
const normalized = path.relative(RELEASES_DIR, filePath).replace(/\\/g, '/');
const lower = normalized.toLowerCase();
if (lower.endsWith('.exe') || lower.includes('win32-x64')) return 'win32-x64';
if (lower.includes('linux-arm64')) return 'linux-arm64';
if (lower.includes('linux-x64')) return 'linux-x64';
if (lower.includes('darwin-arm64')) return 'darwin-arm64';
if (lower.includes('darwin-x64')) return 'darwin-x64';
if (platformHosts && platformHosts.length > 0) {
if (lower.includes('arm64/') || lower.includes('aarch64/')) {
const arm = platformHosts.find((h) => h.includes('arm64') || h.includes('aarch64'));
if (arm) return arm;
}
if (lower.includes('x86_64/')) {
const x64 = platformHosts.find((h) => h.includes('x64'));
if (x64) return x64;
}
}
if (lower.includes('aarch64/')) return 'darwin-arm64';
if (lower.includes('x86_64/')) return 'darwin-x64';
if (lower.includes('arm64/')) return 'linux-arm64';
if (normalized === HOST_NAME || normalized.startsWith(HOST_NAME + '/')) return 'darwin-arm64';
return null;
}
async function createZipArchives(builtEntries) {
let archiver;
try {
archiver = require('archiver');
} catch {
console.warn(' Skipping zip archives: archiver not installed');
return;
}
const byHost = new Map();
const hostOrder = ['darwin-arm64', 'darwin-x64', 'linux-arm64', 'linux-x64', 'win32-x64'];
let fallbackIdx = 0;
for (const { file, platformHosts } of builtEntries) {
const rel = path.relative(RELEASES_DIR, file).replace(/\\/g, '/').toLowerCase();
const hasArchSubdir =
rel.includes('aarch64/') ||
rel.includes('x86_64/') ||
rel.includes('arm64/') ||
rel.includes('linux-') ||
rel.includes('win32') ||
rel.endsWith('.exe');
if (!hasArchSubdir && platformHosts && platformHosts.length > 1) {
for (const h of platformHosts) {
if (!byHost.has(h)) byHost.set(h, file);
}
continue;
}
let host = getHostFromBuiltPath(file, platformHosts);
if (!host) host = hostOrder[fallbackIdx++] || 'unknown';
const isArchSpecific =
rel.includes('/') &&
(rel.includes('aarch64') ||
rel.includes('x86_64') ||
rel.includes('arm64') ||
rel.includes('linux-') ||
rel.includes('win32') ||
rel.endsWith('.exe'));
const current = byHost.get(host);
const currentRel = current ? path.relative(RELEASES_DIR, current).replace(/\\/g, '/').toLowerCase() : '';
if (!current || (isArchSpecific && !currentRel.includes('/'))) {
byHost.set(host, file);
}
}
for (const [host, file] of byHost) {
const zipName = `${HOST_NAME}-${host}.zip`;
const zipPath = path.join(RELEASES_DIR, zipName);
await new Promise((resolve, reject) => {
const output = fs.createWriteStream(zipPath);
const archive = archiver('zip', { zlib: { level: 9 } });
output.on('close', resolve);
archive.on('error', reject);
archive.pipe(output);
if (fs.statSync(file).isDirectory()) {
archive.directory(file, false);
} else {
archive.file(file, { name: path.basename(file) });
}
archive.finalize();
});
console.log(' Packaged:', zipName);
}
const missing = hostOrder.filter((h) => !byHost.has(h));
if (missing.length > 0) {
console.warn(' Warning: no build output for:', missing.join(', '));
console.warn(' Run on Linux (e.g. CI) with --all --package to get all platform zips.');
}
}
async function build(hosts, doPackage) {
patchBareBuildSignForLinux();
// Ensure HRPC spec is mirrored into native-host before packing
console.log('Building HRPC spec...');
execSync('npm run build:hrpc', { cwd: ROOT, stdio: 'inherit' });
if (!fs.existsSync(path.join(NATIVE_HOST_DIR, 'node_modules'))) {
console.log('Installing native-host dependencies...');
execSync('npm install', { cwd: NATIVE_HOST_DIR, stdio: 'inherit' });
}
if (!fs.existsSync(path.join(NATIVE_HOST_DIR, 'spec', 'hrpc', 'index.js'))) {
throw new Error('native-host/spec/hrpc missing after build:hrpc');
}
fs.mkdirSync(RELEASES_DIR, { recursive: true });
const pack = require('bare-pack');
const { readModule, listPrefix } = require('bare-pack/fs');
const traverse = require('bare-module-traverse');
const bundleId = require('bare-bundle-id');
const pkg = require(path.join(NATIVE_HOST_DIR, 'package.json'));
console.log(`\nBuilding ${HOST_NAME} v${pkg.version}`);
console.log(`Targets: ${hosts.join(', ')}`);
console.log(`Entry: ${ENTRY}`);
console.log(`Output: ${RELEASES_DIR}\n`);
const unixHosts = hosts.filter((h) => !h.startsWith('win32'));
const winHosts = hosts.filter((h) => h.startsWith('win32'));
const hasWindows = winHosts.length > 0;
const hasUnix = unixHosts.length > 0;
const built = [];
const builtEntries = [];
const platformLabels = new Map();
platformLabels.set(getPlatformModule('darwin-arm64'), 'Apple (darwin)');
platformLabels.set(getPlatformModule('linux-arm64'), 'Linux');
platformLabels.set(getPlatformModule('win32-x64'), 'Windows');
async function buildAndEmit(bundleHosts, platformHostsList, normalizeForWindows) {
if (bundleHosts.length === 0) return;
console.log(' Bundling module graph' + (normalizeForWindows ? ' (Windows bundle)' : '') + '...');
let bundle = await pack(
pathToFileURL(ENTRY),
{
hosts: bundleHosts,
linked: false,
resolve: traverse.resolve.bare,
builtins: BUILTINS,
},
readModule,
listPrefix
);
bundle = bundle.unmount(pathToFileURL(NATIVE_HOST_DIR + '/'));
patchBundle(bundle);
if (normalizeForWindows) {
bundle = normalizeBundleKeysToWindows(bundle);
console.log(' Normalized bundle keys to Windows path form');
}
bundle.id = bundleId(bundle).toString('hex');
console.log(` Bundle size: ${(bundle.toBuffer().length / 1024 / 1024).toFixed(1)} MB`);
const groups = new Map();
for (const h of platformHostsList) {
const platform = getPlatformModule(h);
if (!groups.has(platform)) groups.set(platform, []);
groups.get(platform).push(h);
}
for (const [platform, platformHosts] of groups) {
const label = platformLabels.get(platform) || 'Unknown';
console.log(` Building ${label} (${platformHosts.join(', ')})...`);
let count = 0;
for await (const file of platform(NATIVE_HOST_DIR, bundle, null, {
name: HOST_NAME,
version: pkg.version,
description: pkg.description,
hosts: platformHosts,
out: RELEASES_DIR,
standalone: true,
})) {
console.log(' Built:', path.relative(ROOT, file));
built.push(file);
builtEntries.push({ file, platformHosts });
count++;
}
console.log(` -> ${count} artifact(s)`);
}
}
if (hasWindows && hasUnix) {
await buildAndEmit(unixHosts, unixHosts, false);
await buildAndEmit(winHosts, winHosts, true);
} else if (hasWindows) {
await buildAndEmit(winHosts, winHosts, true);
} else {
await buildAndEmit(hosts, hosts, false);
}
if (doPackage) {
await createZipArchives(builtEntries);
}
console.log('\nDone.');
return built;
}
const { hosts, doPackage } = parseArgs();
build(hosts, doPackage).catch((err) => {
console.error('\nBuild failed:', err.message || err);
if (err.cause) console.error('Cause:', err.cause);
process.exitCode = 1;
});
+23 -14
View File
@@ -10,39 +10,48 @@ const { execSync } = require('child_process');
const nativeHostDir = path.join(__dirname, '..', 'native-host');
const launcherPath = path.join(nativeHostDir, 'bridge-swarm-host');
// Find node path
const nodeBin = process.execPath;
// Try to find bare - check common locations
let bareBin = null;
try {
bareBin = execSync('which bare', { encoding: 'utf8' }).trim();
} catch (e) {}
if (!bareBin) {
// Try common paths on macOS
for (const p of ['/opt/homebrew/bin/bare', '/usr/local/bin/bare']) {
function isExecutable(p) {
try {
fs.accessSync(p, fs.constants.X_OK);
return true;
} catch {
return false;
}
}
// Prefer the Bare package installed with native-host (pinned runtime).
let bareBin = path.join(nativeHostDir, 'node_modules', 'bare', 'bin', 'bare');
if (!isExecutable(bareBin)) {
bareBin = null;
try {
bareBin = execSync('which bare', { encoding: 'utf8' }).trim();
} catch {}
}
if (!bareBin) {
for (const p of ['/opt/homebrew/bin/bare', '/usr/local/bin/bare']) {
if (isExecutable(p)) {
bareBin = p;
break;
} catch (e) {}
}
}
}
if (!bareBin) {
// Fall back to node's sibling
bareBin = nodeBin.replace(/node$/, 'bare');
}
// bare's npm bin is a Node script that spawns bare-runtime
const launcherContent = `#!/usr/bin/env bash
DIR="$(cd "$(dirname "$0")" && pwd)"
exec "${bareBin}" "$DIR/index.mjs" "$@"
exec "${nodeBin}" "${bareBin}" "$DIR/index.mjs" "$@"
`;
fs.writeFileSync(launcherPath, launcherContent);
fs.chmodSync(launcherPath, '755');
console.log('Native host launcher generated at:', launcherPath);
console.log('Using node:', nodeBin);
console.log('Using bare:', bareBin);
+15 -1
View File
@@ -106,4 +106,18 @@ rpcNs.register({
HRPCBuilder.toDisk(builder);
console.log('hrpc spec built: spec/hyperschema/, spec/hrpc/');
// Mirror into native-host so bare-pack / distributable builds can require('./spec/hrpc')
const HOST_SPEC = path.join(REPO_ROOT, 'native-host', 'spec');
function copyDir(src, dest) {
fs.mkdirSync(dest, { recursive: true });
for (const name of fs.readdirSync(src)) {
const from = path.join(src, name);
const to = path.join(dest, name);
if (fs.statSync(from).isDirectory()) copyDir(from, to);
else fs.copyFileSync(from, to);
}
}
copyDir(SCHEMA_DIR, path.join(HOST_SPEC, 'hyperschema'));
copyDir(HRPC_DIR, path.join(HOST_SPEC, 'hrpc'));
console.log('hrpc spec built: spec/hyperschema/, spec/hrpc/, native-host/spec/');
+62
View File
@@ -0,0 +1,62 @@
# BridgeSwarm — install from a local clone (dev / from-source).
# For end users prefer: irm .../scripts/install.ps1 | iex (release artifacts)
$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$RepoRoot = (Resolve-Path (Join-Path $ScriptDir "..")).Path
Write-Host "BridgeSwarm Install from source" -ForegroundColor Cyan
Write-Host "================================="
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
Write-Host "Error: Node.js is required. Install from https://nodejs.org" -ForegroundColor Red
exit 1
}
Write-Host ""
Write-Host "1. Installing native host..."
$HostDir = Join-Path $RepoRoot "native-host"
Set-Location $HostDir
npm install --no-fund --no-audit 2>$null
if ($LASTEXITCODE -ne 0) { npm install }
Set-Location $RepoRoot
npm install --no-fund --no-audit 2>$null
if ($LASTEXITCODE -ne 0) { npm install }
npm run build
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$nodeCmd = Get-Command node -ErrorAction SilentlyContinue
$nodePath = if ($nodeCmd) { $nodeCmd.Source } else { "node" }
$localBare = Join-Path $HostDir "node_modules\bare\bin\bare"
$bareCmd = Get-Command bare -ErrorAction SilentlyContinue
$barePath = if (Test-Path $localBare) { $localBare } elseif ($bareCmd) { $bareCmd.Source } else { "bare" }
$batContent = "@echo off`r`nset `"DIR=%~dp0`"`r`n`"$nodePath`" `"$barePath`" `"%DIR%index.mjs`" %*"
$batContent | Set-Content (Join-Path $HostDir "bridge-swarm-host.bat") -Encoding ASCII
$HostPath = Join-Path $HostDir "bridge-swarm-host.bat"
$manifestPath = Join-Path $RepoRoot "com.bridgeswarm.json"
$manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json
$manifest.path = $HostPath
$manifestFile = Join-Path $env:LOCALAPPDATA "bridge-swarm\com.bridgeswarm.json"
$manifestDir = Split-Path $manifestFile
if (-not (Test-Path $manifestDir)) { New-Item -ItemType Directory -Path $manifestDir -Force | Out-Null }
$manifest | ConvertTo-Json -Depth 4 | Set-Content $manifestFile -Encoding UTF8
$chromeKey = "HKCU:\Software\Google\Chrome\NativeMessagingHosts\com.bridgeswarm"
New-Item -Path $chromeKey -Force | Out-Null
Set-ItemProperty -Path $chromeKey -Name "(Default)" -Value $manifestFile
$ffKey = "HKCU:\Software\Mozilla\NativeMessagingHosts\com.bridgeswarm"
New-Item -Path $ffKey -Force | Out-Null
Set-ItemProperty -Path $ffKey -Name "(Default)" -Value $manifestFile
Write-Host ""
Write-Host "2. Preparing extension..."
$ExtDir = Join-Path $RepoRoot "extension"
try { Set-Clipboard -Value $ExtDir } catch {}
Start-Process "chrome://extensions" -ErrorAction SilentlyContinue
Write-Host ""
Write-Host "Done. Load unpacked extension from: $ExtDir"
Write-Host "Host bat: $HostPath"
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# BridgeSwarm — install from a local git clone (dev / from-source).
# End users should use the release installer instead:
# curl -fsSL https://git.ssh.surf/snxraven/BridgeSwarm/raw/branch/main/scripts/web-installer.sh | bash
#
# Usage: ./scripts/install-from-source.sh or npm run setup
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$REPO_ROOT"
echo "BridgeSwarm Install from source"
echo "================================="
# Require Node.js
if ! command -v node >/dev/null 2>&1; then
echo "Error: Node.js is required. Install from https://nodejs.org and run this script again."
exit 1
fi
# 1. Native host (pulls Bare via the local `bare` npm dependency)
echo ""
echo "1. Installing native host..."
HOST_DIR="$REPO_ROOT/native-host"
cd "$HOST_DIR"
npm install --no-fund --no-audit 2>/dev/null || npm install
if [[ ! -x "$HOST_DIR/node_modules/bare/bin/bare" ]] && ! command -v bare >/dev/null 2>&1; then
echo "Warning: Bare runtime not found after install. Check native-host dependency \`bare\`."
echo "Continuing; native host may not work until Bare is available."
fi
# Build hrpc spec (generated code for native host) and protomux bundle (from repo root)
echo ""
echo "1b. Building hrpc spec and Protomux bundle..."
cd "$REPO_ROOT"
npm install --no-fund --no-audit 2>/dev/null || npm install
node scripts/build-hrpc.js
npm run build:host
npm run build:protomux
cd "$HOST_DIR"
HOST_PATH="$HOST_DIR/bridge-swarm-host"
CHROME_DIR="$HOME/.config/google-chrome/NativeMessagingHosts"
CHROMIUM_DIR="$HOME/.config/chromium/NativeMessagingHosts"
FIREFOX_DIR="$HOME/.mozilla/native-messaging-hosts"
if [[ "$OSTYPE" == "darwin"* ]]; then
CHROME_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
CHROMIUM_DIR="$HOME/Library/Application Support/Chromium/NativeMessagingHosts"
FIREFOX_DIR="$HOME/Library/Application Support/Mozilla/NativeMessagingHosts"
fi
MANIFEST_NAME="com.bridgeswarm"
MANIFEST_CONTENT=$(sed "s|ABSOLUTE_PATH_TO_NATIVE_HOST|$HOST_PATH|g" "$REPO_ROOT/com.bridgeswarm.json")
for dir in "$CHROME_DIR" "$CHROMIUM_DIR" "$FIREFOX_DIR"; do
mkdir -p "$dir" 2>/dev/null && echo "$MANIFEST_CONTENT" > "$dir/${MANIFEST_NAME}.json" && echo " Native host manifest: $dir"
done
# 2. Extension
echo ""
echo "2. Preparing extension..."
EXT_DIR="$REPO_ROOT/extension"
if [[ ! -f "$EXT_DIR/manifest.json" ]]; then
echo "Error: extension/manifest.json not found."
exit 1
fi
if command -v pbcopy >/dev/null 2>&1; then
echo "$EXT_DIR" | pbcopy
elif command -v xclip >/dev/null 2>&1; then
echo -n "$EXT_DIR" | xclip -selection clipboard 2>/dev/null || true
elif command -v xsel >/dev/null 2>&1; then
echo -n "$EXT_DIR" | xsel --clipboard 2>/dev/null || true
fi
open_page() {
local url="$1" app="$2"
if [[ "$OSTYPE" == "darwin"* ]]; then
open -a "$app" "$url" 2>/dev/null && return 0
fi
command -v xdg-open >/dev/null 2>&1 && xdg-open "$url" 2>/dev/null && return 0
return 1
}
open_page "chrome://extensions" "Google Chrome" || \
open_page "chrome://extensions" "Chromium" || \
open_page "chrome://extensions" "Microsoft Edge" || true
echo ""
echo "Done."
echo ""
echo "Next step: In the browser tab that opened, click 'Load unpacked' and paste this path:"
echo " $EXT_DIR"
echo "(Path is in your clipboard.) Then restart the browser."
+3 -2
View File
@@ -14,9 +14,10 @@ if (-not (Test-Path (Join-Path $HostDir "node_modules"))) {
}
$nodePath = (Get-Command node -ErrorAction SilentlyContinue).Source
$barePath = (Get-Command bare -ErrorAction SilentlyContinue).Source
$localBare = Join-Path $HostDir "node_modules\bare\bin\bare"
$bareCmd = Get-Command bare -ErrorAction SilentlyContinue
if (-not $nodePath) { $nodePath = "node" }
if (-not $barePath) { $barePath = "bare" }
$barePath = if (Test-Path $localBare) { $localBare } elseif ($bareCmd) { $bareCmd.Source } else { "bare" }
$batContent = "@echo off`r`nset `"DIR=%~dp0`"`r`n`"$nodePath`" `"$barePath`" `"%DIR%index.mjs`" %*"
Set-Content (Join-Path $HostDir "bridge-swarm-host.bat") -Value $batContent -Encoding ASCII
+7 -6
View File
@@ -13,15 +13,16 @@ else
echo " node_modules exists, skipping npm install"
fi
BARE_PATH="$(which bare 2>/dev/null)" || true
# Prefer Bare bundled with native-host (npm package `bare` → bare-runtime).
BARE_PATH=""
LOCAL_BARE="$HOST_DIR/node_modules/bare/bin/bare"
[[ -x "$LOCAL_BARE" ]] && BARE_PATH="$LOCAL_BARE"
[[ -z "$BARE_PATH" ]] && BARE_PATH="$(which bare 2>/dev/null)" || true
[[ -z "$BARE_PATH" ]] && for c in /opt/homebrew/bin/bare /usr/local/bin/bare; do [[ -x "$c" ]] && BARE_PATH="$c" && break; done
NODE_PATH=""
if [[ -n "$BARE_PATH" ]]; then
BARE_DIR="${BARE_PATH%/*}"
for c in "$BARE_DIR/node" "$(which node 2>/dev/null)" /opt/homebrew/bin/node /usr/local/bin/node; do
for c in "$(which node 2>/dev/null)" /opt/homebrew/bin/node /usr/local/bin/node; do
[[ -x "$c" ]] && NODE_PATH="$c" && break
done
fi
done
[[ -z "$NODE_PATH" ]] && NODE_PATH="node"
[[ -z "$BARE_PATH" ]] && BARE_PATH="bare"
+148 -74
View File
@@ -1,87 +1,161 @@
# Unified installer for BridgeSwarm (extension + native host).
# Run from anywhere: .\scripts\install.ps1 or cd bridge-swarm; .\scripts\install.ps1
# BridgeSwarm Installer (Windows)
# Downloads the native host binary and extension from the latest Gitea release.
#
# Usage (PowerShell as your normal user):
# irm https://git.ssh.surf/snxraven/BridgeSwarm/raw/branch/main/scripts/install.ps1 | iex
$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$RepoRoot = (Resolve-Path (Join-Path $ScriptDir "..")).Path
Write-Host "BridgeSwarm Install" -ForegroundColor Cyan
Write-Host "============================="
$ReleaseBase = "https://git.ssh.surf/snxraven/BridgeSwarm/releases/download/latest-main"
$InstallDir = "$env:LOCALAPPDATA\bridgeswarm"
$Downloads = "$env:USERPROFILE\Downloads"
$ManifestName = "com.bridgeswarm"
$ExtVersion = "1.0.0"
$ChromeExtId = "fmcenppcipeikpnpopolicnllljclmmi"
$FirefoxExtId = "[email protected]"
# Require Node.js
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
Write-Host "Error: Node.js is required. Install from https://nodejs.org and run this script again." -ForegroundColor Red
exit 1
}
$HostZip = "bridge-swarm-host-win32-x64.zip"
$ExtZip = "BridgeSwarm-$ExtVersion.zip"
$ExtXpi = "BridgeSwarm-$ExtVersion.xpi"
# Ensure bare is available
if (-not (Get-Command bare -ErrorAction SilentlyContinue)) {
Write-Host "Installing Bare runtime (required for native host)..."
npm install -g bare 2>$null
if (-not (Get-Command bare -ErrorAction SilentlyContinue)) {
Write-Host "Warning: Could not install bare. Run: npm install -g bare" -ForegroundColor Yellow
Write-Host ""
Write-Host "BridgeSwarm Installer" -ForegroundColor Cyan
Write-Host "=====================" -ForegroundColor Cyan
Write-Host "Install : $InstallDir"
Write-Host ""
# ── Stop running host ──────────────────────────────────────────────────────────
Write-Host "Stopping any running native host..."
Get-Process | Where-Object { $_.Path -like "*bridge-swarm-host*" } | Stop-Process -Force -ErrorAction SilentlyContinue
# ── Preserve storage ───────────────────────────────────────────────────────────
$StashDir = Join-Path $env:TEMP "bridgeswarm-stash-$([System.Guid]::NewGuid().ToString('N'))"
$StashStorage = Join-Path $StashDir "bridge-swarm-storage"
$HadPrevious = $false
if (Test-Path $InstallDir) {
Write-Host "Preserving existing storage..."
New-Item -ItemType Directory -Path $StashDir -Force | Out-Null
$StorageSrc = Join-Path $InstallDir "bridge-swarm-storage"
if (Test-Path $StorageSrc) {
Copy-Item $StorageSrc $StashStorage -Recurse -Force
Write-Host " Saved: bridge-swarm-storage"
$HadPrevious = $true
}
}
# 1. Native host
Write-Host ""
Write-Host "1. Installing native host..."
$HostDir = Join-Path $RepoRoot "native-host"
Set-Location $HostDir
npm install --no-fund --no-audit 2>$null
if ($LASTEXITCODE -ne 0) { npm install }
# ── Remove previous installation ───────────────────────────────────────────────
Write-Host "Removing any previous installation..."
if (Test-Path $InstallDir) { Remove-Item $InstallDir -Recurse -Force }
# Windows: use .bat that runs bare; Chrome may have limited PATH so use full path if we can find it
$bareCmd = Get-Command bare -ErrorAction SilentlyContinue
$nodeCmd = Get-Command node -ErrorAction SilentlyContinue
$nodePath = if ($nodeCmd) { $nodeCmd.Source } else { "node" }
$barePath = if ($bareCmd) { $bareCmd.Source } else { "bare" }
$batContent = "@echo off`r`nset `"DIR=%~dp0`"`r`n`"$nodePath`" `"$barePath`" `"%DIR%index.mjs`" %*"
$batContent | Set-Content (Join-Path $HostDir "bridge-swarm-host.bat") -Encoding ASCII
$HostPath = Join-Path $HostDir "bridge-swarm-host.bat"
$manifestPath = Join-Path $RepoRoot "com.bridgeswarm.json"
$manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json
$manifest.path = $HostPath
$manifestFile = Join-Path $env:LOCALAPPDATA "bridge-swarm\com.bridgeswarm.json"
$manifestDir = Split-Path $manifestFile
if (-not (Test-Path $manifestDir)) { New-Item -ItemType Directory -Path $manifestDir -Force | Out-Null }
$manifest | ConvertTo-Json -Depth 4 | Set-Content $manifestFile -Encoding UTF8
$chromeKey = "HKCU:\Software\Google\Chrome\NativeMessagingHosts\com.bridgeswarm"
New-Item -Path $chromeKey -Force | Out-Null
Set-ItemProperty -Path $chromeKey -Name "(Default)" -Value $manifestFile
$ffKey = "HKCU:\Software\Mozilla\NativeMessagingHosts\com.bridgeswarm"
New-Item -Path $ffKey -Force | Out-Null
Set-ItemProperty -Path $ffKey -Name "(Default)" -Value $manifestFile
Write-Host " Native host manifest: $manifestFile"
# 2. Extension
Write-Host ""
Write-Host "2. Preparing extension..."
$ExtDir = (Resolve-Path (Join-Path $RepoRoot "extension")).Path
if (-not (Test-Path (Join-Path $ExtDir "manifest.json"))) {
Write-Host "Error: extension/manifest.json not found." -ForegroundColor Red
exit 1
}
Set-Clipboard -Value $ExtDir
$chromePaths = @(
"${env:ProgramFiles}\Google\Chrome\Application\chrome.exe",
"${env:ProgramFiles(x86)}\Google\Chrome\Application\chrome.exe",
"${env:LocalAppData}\Google\Chrome\Application\chrome.exe"
$RegPaths = @(
"HKCU:\Software\Google\Chrome\NativeMessagingHosts\$ManifestName",
"HKCU:\Software\Chromium\NativeMessagingHosts\$ManifestName",
"HKCU:\Software\Mozilla\NativeMessagingHosts\$ManifestName"
)
foreach ($p in $chromePaths) {
if (Test-Path $p) {
Start-Process $p -ArgumentList "chrome://extensions"
break
}
foreach ($p in $RegPaths) {
if (Test-Path $p) { Remove-Item $p -Force -ErrorAction SilentlyContinue }
}
# ── Download host ──────────────────────────────────────────────────────────────
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
New-Item -ItemType Directory -Path $Downloads -Force | Out-Null
Write-Host "Downloading native host..."
$TmpZip = "$env:TEMP\$HostZip"
Invoke-WebRequest "$ReleaseBase/$HostZip" -OutFile $TmpZip
Expand-Archive -Path $TmpZip -DestinationPath $InstallDir -Force
Remove-Item $TmpZip
$HostBin = Get-ChildItem -Path $InstallDir -Recurse -Filter "bridge-swarm-host.exe" | Select-Object -First 1 -ExpandProperty FullName
if (-not $HostBin) {
Write-Host "Error: binary not found in $HostZip" -ForegroundColor Red; exit 1
}
Write-Host " Binary: $HostBin"
# Launcher bat that sets storage path
$HostDir = Split-Path $HostBin -Parent
$Launcher = Join-Path $HostDir "run-bridge-swarm-host.bat"
$Bat = @"
@echo off
set "DIR=%~dp0"
if not defined BRIDGE_SWARM_STORAGE set "BRIDGE_SWARM_STORAGE=%DIR%bridge-swarm-storage"
"%DIR%bridge-swarm-host.exe" %*
"@
Set-Content -Path $Launcher -Value $Bat -Encoding ASCII
$HostBin = $Launcher
# ── Restore storage ────────────────────────────────────────────────────────────
if ($HadPrevious -and (Test-Path $StashStorage)) {
Write-Host "Restoring storage..."
Copy-Item $StashStorage (Join-Path $InstallDir "bridge-swarm-storage") -Recurse -Force
Write-Host " Restored: bridge-swarm-storage"
}
if (Test-Path $StashDir) { Remove-Item $StashDir -Recurse -Force -ErrorAction SilentlyContinue }
# ── Extension ──────────────────────────────────────────────────────────────────
Write-Host "Cleaning up old extension files in Downloads..."
Get-ChildItem "$Downloads\BridgeSwarm-*.zip","$Downloads\BridgeSwarm-*.xpi" -ErrorAction SilentlyContinue | ForEach-Object {
Remove-Item $_.FullName -Force
Write-Host " Removed: $($_.Name)"
}
Write-Host "Downloading extension..."
Invoke-WebRequest "$ReleaseBase/$ExtZip" -OutFile (Join-Path $Downloads $ExtZip)
Write-Host " Saved: $Downloads\$ExtZip"
try {
Invoke-WebRequest "$ReleaseBase/$ExtXpi" -OutFile (Join-Path $Downloads $ExtXpi)
} catch {}
# ── Native messaging manifests ─────────────────────────────────────────────────
Write-Host "Installing native messaging manifest..."
$ManifestChrome = @{
name = "com.bridgeswarm"
description = "BridgeSwarm native host (Bare / Hyperswarm)"
path = $HostBin
type = "stdio"
allowed_origins = @("chrome-extension://$ChromeExtId/")
} | ConvertTo-Json -Depth 4
$ManifestFirefox = @{
name = "com.bridgeswarm"
description = "BridgeSwarm native host (Bare / Hyperswarm)"
path = $HostBin
type = "stdio"
allowed_extensions = @($FirefoxExtId)
} | ConvertTo-Json -Depth 4
$ManifestDir = Join-Path $env:LOCALAPPDATA "bridgeswarm"
New-Item -ItemType Directory -Path $ManifestDir -Force | Out-Null
$ChromeManifestFile = Join-Path $ManifestDir "com.bridgeswarm.json"
$FirefoxManifestFile = Join-Path $ManifestDir "com.bridgeswarm.firefox.json"
Set-Content -Path $ChromeManifestFile -Value $ManifestChrome -Encoding UTF8
Set-Content -Path $FirefoxManifestFile -Value $ManifestFirefox -Encoding UTF8
New-Item -Path "HKCU:\Software\Google\Chrome\NativeMessagingHosts\$ManifestName" -Force | Out-Null
Set-ItemProperty -Path "HKCU:\Software\Google\Chrome\NativeMessagingHosts\$ManifestName" -Name "(Default)" -Value $ChromeManifestFile
New-Item -Path "HKCU:\Software\Chromium\NativeMessagingHosts\$ManifestName" -Force -ErrorAction SilentlyContinue | Out-Null
Set-ItemProperty -Path "HKCU:\Software\Chromium\NativeMessagingHosts\$ManifestName" -Name "(Default)" -Value $ChromeManifestFile -ErrorAction SilentlyContinue
New-Item -Path "HKCU:\Software\Mozilla\NativeMessagingHosts\$ManifestName" -Force | Out-Null
Set-ItemProperty -Path "HKCU:\Software\Mozilla\NativeMessagingHosts\$ManifestName" -Name "(Default)" -Value $FirefoxManifestFile
Write-Host ""
Write-Host "Done." -ForegroundColor Green
Write-Host "=====================" -ForegroundColor Cyan
Write-Host "Installation complete!"
Write-Host ""
Write-Host "Next steps:"
Write-Host ""
Write-Host " Chrome / Edge:"
Write-Host " 1. Open chrome://extensions"
Write-Host " 2. Enable Developer mode"
Write-Host " 3. Drag & drop $Downloads\$ExtZip onto the page"
Write-Host " 4. Extension ID should be: $ChromeExtId"
Write-Host ""
Write-Host " Firefox:"
Write-Host " about:debugging → Load Temporary Add-on → $Downloads\$ExtZip"
Write-Host " (or Install From File with $Downloads\$ExtXpi on Nightly/Dev Edition)"
Write-Host ""
Write-Host " Then restart your browser."
Write-Host ""
Write-Host " Install dir: $InstallDir"
Write-Host ""
Write-Host "Next step: In the browser tab that opened, click 'Load unpacked' and paste this path:"
Write-Host " $ExtDir"
Write-Host "(Path is in your clipboard.) Then restart the browser."
+215 -81
View File
@@ -1,98 +1,232 @@
#!/usr/bin/env bash
# Unified installer for BridgeSwarm (extension + native host).
# Run from anywhere: ./scripts/install.sh or cd bridge-swarm && ./scripts/install.sh
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$REPO_ROOT"
# BridgeSwarm Installer (macOS / Linux)
# Downloads the native host binary and extension from the latest Gitea release.
#
# Usage:
# curl -fsSL https://git.ssh.surf/snxraven/BridgeSwarm/raw/branch/main/scripts/web-installer.sh | bash
# # or:
# curl -fsSL https://git.ssh.surf/snxraven/BridgeSwarm/raw/branch/main/scripts/install.sh | bash
set -euo pipefail
echo "BridgeSwarm Install"
echo "============================="
RELEASE_BASE="https://git.ssh.surf/snxraven/BridgeSwarm/releases/download/latest-main"
INSTALL_DIR="$HOME/.bridgeswarm"
MANIFEST_NAME="com.bridgeswarm"
DOWNLOADS="$HOME/Downloads"
EXT_VERSION="1.0.0"
CHROME_EXT_ID="fmcenppcipeikpnpopolicnllljclmmi"
FIREFOX_EXT_ID="[email protected]"
# Require Node.js
if ! command -v node >/dev/null 2>&1; then
echo "Error: Node.js is required. Install from https://nodejs.org and run this script again."
exit 1
# ── Detect platform ────────────────────────────────────────────────────────────
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
ARCH="$(uname -m)"
[[ "$ARCH" == "x86_64" ]] && ARCH="x64"
[[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]] && ARCH="arm64"
[[ "$OS" == "darwin" ]] && PLATFORM="darwin"
[[ "$OS" == "linux" ]] && PLATFORM="linux"
if [[ -z "${PLATFORM:-}" ]]; then
echo "Unsupported OS: $OS" >&2; exit 1
fi
# Ensure bare is available (required for native host)
if ! command -v bare >/dev/null 2>&1; then
echo "Installing Bare runtime (required for native host)..."
if npm install -g bare 2>/dev/null; then
echo "Bare installed."
else
echo "Warning: Could not install bare. Run: npm install -g bare"
echo "Continuing; native host may not work until bare is installed."
HOST_ZIP="bridge-swarm-host-${PLATFORM}-${ARCH}.zip"
EXT_ZIP="BridgeSwarm-${EXT_VERSION}.zip"
EXT_XPI="BridgeSwarm-${EXT_VERSION}.xpi"
echo ""
echo "BridgeSwarm Installer"
echo "====================="
echo "Platform : ${PLATFORM}-${ARCH}"
echo "Install : ${INSTALL_DIR}"
echo ""
# ── Stop any running native host ───────────────────────────────────────────────
echo "Stopping any running native host..."
pkill -f "bridgeswarm/native-host/index.mjs" 2>/dev/null || true
pkill -f "bridge-swarm-host" 2>/dev/null || true
pkill -f "\.bridgeswarm" 2>/dev/null || true
# ── Preserve user data ─────────────────────────────────────────────────────────
STASH_DIR="$(mktemp -d)"
STASH_STORAGE="${STASH_DIR}/bridge-swarm-storage"
HAD_PREVIOUS=false
if [[ -d "$INSTALL_DIR" ]]; then
echo "Preserving existing storage..."
if [[ -d "${INSTALL_DIR}/bridge-swarm-storage" ]]; then
cp -a "${INSTALL_DIR}/bridge-swarm-storage" "$STASH_STORAGE"
echo " Saved: bridge-swarm-storage"
HAD_PREVIOUS=true
fi
fi
# 1. Native host
echo ""
echo "1. Installing native host..."
HOST_DIR="$REPO_ROOT/native-host"
cd "$HOST_DIR"
npm install --no-fund --no-audit 2>/dev/null || npm install
# ── Remove previous installation ───────────────────────────────────────────────
echo "Removing any previous installation..."
rm -rf "$INSTALL_DIR"
# Build hrpc spec (generated code for native host) and protomux bundle (from repo root)
echo ""
echo "1b. Building hrpc spec and Protomux bundle..."
cd "$REPO_ROOT"
npm install --no-fund --no-audit 2>/dev/null || npm install
node scripts/build-hrpc.js
npm run build:host
npm run build:protomux
cd "$HOST_DIR"
HOST_PATH="$HOST_DIR/bridge-swarm-host"
CHROME_DIR="$HOME/.config/google-chrome/NativeMessagingHosts"
CHROMIUM_DIR="$HOME/.config/chromium/NativeMessagingHosts"
FIREFOX_DIR="$HOME/.mozilla/native-messaging-hosts"
if [[ "$OSTYPE" == "darwin"* ]]; then
CHROME_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
CHROMIUM_DIR="$HOME/Library/Application Support/Chromium/NativeMessagingHosts"
FIREFOX_DIR="$HOME/Library/Application Support/Mozilla/NativeMessagingHosts"
fi
MANIFEST_NAME="com.bridgeswarm"
MANIFEST_CONTENT=$(sed "s|ABSOLUTE_PATH_TO_NATIVE_HOST|$HOST_PATH|g" "$REPO_ROOT/com.bridgeswarm.json")
for dir in "$CHROME_DIR" "$CHROMIUM_DIR" "$FIREFOX_DIR"; do
mkdir -p "$dir" 2>/dev/null && echo "$MANIFEST_CONTENT" > "$dir/${MANIFEST_NAME}.json" && echo " Native host manifest: $dir"
for dir in \
"$HOME/.config/google-chrome/NativeMessagingHosts" \
"$HOME/.config/chromium/NativeMessagingHosts" \
"$HOME/.mozilla/native-messaging-hosts" \
"$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts" \
"$HOME/Library/Application Support/Chromium/NativeMessagingHosts" \
"$HOME/Library/Application Support/Mozilla/NativeMessagingHosts"; do
rm -f "${dir}/${MANIFEST_NAME}.json" 2>/dev/null || true
done
# 2. Extension
echo ""
echo "2. Preparing extension..."
EXT_DIR="$REPO_ROOT/extension"
if [[ ! -f "$EXT_DIR/manifest.json" ]]; then
echo "Error: extension/manifest.json not found."
exit 1
fi
# ── Download host ──────────────────────────────────────────────────────────────
mkdir -p "$INSTALL_DIR"
mkdir -p "$DOWNLOADS"
if command -v pbcopy >/dev/null 2>&1; then
echo "$EXT_DIR" | pbcopy
elif command -v xclip >/dev/null 2>&1; then
echo -n "$EXT_DIR" | xclip -selection clipboard 2>/dev/null || true
elif command -v xsel >/dev/null 2>&1; then
echo -n "$EXT_DIR" | xsel --clipboard 2>/dev/null || true
fi
echo "Downloading native host..."
curl -fsSL "${RELEASE_BASE}/${HOST_ZIP}" -o "/tmp/${HOST_ZIP}"
unzip -q -o "/tmp/${HOST_ZIP}" -d "$INSTALL_DIR"
rm "/tmp/${HOST_ZIP}"
open_page() {
local url="$1" app="$2"
if [[ "$OSTYPE" == "darwin"* ]]; then
open -a "$app" "$url" 2>/dev/null && return 0
HOST_BIN="$(find "$INSTALL_DIR" -type f \( -name "bridge-swarm-host" -o -name "bridge-swarm-host.exe" \) | head -1)"
if [[ -z "$HOST_BIN" ]]; then
echo "Error: binary not found in ${HOST_ZIP}" >&2; exit 1
fi
chmod +x "$HOST_BIN"
# ── Restore storage ────────────────────────────────────────────────────────────
if [[ "$HAD_PREVIOUS" == "true" && -d "$STASH_STORAGE" ]]; then
echo "Restoring storage..."
cp -a "$STASH_STORAGE" "${INSTALL_DIR}/bridge-swarm-storage"
echo " Restored: bridge-swarm-storage"
fi
rm -rf "$STASH_DIR"
# On macOS: clear quarantine, ad-hoc sign, extract/sign native addons
if [[ "$PLATFORM" == "darwin" ]]; then
echo " Clearing quarantine and signing..."
/usr/bin/xattr -rd com.apple.quarantine "$INSTALL_DIR" 2>/dev/null || true
HOST_DIR="$(dirname "$HOST_BIN")"
ADDON_TMPDIR="${HOST_DIR}/tmp"
mkdir -p "$ADDON_TMPDIR"
ENTITLEMENTS_PLIST="${HOST_DIR}/entitlements.plist"
printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>' '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' '<plist version="1.0"><dict><key>com.apple.security.cs.disable-library-validation</key><true/></dict></plist>' > "$ENTITLEMENTS_PLIST"
codesign --force --sign - --entitlements "$ENTITLEMENTS_PLIST" "$HOST_BIN" 2>/dev/null || true
LAUNCHER="${HOST_DIR}/run-bridge-swarm-host.sh"
printf '%s\n' '#!/bin/bash' 'DIR="$(cd "$(dirname "$0")" && pwd)"' 'export TMPDIR="${DIR}/tmp"' 'export BRIDGE_SWARM_STORAGE="${BRIDGE_SWARM_STORAGE:-${DIR}/bridge-swarm-storage}"' 'exec "${DIR}/bridge-swarm-host" "$@"' > "$LAUNCHER"
chmod +x "$LAUNCHER"
echo " Extracting native addons (--extract-addons)..."
"$LAUNCHER" --extract-addons 2>/dev/null || true
sleep 2
SIGNED=0
if [[ -d "$ADDON_TMPDIR" ]]; then
/usr/bin/xattr -rd com.apple.quarantine "$ADDON_TMPDIR" 2>/dev/null || true
while IFS= read -r -d '' f; do
codesign --force --sign - "$f" 2>/dev/null && SIGNED=$((SIGNED + 1)) || true
done < <(find "$ADDON_TMPDIR" \( -name "*.bare" -o -name "*.dylib" \) -print0 2>/dev/null)
fi
command -v xdg-open >/dev/null 2>&1 && xdg-open "$url" 2>/dev/null && return 0
return 1
}
open_page "chrome://extensions" "Google Chrome" || \
open_page "chrome://extensions" "Chromium" || \
open_page "chrome://extensions" "Microsoft Edge" || true
echo " Signed ${SIGNED} native addons; main binary has library-validation disabled"
HOST_BIN="$LAUNCHER"
else
# Linux launcher sets storage path next to the binary
HOST_DIR="$(dirname "$HOST_BIN")"
LAUNCHER="${HOST_DIR}/run-bridge-swarm-host.sh"
printf '%s\n' '#!/bin/bash' 'DIR="$(cd "$(dirname "$0")" && pwd)"' 'export BRIDGE_SWARM_STORAGE="${BRIDGE_SWARM_STORAGE:-${DIR}/bridge-swarm-storage}"' 'exec "${DIR}/bridge-swarm-host" "$@"' > "$LAUNCHER"
chmod +x "$LAUNCHER"
HOST_BIN="$LAUNCHER"
fi
echo " Binary: $HOST_BIN"
# ── Extension downloads ────────────────────────────────────────────────────────
echo "Cleaning up old extension files in Downloads..."
for f in "${DOWNLOADS}"/BridgeSwarm-*.zip "${DOWNLOADS}"/BridgeSwarm-*.xpi; do
[[ -f "$f" ]] && rm -f "$f" && echo " Removed: $f" || true
done
echo "Downloading extension..."
curl -fsSL "${RELEASE_BASE}/${EXT_ZIP}" -o "${DOWNLOADS}/${EXT_ZIP}"
echo " Saved: ${DOWNLOADS}/${EXT_ZIP}"
curl -fsSL "${RELEASE_BASE}/${EXT_XPI}" -o "${DOWNLOADS}/${EXT_XPI}" 2>/dev/null || true
# ── Native messaging manifests ─────────────────────────────────────────────────
echo "Installing native messaging manifest..."
MANIFEST_CHROME=$(cat <<JSON
{
"name": "com.bridgeswarm",
"description": "BridgeSwarm native host (Bare / Hyperswarm)",
"path": "${HOST_BIN}",
"type": "stdio",
"allowed_origins": ["chrome-extension://${CHROME_EXT_ID}/"]
}
JSON
)
MANIFEST_FIREFOX=$(cat <<JSON
{
"name": "com.bridgeswarm",
"description": "BridgeSwarm native host (Bare / Hyperswarm)",
"path": "${HOST_BIN}",
"type": "stdio",
"allowed_extensions": ["${FIREFOX_EXT_ID}"]
}
JSON
)
if [[ "$PLATFORM" == "darwin" ]]; then
CHROME_DIRS=(
"$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
"$HOME/Library/Application Support/Chromium/NativeMessagingHosts"
)
FIREFOX_DIRS=(
"$HOME/Library/Application Support/Mozilla/NativeMessagingHosts"
)
else
CHROME_DIRS=(
"$HOME/.config/google-chrome/NativeMessagingHosts"
"$HOME/.config/chromium/NativeMessagingHosts"
)
FIREFOX_DIRS=(
"$HOME/.mozilla/native-messaging-hosts"
)
fi
for dir in "${CHROME_DIRS[@]}"; do
mkdir -p "$dir"
echo "$MANIFEST_CHROME" > "${dir}/${MANIFEST_NAME}.json"
echo " Wrote: ${dir}/${MANIFEST_NAME}.json"
done
for dir in "${FIREFOX_DIRS[@]}"; do
mkdir -p "$dir"
echo "$MANIFEST_FIREFOX" > "${dir}/${MANIFEST_NAME}.json"
echo " Wrote: ${dir}/${MANIFEST_NAME}.json"
done
# ── Done ───────────────────────────────────────────────────────────────────────
echo ""
echo "Done."
echo "====================="
echo "Installation complete!"
echo ""
echo "Next steps:"
echo ""
echo " Chrome / Edge:"
echo " 1. Open chrome://extensions"
echo " 2. Enable Developer mode"
echo " 3. Drag & drop ${DOWNLOADS}/${EXT_ZIP} onto the page"
echo " (or Load unpacked after extracting)"
echo " 4. Extension ID should be: ${CHROME_EXT_ID}"
echo ""
echo " Firefox (regular):"
echo " 1. Open about:debugging → This Firefox"
echo " 2. Load Temporary Add-on… → select ${DOWNLOADS}/${EXT_ZIP}"
echo ""
echo " Firefox Developer Edition / Nightly (permanent):"
echo " 1. about:config → xpinstall.signatures.required = false"
echo " 2. about:addons → gear → Install Add-on From File → ${DOWNLOADS}/${EXT_XPI}"
echo ""
echo " Then restart your browser."
echo ""
echo " Install dir: ${INSTALL_DIR}"
echo " Update later: run the same install command again"
echo ""
echo "Next step: In the browser tab that opened, click 'Load unpacked' and paste this path:"
echo " $EXT_DIR"
echo "(Path is in your clipboard.) Then restart the browser."
+16 -12
View File
@@ -1,13 +1,17 @@
#!/usr/bin/env node
'use strict';
var path = require('path');
var spawn = require('child_process').spawn;
var isWin = process.platform === 'win32';
var script = path.join(__dirname, isWin ? 'install.ps1' : 'install.sh');
var child = spawn(isWin ? 'powershell' : '/bin/sh', isWin ? ['-ExecutionPolicy', 'Bypass', '-File', script] : [script], {
stdio: 'inherit',
cwd: path.join(__dirname, '..'),
});
child.on('exit', function (code, sig) {
process.exit(code !== null ? code : sig ? 1 : 0);
});
/**
* Cross-platform local setup for a git clone (from-source).
* End users should use the release installers (install.sh / install.ps1).
*/
const { spawnSync } = require('child_process');
const path = require('path');
const isWin = process.platform === 'win32';
const script = isWin ? 'install-from-source.ps1' : 'install-from-source.sh';
const scriptPath = path.join(__dirname, script);
const result = isWin
? spawnSync('powershell', ['-ExecutionPolicy', 'Bypass', '-File', scriptPath], { stdio: 'inherit' })
: spawnSync('bash', [scriptPath], { stdio: 'inherit' });
process.exit(result.status == null ? 1 : result.status);
Regular → Executable
+4 -190
View File
@@ -1,193 +1,7 @@
#!/usr/bin/env bash
# BridgeSwarm — Web Installer (macOS / Linux)
# Downloads the native host binary and extension from the latest Gitea release.
#
# Usage:
# curl -fsSL https://ssh.surf/bridgeswarm/install.sh -o install.sh && bash install.sh
#
set -euo pipefail
echo ""
echo "╔══════════════════════════════════════════════════════════════════╗"
echo "║ 🌉 BridgeSwarm Installer ║"
echo "╚══════════════════════════════════════════════════════════════════╝"
echo ""
cat << 'EOF'
This installer will set up BridgeSwarm on your computer:
• Native Messaging Host → Enables browser ↔ network communication
• Browser Extension → Packaged to ~/Downloads for manual install
• Fixed Extension ID → No random IDs, stable across updates
What's installed where:
• Software: ~/.bridgeswarm/
• Extension: ~/Downloads/BridgeSwarm-*.zip (Chrome) / *.xpi (Firefox)
• Browser manifests: ~/.config/.../NativeMessagingHosts/ (Linux)
~/Library/Application Support/.../NativeMessagingHosts/ (macOS)
%LOCALAPPDATA%/bridge-swarm/ (Windows)
EOF
echo ""
if [ -t 1 ]; then
echo -n "Continue with installation? [y/N]: "
read -r answer
if [[ "${answer,,}" != "y" && "${answer,,}" != "yes" ]]; then
echo "Installation cancelled."
exit 0
fi
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# 1. Install Node.js if missing
if ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1; then
echo "📦 Installing Node.js..."
if [[ "$OSTYPE" == "darwin"* ]]; then
command -v brew >/dev/null 2>&1 || /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install node
elif grep -qiE 'debian|ubuntu' /etc/os-release 2>/dev/null; then
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get update && sudo apt-get install -y nodejs
elif grep -qi fedora /etc/os-release 2>/dev/null || command -v dnf >/dev/null; then
sudo dnf install -y nodejs
else
echo "⚠️ Please install Node.js manually from https://nodejs.org"
exit 1
fi
fi
# 2. git
if ! command -v git >/dev/null 2>&1; then
echo "📦 Installing git..."
[[ "$OSTYPE" == "darwin"* ]] && brew install git || sudo apt install -y git || sudo dnf install -y git || true
fi
# 3. Clone / update to persistent location
INSTALL_DIR="$HOME/.bridgeswarm"
mkdir -p "$INSTALL_DIR"
cd "$INSTALL_DIR" || exit 1
if [ -d ".git" ]; then
echo "📥 Updating existing installation..."
git pull --ff-only origin main || { cd ..; rm -rf "$INSTALL_DIR"; git clone https://git.ssh.surf/snxraven/BridgeSwarm.git "$INSTALL_DIR"; cd "$INSTALL_DIR"; }
else
echo "📥 Cloning BridgeSwarm..."
git clone https://git.ssh.surf/snxraven/BridgeSwarm.git .
fi
# 4. Build
echo "🔨 Building bundles and codegen..."
npm ci --no-audit --prefer-offline --no-fund
cd native-host && npm ci --no-audit --prefer-offline --no-fund || npm install --no-audit --no-fund
cd ..
npm run build
# 5. Package the extension
echo "📦 Packaging extension..."
npm run pack
# 6. Copy to ~/Downloads
DOWNLOADS_DIR="$HOME/Downloads"
mkdir -p "$DOWNLOADS_DIR"
ZIP_FILE=$(ls releases/BridgeSwarm-*.zip 2>/dev/null | head -1)
XPI_FILE=$(ls releases/BridgeSwarm-*.xpi 2>/dev/null | head -1)
if [ -n "$ZIP_FILE" ]; then
cp "$ZIP_FILE" "$DOWNLOADS_DIR/"
echo "✅ Extension saved to ~/Downloads/$(basename "$ZIP_FILE")"
fi
if [ -n "$XPI_FILE" ]; then
cp "$XPI_FILE" "$DOWNLOADS_DIR/"
echo "✅ Firefox extension saved to ~/Downloads/$(basename "$XPI_FILE")"
fi
# 7. Install native host + manifest
echo "🔧 Installing native messaging host..."
HOST_WRAPPER="$INSTALL_DIR/native-host/bridge-swarm-host"
chmod +x "$HOST_WRAPPER" 2>/dev/null || true
MANIFEST_NAME="com.bridgeswarm"
MANIFEST_TEMPLATE="$INSTALL_DIR/com.bridgeswarm.json"
MANIFEST_CONTENT=$(sed "s|ABSOLUTE_PATH_TO_NATIVE_HOST|$HOST_WRAPPER|g" "$MANIFEST_TEMPLATE")
CHROME_DIRS=(
"$HOME/.config/google-chrome/NativeMessagingHosts"
"$HOME/.config/chromium/NativeMessagingHosts"
"$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts"
"$HOME/Library/Application Support/Chromium/NativeMessagingHosts"
)
FIREFOX_DIR="$HOME/.mozilla/native-messaging-hosts"
for dir in "${CHROME_DIRS[@]}" "$FIREFOX_DIR"; do
mkdir -p "$dir" 2>/dev/null
echo "$MANIFEST_CONTENT" > "$dir/$MANIFEST_NAME.json"
echo " ✅ Installed: $dir/$MANIFEST_NAME.json"
done
# 8. Try open browser
echo ""
echo "🌐 Opening browser extension page..."
open_page() {
local url="$1" app="$2"
if [[ "$OSTYPE" == "darwin"* ]]; then
open -a "$app" "$url" 2>/dev/null && return 0
fi
command -v xdg-open >/dev/null 2>&1 && xdg-open "$url" 2>/dev/null && return 0
return 1
}
open_page "chrome://extensions" "Google Chrome" || \
open_page "chrome://extensions" "Chromium" || \
open_page "chrome://extensions" "Microsoft Edge" || {
echo "⚠️ Could not auto-open browser. Please open manually:"
echo " Chrome/Edge: chrome://extensions/"
echo " Firefox: about:addons"
}
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " ✅ Installation Complete!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
cat << INSTALL_EOF
📋 NEXT STEPS - Install the Extension
Chrome / Edge:
1. In the browser that opened, enable "Developer mode" (top right)
2. Click "Unpack extension"
3. Extract ~/Downloads/BridgeSwarm-1.0.0.zip to a folder
4. Click "Load unpacked" and select the extracted folder
5. Your extension ID should be: fmcenppcipeikpnpopolicnllljclmmi
Firefox:
1. Go to about:addons
2. Click the gear icon → "Install Add-on From File"
3. Select ~/Downloads/BridgeSwarm-1.0.0.xpi
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🧪 Test it:
• Open: $INSTALL_DIR/examples/chat/index.html
• In browser console: BridgeSwarm → should show the constructor
📁 Files:
• Extension: ~/Downloads/BridgeSwarm-1.0.0.zip
• Software: ~/.bridgeswarm/
🔄 To update later: run the same install command again
INSTALL_EOF
echo ""
echo "📍 Extension file path copied to clipboard!"
echo "$DOWNLOADS_DIR/BridgeSwarm-1.0.0.zip" | pbcopy 2>/dev/null || \
echo "$DOWNLOADS_DIR/BridgeSwarm-1.0.0.zip" | xclip -sel clip 2>/dev/null || \
echo "$DOWNLOADS_DIR/BridgeSwarm-1.0.0.zip" | xsel -ib 2>/dev/null || true
# curl -fsSL https://git.ssh.surf/snxraven/BridgeSwarm/raw/branch/main/scripts/web-installer.sh | bash
exec bash <(curl -fsSL https://git.ssh.surf/snxraven/BridgeSwarm/raw/branch/main/scripts/install.sh)
@@ -0,0 +1,517 @@
# Native Messaging: Finally Bringing P2P to the Modern Browser
## The Problem That Wouldn't Leave Me Alone
For years, I'd come back to the same frustrating question: why can't we just run peer-to-peer networking in a browser? Not WebRTC with its signaling server requirements, not WebSockets that are really just TCP wrapped in HTTP, but real honest-to-goodness P2P where two browsers connect directly to each other without anyone in the middle.
The browser sandbox exists for good reasons. If any website could open raw sockets, bind to ports, participate in UDP protocols, the security implications would be enormous. Malicious sites could run scanning tools, bypass firewalls, create botnets. The sandbox protects users from themselves and from attackers. This isn't a bug in browser design, it's a feature that's kept the web usable for decades.
But the restriction creates an enormous gap. Want to build a chat application where messages go directly between users? You can't. Want to create a collaborative editing tool without a central server? You can't. Want to make a file sharing app that doesn't require uploading to some cloud service first? You can't. Every real-time web application that's ever been built follows the same client-server pattern: all data flows through a server that you have to deploy, maintain, scale, and pay for.
I'd watched Hyperswarm emerge and mature. It was solving the hard P2P problems: distributed hash tables for discovery, UDP hole-punching for NAT traversal, the Noise protocol for encryption. People were building incredible decentralized applications with it. But they were all running in Node.js or Bun or some other server-side runtime. The browser remained locked out.
This bothered me more than it probably should have.
## Finding the Way In
Chrome extensions have this feature called native messaging. It's been around since the early days, but the official documentation focuses on mundane use cases: integrating with password managers, connecting to desktop notification systems, that sort of thing. The technical capability underneath is much more powerful.
Native messaging lets an extension spawn a process outside the browser and communicate with it through standard input and standard output. The browser handles spawning the process, manages its lifecycle, and provides a clean message-passing interface. Nothing fancy, but effective.
The crucial realization hit me like a freight train: the native process can run whatever code it wants. JavaScript in the browser can't do P2P networking, but JavaScript running in a native process outside the browser can do absolutely anything. The extension becomes a bridge between the privileged world inside the browser and the powerful world outside.
```
┌─────────────────────────────────────────────────────────────────┐
│ Chrome Browser │
│ │
│ ┌─────────────┐ ┌─────────────────────────────────┐ │
│ │ Web Page │ │ Chrome Extension │ │
│ │ │ │ │ │
│ │ window.Bridge│◄──────►│ content.js ──► background.js │ │
│ │ Swarm │ │ (service worker) │ │
│ └─────────────┘ └──────────────┬──────────────────┘ │
│ │ │
└──────────────────────────────────────────│──────────────────────┘
│ native messaging
│ (stdin/stdout)
┌─────────────────────────────────────────────────────────────────┐
│ Native Host (Bare/Node.js) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────┐ │
│ │ Hyperswarm │ │ Hyperbee │ │ Hyperdrive │ │
│ │ (P2P DHT) │ │ (key/value) │ │ (file system) │ │
│ └──────────────┘ └──────────────┘ └────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────┐ │
│ │ Hypercore │ │ Autobase │ │ Hyperdb │ │
│ │ (append-log) │ │(multi-writer)│ │ (database) │ │
│ └──────────────┘ └──────────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
The native host runs Hyperswarm, which handles all the peer discovery through the distributed hash table. It manages the NAT traversal magic that lets connections work behind home routers and corporate firewalls. It performs the Noise protocol handshake to establish encrypted sessions. It runs all the data storage systems that Hyperswarm supports. Nothing P2P happens without the native host being involved.
The extension acts as the intermediary. The service worker maintains the persistent connection to the native host, handles routing messages between different tabs, tracks which swarm belongs to which tab, manages request-response pairs so asynchronous operations return to their correct callers, and deals with disconnections and reconnections. The content script that gets injected into web pages serves as the bridge between the page's JavaScript context and the extension's privileged context.
The injected API that developers use is the final piece. It creates the BridgeSwarm class, handles all the JavaScript-side event emission, and manages communication with the extension through postMessage. The API is intentionally clean and simple, something that feels familiar to anyone who's used a networking library before.
## The Runtime: Why Bare Matters
The native host runs on Bare, a minimal JavaScript runtime that's dramatically smaller than Node.js. This was a deliberate choice for several reasons that affect both the security model and the practical deployment of BridgeSwarm.
Traditional Node.js clocked in at around sixty megabytes when you counted the runtime, its standard library, and all the dependencies needed to run even a simple application. For a tool that's meant to be installed on end-user machines, that's uncomfortably large. The installation process becomes complicated, updates are slow, and users reasonably question why they need to install an entire development environment just to run a browser extension.
Bare takes a fundamentally different approach. Instead of bundling everything you might possibly need, Bare provides only the essential primitives: process management, file system access, the network APIs, and a module loader. The entire runtime is under three megabytes. It starts instantly. It has almost no attack surface compared to the sprawling Node.js codebase. When you install BridgeSwarm, you're installing a tiny runtime that does exactly what it needs to do and nothing more.
The module loading system in Bare deserves special attention. It uses a hyperloader-based system that can load modules from various sources, including npm packages. This means we can use the same packages that work in Node.js, which gave us access to the entire Hyperswarm ecosystem without modification. The Hyperbee key-value store, Hyperdrive file system, Hypercore append-only log, Autobase multi-writer log, and Hyperdb database system all work identically in Bare as they do in Node.js. We get full compatibility with the established P2P stack without sacrificing the lightweight deployment that Bare enables.
Using Bare also simplifies the dependency story. The native host declares its dependencies in a package.json, Bare resolves those dependencies, and everything just works. There's no need to bundle, tree-shake, or compile anything. The installation script pulls down Bare if it's not present, resolves the package dependencies, and you're ready to go. This makes the installer dramatically simpler than it would be with a bundled Node.js solution.
The trade-off is that some Node.js APIs aren't available in Bare. If you need something from the extensive Node.js standard library, you might need to find an alternative package or implement it yourself. For BridgeSwarm's purposes, every module we need was either built for universal JavaScript or had a compatible alternative available. We never hit a situation where the Bare choice prevented us from doing something we needed to do.
One of the most compelling reasons to use Bare is that it runs consistently across platforms. The same JavaScript code that works on macOS works on Linux and Windows without modification. The native host doesn't care about your operating system, it just needs somewhere to run JavaScript. This makes the installation process universal rather than requiring different packages for different platforms.
### The Modules That Make It Work
The native host leverages several interconnected modules from the Hyper ecosystem to provide complete P2P functionality.
Hyperswarm is the networking layer that handles peer discovery and connection establishment. It uses a distributed hash table where peers announce their interest in specific topics. When your application calls join on a topic, Hyperswarm announces to the DHT that you're interested in that topic. Other peers doing the same will be discovered, and Hyperswarm attempts to establish direct connections. This discovery mechanism is entirely decentralized with no central server required.
Corestore provides the storage foundation that the other data modules build upon. It's essentially a system for managing multiple Hypercore instances, each with their own cryptographic key. When you need to store data in Hyperbee or Hyperdrive, Corestore creates and manages the underlying Hypercore that those systems use. It handles the key management so you don't have to think about it.
Hypercore is an append-only log, similar to a blockchain but without the proof-of-work consensus. Data is added in sequence, cryptographically linked to previous entries, and can be verified by anyone with the core's public key. It's the fundamental data structure that Hyperbee and Autobase build upon. For P2P applications, Hypercore provides tamper-evident logging that can be replicated between peers.
Hyperbee builds on Hypercore to provide a B-tree key-value store. Think of it like Redis but distributed and peer-to-peer. You put key-value pairs in, you get them out, and the data replicates automatically between connected peers. The B-tree structure makes lookups efficient even with millions of keys.
Hyperdrive is a P2P file system built on Hyperbee. You can create files and directories, read and write content, and everything syncs automatically between peers who are interested in the same drive. It's like having a shared filesystem that requires no server, where anyone with the drive key can read and write.
Autobase is a multi-writer version of Hypercore. Regular Hypercore has a single writer, but Autobase allows multiple peers to append to the same log while maintaining a consistent ordering through a linearization mechanism. This is crucial for collaborative applications where multiple users might make changes simultaneously.
Hyperdb adds schema and query capabilities on top of these primitives. Rather than just storing raw key-value pairs, you define collections with specific fields. It handles the complexity of replication, conflict resolution, and querying so you can work with a familiar database-like interface while the P2P magic happens underneath.
## How Messages Actually Flow Through the System
The communication between extension and host uses Chrome's native messaging protocol, which itself is beautifully simple. Each message gets serialized as JSON, prefixed with a four-byte little-endian integer indicating the message length, then written to standard output. The receiving side reads the first four bytes to figure out how many more bytes to read, parses the JSON, and processes it.
```javascript
// What the wire format looks like:
// [4 bytes: length][N bytes: JSON]
// Sending a message from host to extension:
const message = JSON.stringify({ type: 'event', event: 'connection', payload: {...} });
const length = Buffer.alloc(4);
length.writeUInt32LE(message.length);
process.stdout.write(Buffer.concat([length, Buffer.from(message)]));
```
This is remarkably similar to how many other protocols work under the hood, but it gets the job done reliably and is easy to debug when things go wrong. Binary data gets base64-encoded in the JSON payload, which adds some overhead but keeps the implementation simple.
Here's what actually happens when your web page calls `swarm.join("my-topic")`:
```mermaid
sequenceDiagram
participant Page as Web Page (api.js)
participant Content as Content Script
participant Background as Service Worker
participant Host as Native Host
Page->>Content: postMessage({ type: 'bridge-swarm-bridge', payload })
Content->>Background: chrome.runtime.sendMessage()
Background->>Host: port.postMessage({ id: 'req_123', type: 'join', payload })
Note over Host: Hyperswarm.join(topic)<br/>Announces to DHT<br/>Begins peer discovery
Host->>Background: messenger.send({ id: 'req_123', type: 'response', payload: { ok: true } })
Background->>Content: sendResponse(payload)
Content->>Page: dispatchEvent('bridge-swarm-bridge-response')
Page->>Page: resolve Promise, user gets control back
```
The request ID tracking is crucial. When the web page makes a request, it doesn't know how long it will take. The background service worker assigns a unique ID, stores the resolve and reject functions in a Map, sends the message to the host, and then returns control to the page immediately. When the response comes back with that same ID, the background looks up the stored functions and resolves or rejects the promise. This makes the API feel synchronous to the developer even though there's a tremendous amount of asynchronous machinery happening underneath.
Now let's trace what happens when a peer connects:
```mermaid
sequenceDiagram
participant Host as Native Host
participant Background as Service Worker
participant Content as Content Script
participant Page as Web Page
Note over Host: Hyperswarm discovers peer<br/>Noise handshake<br/>Connection established
Host->>Background: messenger.send({ type: 'event', event: 'connection', payload: { connId, swarmId, peerInfo } })
Note over Background: Look up which tab owns swarmId<br/>Send only to that tab
Background->>Content: tabs.sendMessage(tabId, { type: 'bridge-swarm-event', payload })
Content->>Page: window.dispatchEvent('bridge-swarm-event')
Note over Page: api.js receives event<br/>Creates BridgeSwarmConnection<br/>Emits 'connection' event
Page->>Page: user callback fires with (conn, peerInfo)
```
The routing logic here is critical. Multiple tabs might each have their own BridgeSwarm instances with different swarm IDs. When the native host emits a connection event, the background needs to figure out which tab should receive it. Every time a BridgeSwarm instance initializes, it registers its swarm ID with the background. The background maintains a mapping of swarm IDs to tab IDs, so when events arrive, it can look up the correct destination.
## The Code Behind It All
Let's look at what this actually looks like in practice. Here's how the native host handles incoming requests:
```javascript
// native-host/host.js - simplified
const commands = {
init: async ({ swarmId, options }) => {
const swarm = new Hyperswarm(options);
swarm.listen(); // Accept incoming connections
swarms.set(swarmId, swarm);
// Track connections for this swarm
swarm.on('connection', (socket, peerInfo) => {
const connId = `conn_${Date.now()}_${Math.random().toString(36).slice(2,8)}`;
connections.set(connId, { socket, swarmId, peerInfo });
// Emit event back to extension
emit('connection', { connId, swarmId, peerInfo: serializePeerInfo(peerInfo) });
// Handle incoming data on this connection
socket.on('data', (data) => {
emit('data', { connId, swarmId, data: data.toString('base64') });
});
socket.on('end', () => {
connections.delete(connId);
emit('end', { connId, swarmId });
});
});
return { ok: true };
},
join: async ({ swarmId, topic }) => {
const swarm = swarms.get(swarmId);
const topicBuffer = Buffer.from(topic.padEnd(32, '\0')).slice(0, 32);
swarm.join(topicBuffer);
return { ok: true };
},
write: async ({ connId, data }) => {
const conn = connections.get(connId);
if (!conn) return { error: 'Connection not found' };
const buffer = Buffer.from(data, 'base64');
conn.socket.write(buffer);
return { ok: true };
},
destroy: async ({ swarmId }) => {
const swarm = swarms.get(swarmId);
if (swarm) {
swarm.destroy();
swarms.delete(swarmId);
}
return { ok: true };
}
};
```
The host maintains several key mappings. The `swarms` Map tracks all active Hyperswarm instances by their swarm ID. The `connections` Map tracks all active P2P connections, keyed by a unique connection ID. There's also a mapping from swarm IDs to connection IDs so the host knows which connections belong to which swarm.
Now here's what the web page API looks like from the developer's perspective:
```javascript
// This is all you need to write as a developer
// Wait for the API to be ready
await BridgeSwarm.ready();
// Create a swarm instance
const swarm = new BridgeSwarm({ appName: 'my-chat-app' });
// Join a topic - this is how peers find each other
await swarm.join('some-topic-name');
// Handle incoming peer connections
swarm.on('connection', (conn, peerInfo) => {
console.log('New peer connected:', peerInfo.publicKey.slice(0, 8) + '...');
// Handle incoming data
conn.on('data', (data) => {
const message = new TextDecoder().decode(data);
console.log('Received:', message);
});
// Send data to this peer
conn.write(new TextEncoder().encode('Hello, peer!'));
});
// Broadcast to all connected peers
for (const conn of swarm.connections()) {
conn.write(new TextEncoder().encode('Hello, everyone!'));
}
// Clean up when done
await swarm.leave('some-topic-name');
swarm.destroy();
```
That ten lines of code does an enormous amount of work under the hood. It initializes a Hyperswarm instance, generates a cryptographic key pair, joins a distributed hash table topic, discovers other peers interested in the same topic, establishes encrypted connections to each peer, handles NAT traversal, manages connection lifecycle, and emits events when things happen.
## The Data Storage Layer
Beyond just peer-to-peer networking, BridgeSwarm exposes the full Hyperswarm data stack to web pages. This means you can build applications that share not just messages but actual data structures.
```javascript
// Hyperbee - key/value store like a P2P Redis
await BridgeSwarm.request('beePut', { key: 'username', value: 'alice' });
const result = await BridgeSwarm.request('beeGet', { key: 'username' });
console.log(result.value); // 'alice'
// Hyperdrive - P2P file system
const fileContent = 'Hello, world!';
const base64 = btoa(fileContent);
await BridgeSwarm.request('drivePut', { path: '/readme.txt', base64 });
const file = await BridgeSwarm.request('driveGet', { path: '/readme.txt' });
// Hyperdb - schema-based database
await BridgeSwarm.request('hyperdbInsert', {
collection: 'users',
doc: { id: 'user1', name: 'Alice', email: 'alice@example.com' }
});
const user = await BridgeSwarm.request('hyperdbGet', {
collection: 'users',
query: { id: 'user1' }
});
```
These data operations flow through the same message-passing infrastructure as everything else. The native host manages the storage, handles replication between peers if desired, and returns results to the web page.
## The Troubles I Faced
Building this system taught me a lot about the gap between what should work in theory and what actually works in practice.
The event routing problem consumed weeks. I'd get connections appearing in the wrong tab, messages going to tabs that had already closed, duplicate events, missing events. The root cause was that I wasn't properly tracking which swarm IDs belonged to which tab IDs. The fix involved explicit registration: when a BridgeSwarm instance initializes, it sends a register message to the background that includes its swarm ID. The background maintains a Map where swarm IDs map to Sets of tab IDs. Events get filtered at both the background level (which tabs should receive this) and the api.js level (is this event for this specific swarm instance).
```javascript
// The registration logic in background.js
const tabSwarms = new Map(); // tabId -> Set<swarmId>
const swarmRefCount = new Map(); // swarmId -> reference count
async function handleRegisterSwarm(message, tabId) {
const { swarmId } = message.payload;
if (!tabSwarms.has(tabId)) {
tabSwarms.set(tabId, new Set());
}
tabSwarms.get(tabId).add(swarmId);
// Track reference count for cleanup
const count = swarmRefCount.get(swarmId) || 0;
swarmRefCount.set(swarmId, count + 1);
}
```
File descriptor locking in Hyperdrive gave me endless headaches. Hyperdrive tries to acquire an exclusive lock on its storage file to prevent corruption from concurrent access. But when multiple rapid operations happen, sometimes the lock fails or conflicts with another operation. The error message "File descriptor could not be locked" became far too familiar. I ended up implementing fallback behavior where files get sent directly over the P2P connection rather than being stored in Hyperdrive first. The sending peer keeps the file in memory and transmits it when the receiving peer requests it. This actually turned out to be more reliable for the direct peer-to-peer use case anyway.
Syntax highlighting in the chat application was absurdly difficult to get right. The marked library for parsing markdown, highlight.js for syntax coloring, and the marked-highlight extension that should connect them all all load from external CDNs at different times. The initialization order was never consistent, and I'd frequently get errors about functions not existing yet. I eventually added polling logic that retries until the libraries are available, along with a fallback that applies highlighting after the HTML is already in the DOM. It's not elegant, but it works.
One subtle issue was how each tab got its identity. Hyperswarm generates a cryptographic key pair when you create the swarm, and you can't change it later. Initially, all tabs using the same application would end up with identical public keys, making it impossible to tell them apart. The solution was generating a unique swarm ID for each BridgeSwarm instance, which creates a separate Hyperswarm with its own independent key pair. Each tab now has its own distinct identity while still being able to discover and communicate with other tabs on the same topic.
## Example Application: P2P Chat
Here's a complete working chat application that demonstrates the system in action. This is essentially what's in the chat-advanced example in the repository.
```javascript
// Complete P2P chat in about 60 lines
const state = {
swarm: null,
connections: new Map(),
nickname: 'Anonymous',
publicKey: ''
};
// Initialize when user clicks Join
async function joinChat(topic, nickname) {
state.nickname = nickname;
// Create the swarm
state.swarm = new window.BridgeSwarm({ appName: 'chat-app' });
// Listen for connections
state.swarm.on('connection', handleConnection);
// Join the topic to discover peers
await state.swarm.join(topic);
// Get our public key
state.publicKey = await state.swarm.getPublicKey();
// Broadcast our presence
broadcastPresence();
}
function handleConnection(conn, peerInfo) {
const connId = Date.now() + '-' + Math.random().toString(36).slice(2, 8);
// Track this connection
state.connections.set(connId, { conn, peerInfo, nickname: 'Unknown' });
// Send handshake
conn.write(JSON.stringify({
type: 'handshake',
nickname: state.nickname
}));
// Handle incoming data
conn.on('data', (data) => {
const msg = JSON.parse(new TextDecoder().decode(data));
switch (msg.type) {
case 'handshake':
// Update peer's nickname
const connObj = state.connections.get(connId);
if (connObj) connObj.nickname = msg.nickname;
broadcastPresence();
break;
case 'chat':
displayMessage(msg.author, msg.content, msg.timestamp);
break;
}
});
// Handle disconnect
conn.on('end', () => {
state.connections.delete(connId);
broadcastPresence();
});
}
function sendMessage(text) {
const msg = {
type: 'chat',
author: state.nickname,
content: text,
timestamp: Date.now()
};
// Send to all connected peers
for (const [, conn] of state.connections) {
conn.conn.write(JSON.stringify(msg));
}
// Display our own message
displayMessage(state.nickname, text, msg.timestamp);
}
function broadcastPresence() {
const peers = Array.from(state.connections.values()).map(c => c.nickname);
updateUserList([state.nickname, ...peers]);
}
```
This example shows the full flow. Users join a topic, connections establish automatically, messages send directly between peers, and presence information syncs across the network. There's no server required.
## Repository and Installation
The full implementation lives at [https://git.ssh.surf/snxraven/BridgeSwarm](https://git.ssh.surf/snxraven/BridgeSwarm). You can also find it on GitHub at [https://github.com/anomalyco/BridgeSwarm](https://github.com/anomalyco/BridgeSwarm).
For the quickest start, you can run the installer directly without cloning:
```bash
curl -fsSL https://ssh.surf/bridgeswarm/install.sh -o install.sh && bash install.sh
```
This downloads BridgeSwarm, builds the native host, packages the extension, and opens your browser to load it. The installer handles all the compilation and configuration automatically.
If you prefer manual installation, clone the repository and run the setup script:
```bash
git clone https://git.ssh.surf/snxraven/BridgeSwarm.git
cd BridgeSwarm
./scripts/install.sh
```
Then load the extension by opening chrome://extensions, enabling developer mode, clicking "Load unpacked," and selecting the extension/ folder.
## What's Possible Now
With this system running in a browser, the types of applications you can build fundamentally change. A real-time chat application requires no server deployment, no database setup, no scaling configuration. Users connect directly, messages flow between them, and there's no infrastructure for you to maintain.
Collaborative document editing becomes achievable without a central coordination server. Multiple users can connect to the same topic, exchange CRDT-based updates through Hypercore replication, and see each other's changes in real-time. The data lives in the users' browsers, synchronized directly between them.
File sharing applications work without uploading files to some cloud service first. One user sends a file directly to another over the encrypted P2P connection. No intermediate server, no storage costs, no privacy concerns about uploading files to third-party services.
Gaming applications benefit from the low latency that direct connections provide. For games where latency matters less like turn-based strategy or card games the peer-to-peer model works excellently. Even real-time games can leverage client-side prediction to compensate for network delays.
The applications are bounded only by what you can imagine. Any software that traditionally requires a central server to coordinate users can potentially be rebuilt as a direct peer-to-peer application.
## Any Page, Anywhere
One of the most powerful aspects of this system is that it works on absolutely any web page, whether that page is served over HTTPS from a production server or opened directly from your local filesystem. The extension's content script gets injected into every page the browser loads, which means any website can potentially become a P2P application without requiring the website operator to run any special infrastructure.
This opens up possibilities that simply weren't feasible before. Consider the humble comment section on a blog post. Traditionally, when someone leaves a comment, it gets stored in a database on some server, and anyone else viewing that page has to request those comments from that same server. The comment system is entirely dependent on the blog's server being up, being configured correctly, and not having been compromised. With BridgeSwarm, the comments could flow directly between readers of the page. No database required, no comment server to maintain, no single point of failure.
The implementation is remarkably simple from the website operator's perspective. You include the BridgeSwarm API script on your page, the same way you might include jQuery or any other library. When a reader opens the page, the extension injects the API automatically. If that reader leaves a comment, it gets broadcast to other readers currently viewing the same page. There's no server component to deploy, no database to manage, no moderation infrastructure to maintain. The comments live in the browsers of the people reading the page.
If you want persistence beyond the current session, you have options. The simplest approach keeps comments entirely in-memory across the currently connected peers. When everyone closes their browser, the comments disappear. This works perfectly for ephemeral discussions, live streams, temporary events, or any situation where you don't need the comments to survive after everyone's left.
But there's a more powerful option if you need persistence. Hyperdb is a schema-based P2P database that's available through the same API. You can store comments in Hyperdb, and they'll persist across browser sessions. The comments get replicated between peers who are online at the same time, so even if someone wasn't viewing the page when you posted, they'll sync up when they eventually visit. There's no central database server to run, but the data survives through the distributed replication. You define a schema with collections, insert documents, run queries, all through the BridgeSwarm API. The database lives in the native host's storage, replicated across any peers who choose to participate in that particular database.
The hybrid approach is particularly interesting. You could use pure P2P for real-time discussion where comments flow directly between current viewers, while simultaneously storing everything in Hyperdb for persistence. New visitors who weren't online when a comment was posted would still receive it through the database replication. The two systems complement each other rather than being mutually exclusive.
The same approach works for live collaboration features. Think about a documentation site where multiple people are reading the same page at the same time. With BridgeSwarm, you could add features where users can see who's currently viewing the page, leave inline annotations that sync in real-time, or even have a persistent collaborative session that continues as people come and go. All of this works without any server-side component beyond serving the initial HTML and JavaScript.
This applies to essentially any embeddable widget or interactive element. A stock ticker that updates peer-to-peer between viewers of the same page. A live sports scoreboard that syncs without a backend. A polling widget where votes broadcast directly between participants. A q&a section for a webinar that doesn't require the host to run any messaging infrastructure. If multiple people can view the same page, they can communicate directly through it.
The content script injection works through Chrome's standard content script mechanism. When you navigate to any URL, the extension's content script runs automatically unless you've disabled it in the settings. There's a configuration option to skip injection on file:// URLs if you're concerned about local testing, but by default the API is available everywhere. The first time a page uses the BridgeSwarm API, it initializes a swarm and connects to whatever topic the application specifies. From that point forward, any other user viewing the same page with the same topic will discover and connect to each other automatically.
The practical implications are significant for developers who want to add P2P features to existing websites. You don't need to convince anyone to host a WebSocket server or deploy a WebRTC signaling service. You don't need to sign up for a third-party real-time service or worry about their rate limits or pricing changes. You simply include a script tag and write your application logic. The P2P networking happens entirely between the browsers of the people using your site.
There are interesting implications for offline usage as well. If you're developing a web application that needs to work in areas with poor connectivity, the P2P model can actually improve resilience. When you have multiple users viewing the same page offline, they can share data between themselves without needing to reach a central server. The data doesn't flow through any infrastructure you control, which means you don't need to worry about server uptime, bandwidth costs, or traffic spikes.
The settings panel in the extension gives users fine-grained control over this behavior. You can disable the extension entirely when you don't want P2P functionality. You can choose to skip injection on file:// URLs if you're testing locally and don't want the behavior there. You can see the debug output if you're trying to understand why connections aren't forming. The extension respects user agency rather than silently doing things in the background.
This architecture also means that websites can be genuinely useful even with a very small audience. A P2P comment section with just two readers still works perfectly. A collaborative document edited by two people functions fine. You don't need thousands of concurrent users to justify the infrastructure cost because there is no infrastructure cost. The cost scales with zero.
## Configuration and Defaults
The extension includes a settings panel accessible by right-clicking the extension icon. These settings control how BridgeSwarm behaves by default when a page requests a swarm, allowing site operators and users to customize the behavior without requiring code changes.
The default application name gets used when a page creates a swarm without explicitly specifying one. The maximum peers setting controls how many simultaneous connections a swarm will maintain, with zero meaning unlimited. Request timeout determines how long the API waits for a response from the native host before rejecting a promise. Ready timeout controls how long BridgeSwarm.ready() waits for the API to become available.
There's also a setting that defaults to enabled which skips injection on file:// URLs. This is useful for developers who want to test their pages locally without triggering the P2P behavior. When this setting is disabled, the API gets injected into local files just like any other page, allowing full testing of P2P functionality without deploying to a server.
The notify on disconnect option shows a browser notification when the native host disconnects unexpectedly, which is helpful for debugging but can be annoying in production. Debug mode enables verbose logging to the console, which makes it much easier to understand why connections aren't forming or why messages aren't being delivered. These settings persist across browser sessions, so once you configure them the way you like, they stay that way.
## Honest Limitations
This isn't the solution for every situation. Users must have the extension installed, which introduces friction that not every project can absorb. You can't simply share a URL and expect it to work without the extension being present.
Some network configurations don't support UDP hole-punching. Symmetric NATs and certain restrictive firewalls can't be bypassed. In these cases, Hyperswarm falls back to relay servers, which reintroduces the server dependency we were trying to avoid and adds latency.
The peer-to-peer model doesn't scale efficiently to thousands of users. Broadcasting a message to ten thousand peers requires ten thousand individual connections, which is dramatically less efficient than one connection to a server that fans out efficiently. The sweet spot is groups of roughly ten to a few hundred simultaneous users.
The native host runs with the same permissions as the user who launches it. This is inherent to the native messaging architecture. Users need to trust that the extension and host they're installing are legitimate, which means the code must be open source and auditable.
## The Path Forward
Several improvements could make this system more powerful. Mobile browser support would be valuable, but Chrome's mobile extension API is significantly more limited than the desktop version. Cross-browser compatibility with Firefox works through the same native messaging mechanism, so that's already achieved.
More sophisticated example applications would help developers get started faster. A shared whiteboard, a simple game, a collaborative code editor each would demonstrate different capabilities of the system and provide templates for developers to build from.
The data layer integration could be deeper. The Hyperdb wrapper is currently minimal, exposing only basic operations. A more complete wrapper library would make building database-backed applications much more approachable. Automatic replication could be added so that applications sync their data across connected peers without explicit configuration.
Connection establishment time could be optimized. There's a noticeable delay between joining a topic and actually being connected to discovered peers. Reducing this would make the system feel significantly more responsive.
## The Core Idea
The browser was never designed for peer-to-peer networking. The sandbox, the security model, the entire architecture assumes a client-server world. But through native messaging, we've found a way to bring P2P capability to the browser without compromising the security model.
The architecture isn't elegant. There's a native process, an extension, multiple layers of message passing, routing logic, fallback behavior for edge cases. It's complex in ways that pure server-based solutions aren't. But it works, and what it enables makes the complexity worthwhile.
For years, building real-time web applications meant accepting the server as a necessary intermediary. Now there's another option. The code is there, the examples are there, the documentation is there. If you've ever wanted to build applications where users communicate directly without a server in the middle, the tools are finally available.