diff --git a/docker/archipelago-source/UPSTREAM.md b/docker/archipelago-source/UPSTREAM.md index 753f0006..f269c072 100644 --- a/docker/archipelago-source/UPSTREAM.md +++ b/docker/archipelago-source/UPSTREAM.md @@ -16,9 +16,11 @@ lookup relays from the defaults. It does not replace GitWorkshop's NIP-34, GRASP, repository browser, issue, pull-request, or review interfaces. The separate dependency patch refreshes the npm lockfile and moves `fflate` to -0.8.3, `react-router-dom` to 7.18.3, and Vitest to 5.0.0. The resulting clean -install reports zero npm advisories; its type-check, 152 unit tests, and -Archipelago subpath production build pass. Keeping this mechanical security +0.8.3, `react-router-dom` to 7.18.3, and Vitest to 5.0.0. On 2026-09-30 the lockfile was refreshed again for `brace-expansion` +1.1.21/5.0.12, `fast-uri` 3.1.8 and `ip-address` 10.7.2 after fresh node +installs failed the retained dependency audit. The resulting clean install +reports zero npm advisories; its type-check, 152 unit tests, and Archipelago +subpath production build pass. The complete image also builds on the X250. Keeping this mechanical security update separate makes both the upstream integration and future dependency refreshes auditable. diff --git a/docker/archipelago-source/gitworkshop-dependencies.patch b/docker/archipelago-source/gitworkshop-dependencies.patch index 2c29d639..9a4e71b4 100644 --- a/docker/archipelago-source/gitworkshop-dependencies.patch +++ b/docker/archipelago-source/gitworkshop-dependencies.patch @@ -1,5 +1,5 @@ diff --git a/package-lock.json b/package-lock.json -index 20631bb..0933917 100644 +index 20631bb..86b6f86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,7 +63,7 @@ @@ -495,9 +495,9 @@ index 20631bb..0933917 100644 - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", -+ "version": "5.0.9", -+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", -+ "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", ++ "version": "5.0.12", ++ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", ++ "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { @@ -678,9 +678,9 @@ index 20631bb..0933917 100644 - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", -+ "version": "1.1.18", -+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", -+ "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", ++ "version": "1.1.21", ++ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", ++ "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -833,9 +833,9 @@ index 20631bb..0933917 100644 - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", - "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", -+ "version": "3.1.7", -+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", -+ "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", ++ "version": "3.1.8", ++ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", ++ "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "funding": [ { "type": "github", @@ -859,9 +859,9 @@ index 20631bb..0933917 100644 - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", -+ "version": "5.0.9", -+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", -+ "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", ++ "version": "5.0.12", ++ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", ++ "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { @@ -893,9 +893,9 @@ index 20631bb..0933917 100644 - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", -+ "version": "10.7.0", -+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", -+ "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", ++ "version": "10.7.2", ++ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", ++ "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", "license": "MIT", "engines": { "node": ">= 12" @@ -1445,4 +1445,3 @@ index bd7190c..6aa5a6f 100644 import { vi } from "vitest"; // Mock window.matchMedia - diff --git a/image-recipe/_archived/build-auto-installer-iso.sh b/image-recipe/_archived/build-auto-installer-iso.sh index 9b2c24cc..b4f70394 100755 --- a/image-recipe/_archived/build-auto-installer-iso.sh +++ b/image-recipe/_archived/build-auto-installer-iso.sh @@ -2607,21 +2607,14 @@ if [ -f "$SCRIPT_DIR/../../scripts/image-versions.sh" ]; then echo " ✅ Bundled image-versions.sh" fi -# Bundle docker UI source files for building custom UIs on first boot -# Always bundle — these are tiny HTML/CSS files, not container images -if true; then - DOCKER_UI_DIR="$SCRIPT_DIR/../../docker" - if [ -d "$DOCKER_UI_DIR" ]; then - echo " Bundling docker UI source files..." - mkdir -p "$ARCH_DIR/docker" - for ui_dir in bitcoin-ui lnd-ui electrs-ui; do - if [ -d "$DOCKER_UI_DIR/$ui_dir" ]; then - cp -r "$DOCKER_UI_DIR/$ui_dir" "$ARCH_DIR/docker/" - echo " ✅ Bundled $ui_dir source" - fi - done - fi -fi +# Build-source apps need their complete contexts even on unbundled ISOs. +# Keep this identical to the OTA runtime payload; a per-app allowlist silently +# omitted GitWorkshop, FIPS and Cuprate and made fresh installs fail at 70%. +DOCKER_UI_DIR="$SCRIPT_DIR/../../docker" +[ -d "$DOCKER_UI_DIR" ] || { echo "Missing docker build sources" >&2; exit 1; } +mkdir -p "$ARCH_DIR/docker" +cp -a "$DOCKER_UI_DIR/." "$ARCH_DIR/docker/" +python3 "$SCRIPT_DIR/../../scripts/check-app-build-contexts.py" "$ARCH_DIR" if [ "$UNBUNDLED" = "1" ]; then echo " ✅ Unbundled build ready (Tor setup included, no container images)" diff --git a/scripts/check-app-build-contexts.py b/scripts/check-app-build-contexts.py new file mode 100644 index 00000000..1f3e1fd8 --- /dev/null +++ b/scripts/check-app-build-contexts.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Validate build-source apps against an OTA/ISO runtime payload before shipping.""" +import sys +from pathlib import Path +import yaml + + +def check(root: Path) -> int: + root = root.resolve() + manifests = sorted((root / 'apps').glob('*/manifest.y*ml')) + if not manifests: + raise ValueError(f'No app manifests in {root / "apps"}') + count = 0 + for manifest in manifests: + app = yaml.safe_load(manifest.read_text())['app'] + build = app.get('container', {}).get('build') + if not build: + continue + context = Path(build['context']) + if context.is_absolute(): + context = root / context.relative_to('/opt/archipelago') + else: + context = manifest.parent / context + context = context.resolve() + if not context.is_relative_to(root) or not context.is_dir(): + raise ValueError(f'{app["id"]}: missing or out-of-payload build context: {context}') + dockerfile = (context / build.get('dockerfile', 'Dockerfile')).resolve() + if not dockerfile.is_relative_to(context) or not dockerfile.is_file(): + raise ValueError(f'{app["id"]}: missing or out-of-context Dockerfile: {dockerfile}') + count += 1 + return count + + +if __name__ == '__main__': + try: + count = check(Path(sys.argv[1] if len(sys.argv) > 1 else '.')) + except (ValueError, KeyError, OSError, yaml.YAMLError) as error: + sys.exit(f'Invalid app build payload: {error}') + print(f'Validated {count} app build contexts and Dockerfiles.') diff --git a/scripts/create-release-manifest.sh b/scripts/create-release-manifest.sh index 5a765956..d89455fb 100755 --- a/scripts/create-release-manifest.sh +++ b/scripts/create-release-manifest.sh @@ -101,6 +101,7 @@ if [ -z "$FRONTEND_ARCHIVE" ]; then cp -r "$PROJECT_ROOT/$runtime_path" "$RUNTIME_DIR/$runtime_path" fi done + python3 "$PROJECT_ROOT/scripts/check-app-build-contexts.py" "$RUNTIME_DIR" # KEEP IN SYNC with the `for unit in [...]` array in # core/archipelago/src/bootstrap.rs (run_runtime_assets). A unit that # bootstrap installs but this list does not ship simply never reaches a diff --git a/scripts/iso-smoke-test.sh b/scripts/iso-smoke-test.sh index b7c336c6..6f4470ab 100755 --- a/scripts/iso-smoke-test.sh +++ b/scripts/iso-smoke-test.sh @@ -64,6 +64,13 @@ for f in live/vmlinuz live/initrd.img live/filesystem.squashfs \ fi done +# Verify the mounted artifact carries every manifest-declared build source. +if python3 "$REPO/scripts/check-app-build-contexts.py" "$MNT/archipelago"; then + ok "app build contexts and Dockerfiles" +else + bad "incomplete app build payload" +fi + # ── GRUB must boot the live system ─────────────────────────────────── if grep -q "boot=live" "$MNT/boot/grub/grub.cfg" 2>/dev/null; then ok "grub.cfg has boot=live" diff --git a/tests/regression/app-build-contexts.py b/tests/regression/app-build-contexts.py new file mode 100644 index 00000000..6a1be14b --- /dev/null +++ b/tests/regression/app-build-contexts.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Exercise release payload checks with complete, incomplete and escaping contexts.""" +import importlib.util +import shutil +import tempfile +import unittest +from pathlib import Path + +REPO = Path(__file__).resolve().parents[2] +spec = importlib.util.spec_from_file_location('contexts', REPO / 'scripts/check-app-build-contexts.py') +contexts = importlib.util.module_from_spec(spec) +spec.loader.exec_module(contexts) + + +class BuildPayloadTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + shutil.copytree(REPO / 'apps', self.root / 'apps') + shutil.copytree(REPO / 'docker', self.root / 'docker') + + def test_complete_payload(self): + self.assertGreaterEqual(contexts.check(self.root), 6) + + def test_iso_old_allowlist_rejected(self): + shutil.rmtree(self.root / 'docker/archipelago-source') + with self.assertRaisesRegex(ValueError, 'archipelago-source.*missing'): + contexts.check(self.root) + + def test_missing_dockerfile_rejected(self): + (self.root / 'docker/archipelago-source/Dockerfile').unlink() + with self.assertRaisesRegex(ValueError, 'archipelago-source.*Dockerfile'): + contexts.check(self.root) + + def test_context_symlink_cannot_escape_payload(self): + target = self.root / 'docker/archipelago-source' + shutil.rmtree(target) + target.symlink_to(REPO / 'docker/archipelago-source', target_is_directory=True) + with self.assertRaisesRegex(ValueError, 'out-of-payload'): + contexts.check(self.root) + + def test_empty_payload_rejected(self): + shutil.rmtree(self.root / 'apps') + with self.assertRaisesRegex(ValueError, 'No app manifests'): + contexts.check(self.root) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/release/run.sh b/tests/release/run.sh index b9607461..b0912870 100755 --- a/tests/release/run.sh +++ b/tests/release/run.sh @@ -71,6 +71,7 @@ summary() { # ── Stage 1: static ────────────────────────────────────────────────── stage "git-diff-check" git diff --check stage "cargo-fmt" timeout 240 cargo fmt --manifest-path core/Cargo.toml --all --check +stage "app-build-contexts" python3 tests/regression/app-build-contexts.py stage "manifest-shell" python3 scripts/check-manifest-shell.py stage "doctor-ports" bash tests/regression/container-doctor-ports.sh stage "bitcoin-pruning" python3 tests/regression/bitcoin-prune-entrypoint.py