Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions extensions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# BearBrowser Bundled Extensions

Two first-party extensions ship bundled with every BearBrowser build.
They are force-installed at first launch via enterprise policy — no manual
installation required.

---

## Bear Spaces (`bear-spaces@bearbrowser.local`)

**Tab workspaces for BearBrowser.** Organises open tabs into named, persistent
workspaces (similar to Arc Spaces). Each workspace maintains its own tab set;
switching workspaces hides the current set and reveals the selected one without
closing any tabs.

Permissions: `tabs`, `tabHide`, `storage`, `contextMenus`, `nativeMessaging`.

---

## Bear Containers (`bear-containers@mdheller`)

**Multi-account container isolation.** Each container gets its own cookie jar,
localStorage, IndexedDB, and network cache, preventing cross-site tracking and
enabling simultaneous sessions under different identities (work, personal, research,
etc.) in the same browser window.

Permissions: `cookies`, `tabs`, `storage`, `contextMenus`, `webRequest`,
`webRequestBlocking`, `contextualIdentities`, `<all_urls>`.

---

## Development: loading via about:debugging

To iterate on an extension without running a full overlay build:

1. Open BearBrowser and navigate to `about:debugging#/runtime/this-firefox`.
2. Click **Load Temporary Add-on...**.
3. Select the `manifest.json` inside the extension directory
(`extensions/bear-spaces/manifest.json` or `extensions/bear-containers/manifest.json`).
4. The extension loads for the current session. It is removed on restart.

For persistent dev loads without a full build, create a developer profile and
add the extension to it directly.

---

## How bundling works in the overlay build

During `scripts/bearbrowser-overlay-binary.sh` (step 7/10):

1. **`scripts/bearbrowser-pack-extensions.sh`** zips each directory under
`extensions/` into a `.xpi` named by its gecko ID and writes the files to
`build/extensions/`.

2. **`scripts/bearbrowser-install-extensions.sh`** copies the `.xpi` files into
`BearBrowser.app/Contents/Resources/distribution/extensions/` and merges the
following into `distribution/policies.json`:
- `Extensions.Install` — the `file://` URLs of each `.xpi`
- `ExtensionSettings.<gecko-id>.installation_mode = "force_installed"` — so
the extensions install silently even though the wildcard policy blocks
user-initiated installs

On first browser launch the enterprise policy engine installs both extensions
automatically. No user action is required.
148 changes: 148 additions & 0 deletions scripts/bearbrowser-install-extensions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env bash
# MIT License
# Copyright (c) 2026 @mdheller
#
# bearbrowser-install-extensions.sh — Install packed .xpi extensions into a
# BearBrowser.app bundle and wire them into policies.json as force-installed
# enterprise extensions.
#
# This script runs AFTER bearbrowser-pack-extensions.sh has produced .xpi files
# in build/extensions/ and AFTER the overlay build's profile injection step has
# written distribution/policies.json. It:
#
# 1. Creates <app>/Contents/Resources/distribution/extensions/
# 2. Copies .xpi files from build/extensions/ into that directory
# 3. Merges Extension and ExtensionSettings policies into the existing
# policies.json (or creates one if absent), preserving all other policies.
#
# The force_installed installation_mode causes Firefox/LibreWolf to install the
# extension silently on first run without any user prompt, even when the wildcard
# ExtensionSettings blocks other installs.
#
# Usage:
# ./scripts/bearbrowser-install-extensions.sh [/path/to/BearBrowser.app]
#
# Arguments:
# /path/to/BearBrowser.app App bundle to install into.
# Default: /Applications/BearBrowser.app
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="$(cd "$SCRIPT_DIR/.." && pwd)"

APP="${1:-/Applications/BearBrowser.app}"
XPI_SRC="$REPO/build/extensions"

if [ ! -d "$APP" ]; then
echo "ERROR: app bundle not found: $APP" >&2
exit 64
fi

if [ ! -d "$XPI_SRC" ]; then
echo "ERROR: packed extensions directory not found: $XPI_SRC" >&2
echo "Run bearbrowser-pack-extensions.sh first." >&2
exit 1
fi

DIST_DIR="$APP/Contents/Resources/distribution"
EXT_DIR="$DIST_DIR/extensions"
POLICIES_JSON="$DIST_DIR/policies.json"

mkdir -p "$EXT_DIR"

echo "bearbrowser-install-extensions: installing into $APP"

