diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index 810bb644ba..7eb3da4095 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -443,13 +443,17 @@ jobs: - name: Install frontend dependencies working-directory: studio/frontend + # `npm ci` so a release build can never pull a fresh minor/patch + # of a transitive dep from the registry via caret-range resolution; + # the tree is exactly what the committed lockfile pins. + # # Lifecycle scripts (esbuild native-binary postinstall, etc.) are # required for `vite build`. The pre-install lockfile structural # audit (lockfile_supply_chain_audit.py) is the practical defence # against the npm postinstall-dropper class -- it fires BEFORE any # tarball runs, on the injection pattern itself rather than an # advisory-DB lookup. - run: npm install --no-fund --no-audit + run: npm ci --no-fund --no-audit # ── Rust ── - name: Install Rust stable diff --git a/studio/setup.ps1 b/studio/setup.ps1 index 40788a0ecb..095c767c4d 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -1333,9 +1333,13 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { # metadata but no actual content (bin/, lib/). When this happens bun install # exits 0 but leaves binaries missing. We validate after install and clear # the cache + retry once before falling back to npm. + # + # --frozen-lockfile so a Windows end-user install can never pull a fresh + # minor/patch of a transitive dep from the registry via caret-range + # resolution; the tree is exactly what the committed lockfile pins. if ($UseBun) { Write-Host " Using bun for package install (faster)" -ForegroundColor DarkGray - $bunExit = Invoke-SetupCommand { bun install } + $bunExit = Invoke-SetupCommand { bun install --frozen-lockfile } # On Windows, .bin/ entries vary by package manager: # npm → tsc, tsc.cmd, tsc.ps1 # bun → tsc.exe, tsc.bunx @@ -1349,7 +1353,7 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { Remove-Item "node_modules" -Recurse -Force -ErrorAction SilentlyContinue } Invoke-SetupCommand { bun pm cache rm } | Out-Null - $bunExit = Invoke-SetupCommand { bun install } + $bunExit = Invoke-SetupCommand { bun install --frozen-lockfile } $hasTsc = (Test-Path "node_modules\.bin\tsc") -or (Test-Path "node_modules\.bin\tsc.cmd") -or (Test-Path "node_modules\.bin\tsc.exe") -or (Test-Path "node_modules\.bin\tsc.bunx") $hasVite = (Test-Path "node_modules\.bin\vite") -or (Test-Path "node_modules\.bin\vite.cmd") -or (Test-Path "node_modules\.bin\vite.exe") -or (Test-Path "node_modules\.bin\vite.bunx") if ($bunExit -ne 0 -or -not $hasTsc -or -not $hasVite) { @@ -1368,13 +1372,16 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { } } if (-not $UseBun) { - $npmExit = Invoke-SetupCommand { npm install } + # npm ci (not npm install) so the install is pinned to the committed + # lockfile -- a hijacked transitive cannot land via caret-range + # resolution. Fails fast if package.json and the lockfile have drifted. + $npmExit = Invoke-SetupCommand { npm ci } if ($npmExit -ne 0) { Pop-Location $ErrorActionPreference = $prevEAP_npm foreach ($gi in $HiddenGitignores) { Rename-Item -Path "$gi._twbuild" -NewName (Split-Path $gi -Leaf) -Force -ErrorAction SilentlyContinue } - Write-Host "[ERROR] npm install failed (exit code $npmExit)" -ForegroundColor Red - Write-Host " Try running 'npm install' manually in frontend/ to see errors" -ForegroundColor Yellow + Write-Host "[ERROR] npm ci failed (exit code $npmExit)" -ForegroundColor Red + Write-Host " Try running 'npm ci' manually in frontend/ to see errors" -ForegroundColor Yellow exit 1 } } @@ -1411,11 +1418,13 @@ if (Test-Path $OxcValidatorDir) { $prevEAP_oxc = $ErrorActionPreference $ErrorActionPreference = "Continue" Push-Location $OxcValidatorDir - $oxcInstallExit = Invoke-SetupCommand { npm install } + # npm ci pins the oxc validator install to its committed lockfile so a + # hijacked transitive cannot land via caret-range resolution. + $oxcInstallExit = Invoke-SetupCommand { npm ci } if ($oxcInstallExit -ne 0) { Pop-Location $ErrorActionPreference = $prevEAP_oxc - Write-Host "[ERROR] OXC validator npm install failed (exit code $oxcInstallExit)" -ForegroundColor Red + Write-Host "[ERROR] OXC validator npm ci failed (exit code $oxcInstallExit)" -ForegroundColor Red exit 1 } Pop-Location diff --git a/studio/setup.sh b/studio/setup.sh index 8511cf5822..6d71b68319 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -417,7 +417,9 @@ fi # end frontend build check # ── oxc-validator runtime ── if [ -d "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator" ] && command -v npm &>/dev/null; then cd "$SCRIPT_DIR/backend/core/data_recipe/oxc-validator" - run_quiet_no_exit "npm install (oxc validator runtime)" npm install --no-fund --no-audit --loglevel=error + # npm ci pins the oxc validator install to its committed lockfile so a + # hijacked transitive cannot land via caret-range resolution. + run_quiet_no_exit "npm ci (oxc validator runtime)" npm ci --no-fund --no-audit --loglevel=error _oxc_install_rc=$? if [ "$_oxc_install_rc" -ne 0 ]; then exit "$_oxc_install_rc"