Fix IDs
CI / Build & Test (push) Successful in 3m4s

This commit is contained in:
Raven Scott
2026-07-26 22:02:29 -04:00
parent 914bc7e404
commit ad41323e27
11 changed files with 214 additions and 48 deletions
+1 -1
View File
@@ -62,7 +62,7 @@ This installs the host to `~/.bridgeswarm/` (or `%LOCALAPPDATA%\bridgeswarm\` on
1. Open `chrome://extensions` 1. Open `chrome://extensions`
2. Enable **Developer mode** 2. Enable **Developer mode**
3. Drag & drop `~/Downloads/BridgeSwarm-1.0.0.zip` onto the page (or Load unpacked after extracting) 3. Drag & drop `~/Downloads/BridgeSwarm-1.0.0.zip` onto the page (or Load unpacked after extracting)
4. Extension ID should be `fmcenppcipeikpnpopolicnllljclmmi` 4. Extension ID should be `jhmbaojjfkkpoolhkoohklbjokdmbdpm`
5. Restart the browser 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`). **Firefox:** temporary via `about:debugging` → Load Temporary Add-on, or permanent on Nightly/Dev Edition via Install Add-on From File (`.xpi`).
+2 -2
View File
@@ -4,9 +4,9 @@
"path": "ABSOLUTE_PATH_TO_NATIVE_HOST", "path": "ABSOLUTE_PATH_TO_NATIVE_HOST",
"type": "stdio", "type": "stdio",
"allowed_origins": [ "allowed_origins": [
"chrome-extension://fmcenppcipeikpnpopolicnllljclmmi/" "chrome-extension://jhmbaojjfkkpoolhkoohklbjokdmbdpm/"
], ],
"allowed_extensions": [ "allowed_extensions": [
"bridgeswarm-58514e5bbfe0d5e3@example.org" "bridgeswarm-2b4c4bed6fceb6de@example.org"
] ]
} }
+2 -2
View File
@@ -3,7 +3,7 @@
"name": "BridgeSwarm", "name": "BridgeSwarm",
"version": "1.0.0", "version": "1.0.0",
"description": "P2P Hyperswarm in the browser via native messaging host", "description": "P2P Hyperswarm in the browser via native messaging host",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmpSyoCuwrSnyspandk7hcQ3rTVegPWPyABcn9qX9EMleN7FIPRN81Z31IPXnbMDfXavnAnUc0vEH1ujtv/PAjew58kN4CFUTE1opU15v6dzn+ltrtG1aX+9dwYAOBsxKFU5TzfXI4SvixWzEGjJ1Irh4QEYJh4/C1A8mOdF+VvIA4y5ko7npH+7SRmCnxQ+c4Jh7gmG7qvblEHZzvM5VxT0H4gXivxD7+ABDQFnlet8ojVgyN6azND4BuPER5672vRdzjr4Emd9bL1L3N9QzQzvT7tUdFkJ2Giph9Bog9yyymnxbb8S4u0u8OlDdLBQlCWrERoYxHG9QGZse1EFWsQIDAQAB", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxbk6DH/lfwyITfu7q07GIvt29Ee1IubQpVCK6Njw+WTs3jDJkcBh6VIfRsTzc1cg8W7M5IUST8B6yyKHVk41/ydVzeNCVJCxHnUr6PZAAEkTq6+moi6z++eUumhN3nY+T8FC57clG7ffhTfeZ7kwDXQLyQkDf6uoZeJL4MQ49R8aILlrTeGSCdCbwY3JzZSRgtHj1RGLHjs+J6wMIou+RtzTEEmCbyXkyywQ8UbKDzIqOVQx1TIWAWwp/FSZtcmWmjR1Ipnp9TxaGhiS6fVLnxT3aGDqLXilKoPoKPNUVb5B0NTgzzMKmqJ1yRlw8knZ5lDoYJ5p4Rj1wR1cjyskOwIDAQAB",
"permissions": [ "permissions": [
"nativeMessaging", "nativeMessaging",
"storage", "storage",
@@ -50,7 +50,7 @@
}, },
"browser_specific_settings": { "browser_specific_settings": {
"gecko": { "gecko": {
"id": "bridgeswarm-58514e5bbfe0d5e3@example.org", "id": "bridgeswarm-2b4c4bed6fceb6de@example.org",
"strict_min_version": "79.0" "strict_min_version": "79.0"
} }
}, },
+59
View File
@@ -0,0 +1,59 @@
{
"manifest_version": 3,
"name": "BridgeSwarm",
"version": "1.0.0",
"description": "P2P Hyperswarm in the browser via native messaging host",
"permissions": [
"nativeMessaging",
"storage",
"scripting",
"notifications",
"tabs"
],
"action": {
"default_title": "BridgeSwarm Dashboard"
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"content.js"
],
"run_at": "document_start"
}
],
"web_accessible_resources": [
{
"resources": [
"api.js",
"framed-stream.js",
"protomux-bundle.js",
"defaults.js",
"dashboard.html",
"dashboard.js"
],
"matches": [
"<all_urls>"
]
}
],
"icons": {
"16": "icons/16.png",
"48": "icons/48.png",
"128": "icons/128.png"
},
"browser_specific_settings": {
"gecko": {
"id": "[email protected]",
"strict_min_version": "79.0"
}
},
"options_ui": {
"page": "options.html"
}
}
+1
View File
@@ -5,6 +5,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"setup": "node scripts/run-install.js", "setup": "node scripts/run-install.js",
"generate:extension-id": "node scripts/generate-extension-id.js",
"build:host": "node scripts/build-host.js", "build:host": "node scripts/build-host.js",
"build:protomux": "node scripts/bundle-protomux.js", "build:protomux": "node scripts/bundle-protomux.js",
"build:hrpc": "node scripts/build-hrpc.js", "build:hrpc": "node scripts/build-hrpc.js",
Regular → Executable
+27 -12
View File
@@ -1,8 +1,13 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict'; 'use strict';
/** /**
* Generates a new extension ID and updates manifest.json and com.bridgeswarm.json. * Generates a new BridgeSwarm extension ID pair and updates:
* Run on install so each copy of the template gets a unique ID (no overwrites). * - extension/manifest.json (Chrome key + gecko id)
* - extension/manifest_firefox.json (gecko id only; no key)
* - com.bridgeswarm.json (allowed_origins / allowed_extensions)
*
* Also prints the IDs so installers/docs can be synced.
* Do NOT reuse holesail-browser's Chrome key/ID — they must stay unique.
*/ */
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs'); const fs = require('fs');
@@ -10,29 +15,24 @@ const path = require('path');
const repoRoot = path.join(__dirname, '..'); const repoRoot = path.join(__dirname, '..');
const manifestPath = path.join(repoRoot, 'extension', 'manifest.json'); const manifestPath = path.join(repoRoot, 'extension', 'manifest.json');
const manifestFirefoxPath = path.join(repoRoot, 'extension', 'manifest_firefox.json');
const nativeManifestPath = path.join(repoRoot, 'com.bridgeswarm.json'); const nativeManifestPath = path.join(repoRoot, 'com.bridgeswarm.json');
// Generate RSA 2048-bit key pair
const { publicKey } = crypto.generateKeyPairSync('rsa', { const { publicKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048, modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'der' }, publicKeyEncoding: { type: 'spki', format: 'der' },
privateKeyEncoding: { type: 'pkcs8', format: 'der' } privateKeyEncoding: { type: 'pkcs8', format: 'der' },
}); });
// Chrome extension ID: first 16 bytes of SHA256(publicKey), each byte -> 2 chars a-p // Chrome extension ID: first 16 bytes of SHA256(publicKey), each nibble → ap
// (Chromium crx_id.py: HexToMPDecimal)
const hash = crypto.createHash('sha256').update(publicKey).digest(); const hash = crypto.createHash('sha256').update(publicKey).digest();
const chromeId = Array.from(hash.slice(0, 16)) const chromeId = Array.from(hash.slice(0, 16))
.map(b => String.fromCharCode(97 + (b >> 4)) + String.fromCharCode(97 + (b & 0x0f))) .map((b) => String.fromCharCode(97 + (b >> 4)) + String.fromCharCode(97 + (b & 0x0f)))
.join(''); .join('');
// Firefox ID: unique per install
const firefoxId = 'bridgeswarm-' + crypto.randomBytes(8).toString('hex') + '@example.org'; const firefoxId = 'bridgeswarm-' + crypto.randomBytes(8).toString('hex') + '@example.org';
// Base64-encode public key for manifest
const keyBase64 = publicKey.toString('base64'); const keyBase64 = publicKey.toString('base64');
// Update extension manifest
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
manifest.key = keyBase64; manifest.key = keyBase64;
if (manifest.browser_specific_settings?.gecko) { if (manifest.browser_specific_settings?.gecko) {
@@ -40,10 +40,25 @@ if (manifest.browser_specific_settings?.gecko) {
} }
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n'); fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
// Update native host manifest template // Firefox manifest: same as Chrome but without the Chrome-only "key"
if (fs.existsSync(manifestFirefoxPath)) {
const manifestFx = JSON.parse(fs.readFileSync(manifestFirefoxPath, 'utf8'));
if (manifestFx.browser_specific_settings?.gecko) {
manifestFx.browser_specific_settings.gecko.id = firefoxId;
}
delete manifestFx.key;
fs.writeFileSync(manifestFirefoxPath, JSON.stringify(manifestFx, null, 2) + '\n');
} else {
const fx = { ...manifest };
delete fx.key;
fs.writeFileSync(manifestFirefoxPath, JSON.stringify(fx, null, 2) + '\n');
}
const nativeManifest = JSON.parse(fs.readFileSync(nativeManifestPath, 'utf8')); const nativeManifest = JSON.parse(fs.readFileSync(nativeManifestPath, 'utf8'));
nativeManifest.allowed_origins = ['chrome-extension://' + chromeId + '/']; nativeManifest.allowed_origins = ['chrome-extension://' + chromeId + '/'];
nativeManifest.allowed_extensions = [firefoxId]; nativeManifest.allowed_extensions = [firefoxId];
fs.writeFileSync(nativeManifestPath, JSON.stringify(nativeManifest, null, 2) + '\n'); fs.writeFileSync(nativeManifestPath, JSON.stringify(nativeManifest, null, 2) + '\n');
console.log('Generated extension ID:', chromeId, '(' + firefoxId + ')'); console.log('Generated extension ID:', chromeId, '(' + firefoxId + ')');
console.log('CHROME_EXT_ID=' + chromeId);
console.log('FIREFOX_EXT_ID=' + firefoxId);
+4 -2
View File
@@ -10,8 +10,10 @@ $InstallDir = "$env:LOCALAPPDATA\bridgeswarm"
$Downloads = "$env:USERPROFILE\Downloads" $Downloads = "$env:USERPROFILE\Downloads"
$ManifestName = "com.bridgeswarm" $ManifestName = "com.bridgeswarm"
$ExtVersion = "1.0.0" $ExtVersion = "1.0.0"
$ChromeExtId = "fmcenppcipeikpnpopolicnllljclmmi" # Must match extension/manifest.json "key" / gecko.id and com.bridgeswarm.json
$FirefoxExtId = "bridgeswarm[email protected]" # (unique to BridgeSwarm — do not share with holesail-browser)
$ChromeExtId = "jhmbaojjfkkpoolhkoohklbjokdmbdpm"
$FirefoxExtId = "[email protected]"
$HostZip = "bridge-swarm-host-win32-x64.zip" $HostZip = "bridge-swarm-host-win32-x64.zip"
$ExtZip = "BridgeSwarm-$ExtVersion.zip" $ExtZip = "BridgeSwarm-$ExtVersion.zip"
+4 -2
View File
@@ -13,8 +13,10 @@ INSTALL_DIR="$HOME/.bridgeswarm"
MANIFEST_NAME="com.bridgeswarm" MANIFEST_NAME="com.bridgeswarm"
DOWNLOADS="$HOME/Downloads" DOWNLOADS="$HOME/Downloads"
EXT_VERSION="1.0.0" EXT_VERSION="1.0.0"
CHROME_EXT_ID="fmcenppcipeikpnpopolicnllljclmmi" # Must match extension/manifest.json "key" / gecko.id and com.bridgeswarm.json
FIREFOX_EXT_ID="bridgeswarm[email protected]" # (unique to BridgeSwarm — do not share with holesail-browser)
CHROME_EXT_ID="jhmbaojjfkkpoolhkoohklbjokdmbdpm"
FIREFOX_EXT_ID="[email protected]"
# ── Detect platform ──────────────────────────────────────────────────────────── # ── Detect platform ────────────────────────────────────────────────────────────
OS="$(uname -s | tr '[:upper:]' '[:lower:]')" OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
+39 -12
View File
@@ -1,7 +1,8 @@
/** /**
* Pack the browser extension into a zip (and .xpi) for distribution. * Pack the browser extension into a zip (Chrome) and .xpi (Firefox).
* Zip contains manifest.json (Chrome, with "key").
* XPI contains manifest_firefox.json as manifest.json (no "key").
* Output: releases/BridgeSwarm-<version>.zip, releases/BridgeSwarm-<version>.xpi * Output: releases/BridgeSwarm-<version>.zip, releases/BridgeSwarm-<version>.xpi
* Excludes: protomux-entry.cjs, *.map
*/ */
const fs = require('fs'); const fs = require('fs');
@@ -13,10 +14,8 @@ const REPO_ROOT = path.resolve(__dirname, '..');
const EXT_DIR = path.join(REPO_ROOT, 'extension'); const EXT_DIR = path.join(REPO_ROOT, 'extension');
const RELEASES_DIR = path.join(REPO_ROOT, 'releases'); const RELEASES_DIR = path.join(REPO_ROOT, 'releases');
const EXCLUDE = new Set([ const EXCLUDE_NAMES = new Set(['protomux-entry.cjs']);
'protomux-entry.cjs', const EXCLUDE_EXTS = new Set(['.map']);
'.map'
]);
function getVersion() { function getVersion() {
const manifestPath = path.join(EXT_DIR, 'manifest.json'); const manifestPath = path.join(EXT_DIR, 'manifest.json');
@@ -34,8 +33,9 @@ function listExtensionFiles(dir, base = dir) {
if (e.isDirectory()) { if (e.isDirectory()) {
files.push(...listExtensionFiles(full, base)); files.push(...listExtensionFiles(full, base));
} else { } else {
const skip = EXCLUDE.has(e.name) || [...EXCLUDE].some(x => x.startsWith('.') && rel.endsWith(x)); const ext = path.extname(e.name);
if (!skip) files.push({ full, rel }); if (EXCLUDE_NAMES.has(e.name) || EXCLUDE_EXTS.has(ext)) continue;
files.push({ full, rel });
} }
} }
return files; return files;
@@ -51,7 +51,7 @@ function createZip(version) {
archive.on('error', reject); archive.on('error', reject);
archive.pipe(output); archive.pipe(output);
const files = listExtensionFiles(EXT_DIR); const files = listExtensionFiles(EXT_DIR).filter(({ rel }) => rel !== 'manifest_firefox.json');
for (const { full, rel } of files) { for (const { full, rel } of files) {
archive.file(full, { name: rel }); archive.file(full, { name: rel });
} }
@@ -59,6 +59,32 @@ function createZip(version) {
}); });
} }
function createXpi(version) {
const firefoxManifestPath = path.join(EXT_DIR, 'manifest_firefox.json');
if (!fs.existsSync(firefoxManifestPath)) {
throw new Error('extension/manifest_firefox.json missing — run scripts/generate-extension-id.js');
}
const xpiPath = path.join(RELEASES_DIR, `BridgeSwarm-${version}.xpi`);
const output = fs.createWriteStream(xpiPath);
const archive = archiver('zip', { zlib: { level: 9 } });
return new Promise((resolve, reject) => {
output.on('close', () => resolve(xpiPath));
archive.on('error', reject);
archive.pipe(output);
const files = listExtensionFiles(EXT_DIR).filter(
({ rel }) => rel !== 'manifest.json' && rel !== 'manifest_firefox.json'
);
for (const { full, rel } of files) {
archive.file(full, { name: rel });
}
archive.file(firefoxManifestPath, { name: 'manifest.json' });
archive.finalize();
});
}
async function main() { async function main() {
const version = getVersion(); const version = getVersion();
console.log('BridgeSwarm version from manifest:', version); console.log('BridgeSwarm version from manifest:', version);
@@ -71,12 +97,13 @@ async function main() {
console.log('Created releases/'); console.log('Created releases/');
} }
console.log('Creating zip...');
try { try {
console.log('Creating Chrome zip...');
const zipPath = await createZip(version); const zipPath = await createZip(version);
console.log('Written:', zipPath); console.log('Written:', zipPath);
const xpiPath = path.join(RELEASES_DIR, `BridgeSwarm-${version}.xpi`);
fs.copyFileSync(zipPath, xpiPath); console.log('Creating Firefox xpi...');
const xpiPath = await createXpi(version);
console.log('Written:', xpiPath); console.log('Written:', xpiPath);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@@ -1,11 +1,43 @@
# Update native host manifest with your Chrome extension ID (fix "Access forbidden"). # Update native host manifest with your Chrome extension ID (fix "Access forbidden").
# Usage: .\scripts\update-native-manifest-extension-id.ps1 YOUR_EXTENSION_ID # Optionally pass a Firefox extension ID as the second argument.
param([Parameter(Mandatory=$true)] [string] $ExtensionId) # Usage:
# .\scripts\update-native-manifest-extension-id.ps1 CHROME_EXT_ID
# .\scripts\update-native-manifest-extension-id.ps1 CHROME_EXT_ID FIREFOX_EXT_ID
param(
[Parameter(Mandatory = $true)][string]$ExtensionId,
[Parameter(Mandatory = $false)][string]$FirefoxExtensionId
)
$ExtensionId = $ExtensionId -replace '^chrome-extension://', '' -replace '/$', '' $ExtensionId = $ExtensionId -replace '^chrome-extension://', '' -replace '/$', ''
$Origin = "chrome-extension://$ExtensionId/" $Origin = "chrome-extension://$ExtensionId/"
$manifestFile = (Get-ItemProperty -Path "HKCU:\Software\Google\Chrome\NativeMessagingHosts\com.bridgeswarm" -ErrorAction SilentlyContinue).'(Default)' $updated = $false
if (-not $manifestFile -or -not (Test-Path $manifestFile)) { Write-Host "Run scripts/install.ps1 first."; exit 1 }
$chromeKey = "HKCU:\Software\Google\Chrome\NativeMessagingHosts\com.bridgeswarm"
$manifestFile = (Get-ItemProperty -Path $chromeKey -ErrorAction SilentlyContinue).'(Default)'
if ($manifestFile -and (Test-Path $manifestFile)) {
$m = Get-Content $manifestFile -Raw | ConvertFrom-Json $m = Get-Content $manifestFile -Raw | ConvertFrom-Json
$m.allowed_origins = @($Origin) $m.allowed_origins = @($Origin)
$m | ConvertTo-Json -Depth 4 | Set-Content $manifestFile -Encoding UTF8 $m | ConvertTo-Json -Depth 4 | Set-Content $manifestFile -Encoding UTF8
Write-Host "Updated $manifestFile. Restart Chrome." Write-Host "Updated $manifestFile (allowed_origins)"
$updated = $true
}
if ($FirefoxExtensionId) {
$ffKey = "HKCU:\Software\Mozilla\NativeMessagingHosts\com.bridgeswarm"
$ffManifest = (Get-ItemProperty -Path $ffKey -ErrorAction SilentlyContinue).'(Default)'
if ($ffManifest -and (Test-Path $ffManifest)) {
$m = Get-Content $ffManifest -Raw | ConvertFrom-Json
$m.allowed_extensions = @($FirefoxExtensionId)
$m | ConvertTo-Json -Depth 4 | Set-Content $ffManifest -Encoding UTF8
Write-Host "Updated $ffManifest (allowed_extensions)"
$updated = $true
} else {
Write-Host "Note: Firefox manifest not found — skipping Firefox update"
}
}
if (-not $updated) {
Write-Host "No manifest found. Run scripts/install.ps1 first."
exit 1
}
Write-Host "Done. Restart your browser."
+34 -6
View File
@@ -1,15 +1,27 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Update native host manifest with your Chrome extension ID (fix "Access forbidden"). # Update native host manifest with your Chrome extension ID (fix "Access forbidden").
# Usage: ./scripts/update-native-manifest-extension-id.sh YOUR_EXTENSION_ID # Also accepts a Firefox extension ID as a second argument to update allowed_extensions.
# Usage: ./scripts/update-native-manifest-extension-id.sh CHROME_EXT_ID [FIREFOX_EXT_ID]
set -e set -e
[[ -z "$1" ]] && echo "Usage: $0 YOUR_CHROME_EXTENSION_ID (from chrome://extensions)" && exit 1 [[ -z "$1" ]] && echo "Usage: $0 CHROME_EXT_ID [FIREFOX_EXT_ID]" && exit 1
EXT_ID="${1#chrome-extension://}"; EXT_ID="${EXT_ID%/}" EXT_ID="${1#chrome-extension://}"; EXT_ID="${EXT_ID%/}"
ORIGIN="chrome-extension://${EXT_ID}/" ORIGIN="chrome-extension://${EXT_ID}/"
FIREFOX_EXT_ID="${2:-}"
MANIFEST_NAME="com.bridgeswarm"
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"
else
CHROME_DIR="$HOME/.config/google-chrome/NativeMessagingHosts" CHROME_DIR="$HOME/.config/google-chrome/NativeMessagingHosts"
CHROMIUM_DIR="$HOME/.config/chromium/NativeMessagingHosts" CHROMIUM_DIR="$HOME/.config/chromium/NativeMessagingHosts"
[[ "$OSTYPE" == "darwin"* ]] && CHROME_DIR="$HOME/Library/Application Support/Google/Chrome/NativeMessagingHosts" && CHROMIUM_DIR="$HOME/Library/Application Support/Chromium/NativeMessagingHosts" FIREFOX_DIR="$HOME/.mozilla/native-messaging-hosts"
MANIFEST_NAME="com.bridgeswarm" fi
updated=0 updated=0
for dir in "$CHROME_DIR" "$CHROMIUM_DIR"; do for dir in "$CHROME_DIR" "$CHROMIUM_DIR"; do
f="$dir/${MANIFEST_NAME}.json" f="$dir/${MANIFEST_NAME}.json"
[[ ! -f "$f" ]] && continue [[ ! -f "$f" ]] && continue
@@ -18,8 +30,24 @@ for dir in "$CHROME_DIR" "$CHROMIUM_DIR"; do
else else
sed -i.bak "s|\"chrome-extension://[^\"]*/\"|\"$ORIGIN\"|g" "$f" && rm -f "${f}.bak" sed -i.bak "s|\"chrome-extension://[^\"]*/\"|\"$ORIGIN\"|g" "$f" && rm -f "${f}.bak"
fi fi
echo "Updated $f" echo "Updated $f (allowed_origins)"
updated=1 updated=1
done done
if [[ -n "$FIREFOX_EXT_ID" ]]; then
f="$FIREFOX_DIR/${MANIFEST_NAME}.json"
if [[ -f "$f" ]]; then
if command -v node >/dev/null 2>&1; then
node -e "const fs=require('fs');const j=JSON.parse(fs.readFileSync('$f','utf8'));j.allowed_extensions=['$FIREFOX_EXT_ID'];fs.writeFileSync('$f',JSON.stringify(j,null,2));"
else
sed -i.bak "s|\"bridgeswarm[^\"]*@[^\"]*\"|\"$FIREFOX_EXT_ID\"|g" "$f" && rm -f "${f}.bak"
fi
echo "Updated $f (allowed_extensions)"
updated=1
else
echo "Note: Firefox manifest not found at $f — skipping Firefox update"
fi
fi
[[ $updated -eq 0 ]] && echo "No manifest found. Run scripts/install.sh first." && exit 1 [[ $updated -eq 0 ]] && echo "No manifest found. Run scripts/install.sh first." && exit 1
echo "Done. Restart Chrome." echo "Done. Restart your browser."