# ── Step 1: Copy .xpi files ─────────────────────────────────────────────────
installed_ids=()
for xpi in "$XPI_SRC"/*.xpi; do
if [ ! -f "$xpi" ]; then
continue
fi
xpi_name="$(basename "$xpi")"
cp "$xpi" "$EXT_DIR/$xpi_name"
# Derive gecko ID from filename (strip .xpi)
gecko_id="${xpi_name%.xpi}"
installed_ids+=("$gecko_id")
echo " copied: $xpi_name → distribution/extensions/"
done

if [ "${#installed_ids[@]}" -eq 0 ]; then
echo " WARNING: no .xpi files found in $XPI_SRC" >&2
exit 0
fi

# ── Step 2: Merge extension policies into policies.json ──────────────────────
# Absolute install URL paths used in the policy (the app must live at $APP at
# runtime for file:// URLs to resolve; for build-time bundling they are
# relative to the bundle so we compute the canonical path).
# The bundle's canonical install location is the path the user has the app at.
# We write paths relative to the bundle root using a placeholder and let the
# policy mergeread from the distribution/extensions/ dir which is always a
# sibling of the policies.json file.
#
# Firefox policy: install_url in ExtensionSettings must be an https:// or
# file:// URL. For bundled local .xpi files the file:// URL must point to
# where the app is installed. We write the canonical /Applications path here;
# if the app lives elsewhere the admin should re-run this script with the
# correct APP path.

python3 - "$POLICIES_JSON" "$EXT_DIR" "${installed_ids[@]}" <<'PY'
import json, sys, os, pathlib

policies_path = sys.argv[1]
ext_dir = sys.argv[2]
gecko_ids = sys.argv[3:]

# Load existing policies.json if present
if os.path.exists(policies_path):
with open(policies_path, "r", encoding="utf-8") as f:
doc = json.load(f)
else:
doc = {}

policies = doc.setdefault("policies", {})

# Build the Install list and ExtensionSettings entries
install_urls = []
ext_settings = {}
for gid in gecko_ids:
xpi_name = f"{gid}.xpi"
# Compute the file:// URL. At runtime the .app lives wherever the user
# installed it; the ext_dir argument is the absolute path inside the bundle
# as it sits on disk right now. Use that path so the URL is accurate for
# the current install location.
xpi_path = os.path.realpath(os.path.join(ext_dir, xpi_name))
xpi_url = pathlib.Path(xpi_path).as_uri()
install_urls.append(xpi_url)
ext_settings[gid] = {
"installation_mode": "force_installed",
"install_url": xpi_url,
}

# Merge — preserve any pre-existing Extensions.Install entries
existing_install = (
policies.get("Extensions", {}).get("Install", [])
)
# Deduplicate: remove stale entries for the same gecko IDs, then re-add
existing_install = [
u for u in existing_install
if not any(gid in u for gid in gecko_ids)
]
existing_install.extend(install_urls)

policies.setdefault("Extensions", {})["Install"] = existing_install

# Merge ExtensionSettings: keep existing entries, overlay our force_installed
existing_ext_settings = policies.setdefault("ExtensionSettings", {})
existing_ext_settings.update(ext_settings)

with open(policies_path, "w", encoding="utf-8") as f:
json.dump(doc, f, indent=2, ensure_ascii=False)
f.write("\n")

print(f" policies.json updated: {policies_path}")
for gid in gecko_ids:
print(f" force_installed: {gid}")
PY

echo "bearbrowser-install-extensions: done (${#installed_ids[@]} extension(s) installed)"
41 changes: 24 additions & 17 deletions scripts/bearbrowser-overlay-binary.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ Steps:
4. Write BearBrowser Info.plist from the canonical template.
5. Install the BearBrowser icon.
6. Inject profile settings (user.js, policies.json).
7. Strip quarantine extended attributes.
8. Apply ad-hoc code signature.
9. Run branding and identity verification (unless --skip-verify).
7. Pack and install bundled extensions (Bear Spaces, Bear Containers).
8. Strip quarantine extended attributes.
9. Apply ad-hoc code signature.
10. Run branding and identity verification (unless --skip-verify).

Options:
--input-app Path to the source LibreWolf.app bundle. Required.
Expand Down Expand Up @@ -98,14 +99,14 @@ echo "version=$version"
echo

# ── Step 1: Copy the base bundle ─────────────────────────────────────────────
echo "[1/9] Copying base bundle..."
echo "[1/10] Copying base bundle..."
rm -rf "$out_app"
mkdir -p "$(dirname "$out_app")"
cp -R "$input_app" "$out_app"
echo " done → $out_app"

# ── Step 2: Apply text-format branding overlay ───────────────────────────────
echo "[2/9] Applying BearBrowser text branding overlay..."
echo "[2/10] Applying BearBrowser text branding overlay..."
bash "$script_dir/apply-bearbrowser-branding.sh" --workspace "$out_app"
# The branding script creates .bearbrowser/branding.json at the workspace root.
# Inside an app bundle this file sits outside Contents/ and breaks codesign's
Expand All @@ -122,7 +123,7 @@ find "$out_app" -maxdepth 1 -not -name "Contents" -not -path "$out_app" -delete
echo " done"

# ── Step 3: Create the BearBrowser wrapper launcher ──────────────────────────
echo "[3/9] Creating BearBrowser launcher wrapper..."
echo "[3/10] Creating BearBrowser launcher wrapper..."
macos_dir="$out_app/Contents/MacOS"

# Detect the real Firefox/LibreWolf executable name. The main entry point is
Expand Down Expand Up @@ -155,7 +156,7 @@ chmod +x "$macos_dir/BearBrowser"
echo " created $macos_dir/BearBrowser → exec $real_bin"

# ── Step 4: Write BearBrowser Info.plist ──────────────────────────────────────
echo "[4/9] Writing BearBrowser Info.plist..."
echo "[4/10] Writing BearBrowser Info.plist..."
if [ ! -f "$info_template" ]; then
echo "ERROR: Info.plist template missing: $info_template" >&2
exit 1
Expand Down Expand Up @@ -189,7 +190,7 @@ PY
echo " done"

# ── Step 5: Install BearBrowser icon ─────────────────────────────────────────
echo "[5/9] Installing BearBrowser icon..."
echo "[5/10] Installing BearBrowser icon..."
icon_svg="$repo_root/branding/bearbrowser.svg"
if [ -f "$icon_svg" ]; then
cp "$icon_svg" "$out_app/Contents/Resources/BearBrowser.svg"
Expand All @@ -204,7 +205,7 @@ else
fi

# ── Step 6: Inject profile settings ─────────────────────────────────────────
echo "[6/9] Injecting profile settings (profile=$profile)..."
echo "[6/10] Injecting profile settings (profile=$profile)..."
profile_dir="$repo_root/settings/profiles/$profile"
if [ -d "$profile_dir" ]; then
# The packaged app applies shipped prefs from a default-pref file
Expand Down Expand Up @@ -276,16 +277,22 @@ else
echo " WARNING: no bundled fonts in $fonts_src — font allowlist will be a safe no-op"
fi

# ── Step 7: Strip quarantine extended attributes ─────────────────────────────
echo "[7/9] Stripping quarantine attributes..."
# ── Step 7: Pack and install bundled extensions ──────────────────────────────
echo "[7/10] Packing and installing bundled extensions..."
"$script_dir/bearbrowser-pack-extensions.sh" "$repo_root/build/extensions" 2>&1 | sed 's/^/ /'
"$script_dir/bearbrowser-install-extensions.sh" "$out_app" 2>&1 | sed 's/^/ /'
echo " done"

# ── Step 8: Strip quarantine extended attributes ─────────────────────────────
echo "[8/10] Stripping quarantine attributes..."
xattr -cr "$out_app" 2>/dev/null || true
echo " done"

# ── Step 8: Ad-hoc code signature ────────────────────────────────────────────
# ── Step 9: Ad-hoc code signature ────────────────────────────────────────────
if [ "$skip_sign" = "true" ]; then
echo "[8/9] Skipping signing (--skip-sign)."
echo "[9/10] Skipping signing (--skip-sign)."
else
echo "[8/9] Applying ad-hoc code signature..."
echo "[9/10] Applying ad-hoc code signature..."
if command -v codesign >/dev/null 2>&1; then
# Ad-hoc signing a modified Gecko bundle requires signing inner components
# first (frameworks, helpers, nested apps) then the outer bundle.
Expand All @@ -306,11 +313,11 @@ else
fi
fi

# ── Step 9: Verify BearBrowser identity ─────────────────────────────────────
# ── Step 10: Verify BearBrowser identity ─────────────────────────────────────
if [ "$skip_verify" = "true" ]; then
echo "[9/9] Skipping verification (--skip-verify)."
echo "[10/10] Skipping verification (--skip-verify)."
else
echo "[9/9] Verifying BearBrowser identity..."
echo "[10/10] Verifying BearBrowser identity..."
# Pass --skip-signing to the verifier since ad-hoc signatures won't pass
# spctl --assess (Gatekeeper requires a notarized Developer ID signature).
bash "$script_dir/verify-macos-app.sh" --app "$out_app" --skip-signing 2>&1 | sed 's/^/ /'
Expand Down
Loading
Loading