Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package git-bug for openSUSE:Factory checked 
in at 2026-10-24 22:04:49
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/git-bug (Old)
 and      /work/SRC/openSUSE:Factory/.git-bug.new.2252680 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "git-bug"

Sat Oct 24 22:04:49 2026 rev:20 rq:1392166 version:0.11.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/git-bug/git-bug.changes  2026-10-08 
11:58:58.734957871 +0200
+++ /work/SRC/openSUSE:Factory/.git-bug.new.2252680/git-bug.changes     
2026-10-24 22:06:33.822478398 +0200
@@ -1,0 +2,112 @@
+Fri Oct 23 17:38:53 UTC 2026 - Matej Cepl <[email protected]>
+
+- Fix vendored CVEs and build Web UI native addons from source
+  Update vendored dependencies for security fixes:
+  - npm: fast-uri 3.1.7 -> 3.1.9 and qs 6.16.0 -> 6.16.1. The
+    reviewed downstream inventory replaces only these two
+    sources, the npm manifest pins them through overrides, and
+    prepare-webui-lock.py gains --security-inventory to keep
+    them when the lock is regenerated.
+  - Go: replace golang.org/x/net with v0.60.0 through the
+    go_modules service.
+  - Not changed: picomatch stays at 4.0.7, the newest npm
+    release, which is still affected.
+  - follow-redirects and webpack-dev-server are not vendored.
+  - GO-2026-5932 concerns only the unmaintained
+    golang.org/x/crypto/openpgp, which is neither used nor
+    vendored; upstream uses ProtonMail's fork.
+  - The Web UI build also loaded upstream's prebuilt npm binaries
+    for Rolldown, Tailwind CSS oxide and Lightning CSS, so
+    unverified machine code generated the JavaScript and CSS
+    embedded in git-bug. The npm container carried native
+    binaries for every platform: 1,167 tarballs and 809 MiB, most
+    of it never used on Linux x86_64. The RPM now compiles these
+    three Node-API modules from their upstream Rust workspaces:
+    - New native-addons.py reads the addon versions from
+      package-lock.json, regenerates tar_scm, recompress and
+      cargo_vendor services in a marked region of _service, and
+      writes native-addons.spec.inc with the source tags and the
+      %{native_addons} list. The service run adds the upstream
+      source and vendored crate tarballs for Rolldown 1.2.8,
+      Tailwind CSS 4.3.3 and Lightning CSS 1.33.0.
+    - The spec builds each module with plain cargo, --offline and
+      --locked against upstream's Cargo.lock, keeping upstream's
+      release profiles and project rustflags (%cargo_build would
+      override Lightning CSS's link flags through RUSTFLAGS). It
+      checks each tag against the locked npm version, fails if
+      npm installs any ELF file, and installs the modules where
+      the npm loaders look first.
+    - prepare-webui-lock.py unifies addons that npm resolves
+      in several versions; @tailwindcss/node's exact Lightning
+      CSS 1.32.0 pin is overridden to 1.33.0, changing no other
+      lock entry. It also writes the reduced build inventory
+      node-sources.json: npm's selected sources minus optional
+      platform-specific packages. The complete reviewed inventory
+      moves to node-sources-full.json for lock generation.
+    - The npm container shrinks to 860 tarballs (61 MiB); all
+      sources total 111 MiB.
+    - Drop the esbuild build dependency: the Vite 8 build never
+      runs esbuild.
+    - Add _constraints for the Rust builds on x86_64, ignore the
+      tar_scm clone caches, and update WEBUI-PACKAGING.md.
+  Fixes: bsc#1286302
+  Fixes: bsc#1285711
+  Fixes: CVE-2026-110725
+  Fixes: CVE-2026-110306
+  Fixes: CVE-2026-110095
+  Fixes: CVE-2026-110180
+  Fixes: CVE-2026-110816
+  Fixes: CVE-2026-97032
+  Fixes: CVE-2026-78663
+  Fixes: CVE-2026-78669
+  Fixes: CVE-2026-78660
+  Fixes: CVE-2026-78659
+  References: bsc#1286061
+  References: CVE-2026-112366
+  References: bsc#1286880
+  References: CVE-2026-111756
+  References: bsc#1286894
+  References: CVE-2026-112085
+  References: GO-2026-5932
+
+-------------------------------------------------------------------
+Fri Oct 23 08:23:34 UTC 2026 - Matej Cepl <[email protected]>
+
+- Build the native npm addons of the Web UI (Rolldown 1.2.8,
+  Tailwind CSS oxide 4.3.3, Lightning CSS 1.33.0) from their
+  upstream Rust sources instead of using prebuilt binaries
+  from npm. New native-addons.py derives their versions from
+  the npm lock and generates the tar_scm, recompress and
+  cargo_vendor services and native-addons.spec.inc.
+- Drop all optional platform-specific npm packages and the
+  sources npm never selects from the build inventory: the npm
+  container shrinks from 1,167 tarballs (809 MiB) to 860
+  tarballs (61 MiB). The complete reviewed inventory is kept as
+  node-sources-full.json for lock generation.
+- Unify Lightning CSS on 1.33.0 with an npm override, so that
+  only one version has to be compiled.
+- Drop the esbuild build dependency; the Vite 8 build does not
+  run esbuild.
+- Add _constraints for the Rust builds on x86_64.
+
+-------------------------------------------------------------------
+Thu Oct 22 21:13:37 UTC 2026 - Matej Cepl <[email protected]>
+
+- Update JavaScript vendoring for security fixes:
+- fast-uri 3.1.7 -> 3.1.9 (bsc#1286302, CVE-2026-110725,
+  CVE-2026-110306, CVE-2026-110095, CVE-2026-110180).
+- qs 6.16.0 -> 6.16.1 (bsc#1285711, CVE-2026-110816).
+- Regenerate npm lock and vendor container; support reviewed
+  downstream security inventory in the lock preparation tool.
+- picomatch remains affected by bsc#1286061, CVE-2026-112366;
+  no fixed npm release is available through 4.0.7.
+- follow-redirects (bsc#1286880, CVE-2026-111756) and
+  webpack-dev-server (bsc#1286894, CVE-2026-112085) are not vendored.
+- Replace vendored golang.org/x/net with v0.60.0 to fix security
+  vulnerabilities (CVE-2026-97032, CVE-2026-78663, CVE-2026-78669,
+  CVE-2026-78660, CVE-2026-78659).
+- GO-2026-5932 only affects the unmaintained
+  golang.org/x/crypto/openpgp package which is neither used nor
+  vendored (upstream uses ProtonMail's fork instead).
+
+-------------------------------------------------------------------

New:
----
  _constraints
  lightningcss-1.33.0.tar.zst
  native-addons.py
  native-addons.spec.inc
  node-sources-full.json
  rolldown-1.2.8.tar.zst
  tailwindcss-4.3.3.tar.zst
  test_native_addons.py
  vendor-lightningcss.tar.zst
  vendor-rolldown.tar.zst
  vendor-tailwindcss.tar.zst

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ git-bug.spec ++++++
--- /var/tmp/diff_new_pack.ktat8e/_old  2026-10-24 22:06:40.316751722 +0200
+++ /var/tmp/diff_new_pack.ktat8e/_new  2026-10-24 22:06:40.317751764 +0200
@@ -16,10 +16,12 @@
 #
 
 
-# The npm native build tools have only been validated on x86_64. Keep the
-# CLI available elsewhere; enable other architectures after testing them.
+# The Web UI has only been validated on x86_64. Keep the CLI available
+# elsewhere; enable other architectures after testing them, adding their
+# Node-API platform suffix below.
 %ifarch x86_64
 %bcond_without webui
+%global napi_triple linux-x64-gnu
 %else
 %bcond_with webui
 %endif
@@ -35,8 +37,8 @@
 %else
 License:        MIT
 %endif
-URL:            https://github.com/MichaelMure/git-bug
-Source0:        
https://github.com/MichaelMure/%{name}/archive/refs/tags/v%{version}.tar.gz#/git-bug-%{version}.tar.gz
+URL:            https://github.com/git-bug/git-bug
+Source0:        
https://github.com/git-bug/%{name}/archive/refs/tags/v%{version}.tar.gz#/git-bug-%{version}.tar.gz
 # Source0:        git-bug-%%{version}.tar.gz
 Source1:        vendor.tar.gz
 Source2:        package-lock.json
@@ -52,19 +54,33 @@
 Source12:       WEBUI-PACKAGING.md
 Source13:       collect-webui-licenses.py
 Source14:       test_collect_webui_licenses.py
+Source15:       node-sources-full.json
+Source16:       native-addons.py
+Source17:       test_native_addons.py
+Source18:       native-addons.spec.inc
 BuildRequires:  git
 BuildRequires:  golang(API) >= 1.26
 BuildRequires:  golang-packaging
 %if %{with webui}
 # OBS/osc unpacks node_modules.obscpio into the source directory. This include
-# declares its npm tarballs as individual sources for the source RPM.
+# declares its npm tarballs as individual sources for the source RPM. The
+# inventory contains no prebuilt platform binaries.
 %include %{_sourcedir}/node_modules.spec.inc
-# The npm esbuild wrapper requires an executable with the identical version.
-BuildRequires:  esbuild = 0.28.2
+# Upstream Rust sources and vendored crates of the native npm addons
+# (Rolldown, Tailwind CSS oxide, Lightning CSS), declared together with the
+# %%{native_addons} list. Both are generated by native-addons.py.
+%include %{_sourcedir}/native-addons.spec.inc
+BuildRequires:  cargo
+BuildRequires:  cmake
+BuildRequires:  gcc
+BuildRequires:  gcc-c++
 BuildRequires:  local-npm-registry >= 1.1.0
+BuildRequires:  make
 BuildRequires:  nodejs24
 BuildRequires:  npm24
 BuildRequires:  python3-base
+BuildRequires:  rust >= 1.98
+BuildRequires:  zstd
 %endif
 
 %description
@@ -125,6 +141,15 @@
 # Use the npm-generated installation tree, not the service download inventory.
 cp %{SOURCE2} webui/package-lock.json
 cp %{SOURCE3} webui/package.json
+# Unpack each native addon with only the crates directory of its vendor
+# tarball. Upstream's Cargo.lock and .cargo/config.toml stay in effect;
+# Lightning CSS sets its link flags there.
+mkdir native
+for addon in %{native_addons}; do
+    IFS=, read -r name version crate library target manifest npm <<< "$addon"
+    tar -C native -xf %{_sourcedir}/$name-$version.tar.zst
+    tar -C native/$name-$version -xf %{_sourcedir}/vendor-$name.tar.zst 
vendor-$name
+done
 %endif
 
 %build
@@ -133,6 +158,24 @@
 export GOSUMDB=off
 export GOFLAGS="-mod=vendor -buildmode=pie"
 %if %{with webui}
+# Print name@version from a package.json file.
+package_id() {
+    python3 -c 'import json, sys; d = json.load(open(sys.argv[1])); 
print(d["name"] + "@" + d["version"])' "$1"
+}
+# Compile the Node-API modules of the native addons from source; npm's
+# prebuilt platform packages are not in the sources. The modules only run
+# during this build and are not packaged. Plain cargo keeps upstream's release
+# profiles and project rustflags, which %%cargo_build overrides via RUSTFLAGS.
+export CARGO_HOME="$PWD/.cargo-home"
+for addon in %{native_addons}; do
+    IFS=, read -r name version crate library target manifest npm <<< "$addon"
+    pushd native/$name-$version
+    test "$(package_id "$manifest")" = "$npm@$version"
+    cargo build --release --offline --locked %{?_smp_mflags} -p "$crate" \
+        --config "source.crates-io.replace-with='vendored-sources'" \
+        --config "source.vendored-sources.directory='$PWD/vendor-$name'"
+    popd
+done
 # Keep npm configuration and caches inside the build tree. All dependencies
 # come from source tarballs served over loopback; disable install scripts.
 export npm_config_cache="$PWD/.npm-cache"
@@ -140,12 +183,26 @@
 export npm_config_globalconfig="$PWD/.npmrc-global"
 export npm_config_update_notifier=false
 export NODE_OPTIONS="--max-old-space-size=1536"
-export ESBUILD_BINARY_PATH="$(command -v esbuild)"
-test -x "$ESBUILD_BINARY_PATH"
 pushd webui
 local-npm-registry %{_sourcedir} ci \
     --include=dev --ignore-scripts --legacy-peer-deps \
     --no-audit --no-fund --maxsockets=2
+# npm must not have installed any machine code.
+python3 - <<'EOF'
+import os, sys
+found = [os.path.join(root, name) for root, _, names in os.walk("node_modules")
+         for name in names if not os.path.islink(os.path.join(root, name))
+         and open(os.path.join(root, name), "rb").read(4) == b"\x7fELF"]
+sys.exit("Prebuilt ELF files from npm:\n" + "\n".join(found) if found else 0)
+EOF
+# Install the compiled modules where the npm loaders look first.
+for addon in %{native_addons}; do
+    IFS=, read -r name version crate library target manifest npm <<< "$addon"
+    test "$(package_id node_modules/$npm/package.json)" = "$npm@$version"
+    test -d "$(dirname "$target")"
+    install -m 0755 ../native/$name-$version/target/release/$library \
+        "$target.%{napi_triple}.node"
+done
 npm run build
 test -s dist/index.html.gz
 popd

++++++ WEBUI-PACKAGING.md ++++++
--- /var/tmp/diff_new_pack.ktat8e/_old  2026-10-24 22:06:40.395755047 +0200
+++ /var/tmp/diff_new_pack.ktat8e/_new  2026-10-24 22:06:40.398755173 +0200
@@ -2,42 +2,50 @@
 
 ## Status
 
-The spec now builds and embeds the Web UI by default on x86-64. Other
-architectures retain the CLI-only build until their native frontend tools are
-validated. Use `--without webui` for the CLI-only variant or `--with webui` to
-explicitly test the frontend on another architecture.
-
-`_service` generates `node_modules.obscpio` and `node_modules.spec.inc` for the
-traditional OSC workflow. Both the Web UI-enabled and CLI-only RPM builds have
-passed locally with external networking disabled.
-
-This proof used upstream npm native binaries for Rolldown, Oxc, Tailwind, and
-Lightning CSS. Only esbuild was replaced with the matching Factory executable.
-A successful baseline is not proof that the package meets source-build policy
-or supports every openSUSE architecture.
+The spec builds and embeds the Web UI by default on x86-64. Other
+architectures retain the CLI-only build until they are tested. Use
+`--without webui` for the CLI-only variant or `--with webui` to explicitly test
+the frontend on another architecture (after adding its Node-API platform
+suffix to the spec's `napi_triple`).
+
+`_service` generates the npm container and source include, and fetches and
+vendors the Rust sources of the native npm addons. The RPM compiles Rolldown,
+Tailwind CSS oxide and Lightning CSS from those sources. No prebuilt machine
+code from npm enters the build; the spec fails if npm installs any ELF file.
+Both the Web UI-enabled and CLI-only RPM builds have passed locally with
+external networking disabled.
+
+The JavaScript inside the npm tarballs is still upstream's published,
+bundled output. Rebuilding the JavaScript toolchain from TypeScript sources is
+out of scope.
 
 | Plan step | Status |
 |---|---|
 | 1. Separate downloading and npm resolution | Complete |
 | 2. Implement converter | Complete |
 | 3. Use service's multi-lock input | Complete and tested with installed 
service |
-| 4. Test conversion | Complete; 27 unit tests across preparation and notice 
collection |
+| 4. Test conversion | Complete; 37 unit tests across preparation, native 
addons and notice collection |
 | 5. Generate genuine npm lock | Generated; provenance checked and fresh-cache 
installation passed. Exact peer-graph equivalence is not claimed. |
-| 6. Integrate service/submission workflow | Implemented with a manual 
service, legacy cpio container, and generated source include |
-| 7. Factory tools and native dependencies | Factory esbuild validated; 
remaining native tooling unresolved |
-| 8. Reduce dependencies | Pending; baseline established |
+| 6. Integrate service/submission workflow | Implemented with manual services, 
a legacy cpio container and generated source includes |
+| 7. Factory tools and native dependencies | Complete: native addons compiled 
from Rust sources; esbuild is not used by the build |
+| 8. Reduce dependencies | Build inventory reduced to the 860 sources npm 
installs; development dependencies not yet pruned |
 | 9. Update spec and source patches | Spec integration complete; no upstream 
source patch required |
 | 10. Offline checks and packaging review | Both local RPM variants passed; 
packaged Web UI binary passed smoke tests. Clean OBS build and full license 
review pending |
 | 11. Maintenance documentation | Service regeneration, RPM build, and 
fallback documented |
 
-## Two different artifacts
+## Three different artifacts
 
-- `node-sources.json` is a generated download inventory for
-  `obs-service-node_modules`. It describes tarball URLs and checksums, not an
-  installation layout. Never pass it to npm as `package-lock.json`.
+- `node-sources-full.json` is the reviewed complete download inventory: the
+  upstream pnpm inventory with the downstream security replacements. Lock
+  generation resolves against it.
+- `node-sources.json` is the reduced build inventory written by
+  `prepare-webui-lock.py` and consumed by `obs-service-node_modules`. It
+  contains the sources npm selected, minus optional platform-specific packages.
 - `package-lock.json` must be produced and validated by npm. It records the
-  actual installation tree and peer resolution. It is not generated by the
-  converter.
+  actual installation tree and peer resolution.
+
+The two inventories describe tarball URLs and checksums, not an installation
+layout. Never pass them to npm as `package-lock.json`.
 
 The upstream `webui/pnpm-lock.yaml` remains the authority for the initial 
source
 inventory. Generating an npm lock must not silently fetch newer versions from
@@ -55,12 +63,17 @@
 ```sh
 python3 pnpm-to-obs.py \
     --input /path/to/git-bug-0.11.0/webui/pnpm-lock.yaml \
-    --output node-sources.json
+    --output node-sources-upstream.json
 
 PYTHONDONTWRITEBYTECODE=1 python3 -m unittest -v \
-    test_pnpm_to_obs.py test_prepare_webui_lock.py 
test_collect_webui_licenses.py
+    test_pnpm_to_obs.py test_prepare_webui_lock.py 
test_collect_webui_licenses.py \
+    test_native_addons.py
 ```
 
+The converter writes the plain upstream inventory. The submitted
+`node-sources-full.json` is that inventory plus the reviewed security
+replacements described below, so do not overwrite it with converter output.
+
 The converter supports pnpm lockfile version 9. It keeps all package versions
 and platform variants, including optional native bindings. It preserves 
explicit
 HTTPS tarball URLs and derives standard npm registry URLs for integrity-only
@@ -79,23 +92,22 @@
 
 ### Download and verify
 
-For a standalone download cache, the service can consume the inventory 
directly:
+Lock generation needs every tarball of the complete inventory. The submitted
+container holds only the reduced build inventory, so download the complete one
+with the service directly:
 
 ```sh
 /usr/lib/obs/service/node_modules \
-    --input node-sources.json \
+    --input node-sources-full.json \
     --node-dir /path/to/downloaded-tarballs \
     --download
-
-python3 pnpm-to-obs.py \
-    --input /path/to/git-bug-0.11.0/webui/pnpm-lock.yaml \
-    --output node-sources.json \
-    --verify-dir /path/to/downloaded-tarballs
 ```
 
 Verification is required. The installed service can log a download error 
without
-returning a failing exit status. The converter's verification checks that every
-expected tarball exists and matches its recorded integrity hashes.
+returning a failing exit status. `prepare-webui-lock.py` checks that every
+tarball of the inventory it resolves against exists and matches its recorded
+integrity hashes before starting npm. For the plain upstream inventory,
+`pnpm-to-obs.py --verify-dir` performs the same check.
 
 These downloads are source preparation. They must not happen during rpmbuild.
 
@@ -119,8 +131,9 @@
 
 Submit both `node_modules.obscpio` and `node_modules.spec.inc` along with the
 manifest, npm lock, scripts, spec, and service definition. The cpio container
-currently occupies about 809 MiB. `.gitignore` explicitly allows this file;
-the existing Git LFS attributes cover it.
+holds 860 tarballs and occupies about 61 MiB, down from 809 MiB for the
+complete inventory. `.gitignore` explicitly allows this file; the existing Git
+LFS attributes cover it.
 
 OBS/osc expands the container into individual source tarballs before RPM runs.
 The generated include declares them as sources numbered from 10000. The RPM
@@ -135,9 +148,29 @@
 cpio -idm < /path/to/package/node_modules.obscpio
 ```
 
-Use the extracted directory as `--verify-dir` for the converter and as
-`--tarballs` for the npm-lock preparation tool. Generating the installation 
lock
-is a maintainer operation; the spec only consumes the submitted lock.
+Generating the installation lock is a maintainer operation that uses the
+complete download from the previous section; the spec only consumes the
+submitted lock.
+
+## Downstream security updates
+
+The submitted inventory replaces upstream fast-uri 3.1.7 with 3.1.9 and
+qs 6.16.0 with 6.16.1. The npm manifest pins these replacements through
+overrides. All other source records are unchanged.
+
+To regenerate the lock with these reviewed replacements, add
+`--security-inventory /absolute/path/to/node-sources-full.json` to the
+`prepare-webui-lock.py` command below. Without this option, the helper
+reproduces the upstream inventory and does not retain the downstream security
+fixes. The helper rejects changes to unrelated source records and verifies
+tarball hashes before resolution. It refuses `node-sources.json` here, because
+it overwrites that file with the reduced build inventory.
+
+Picomatch 2.3.2 and 4.0.7 remain vendored. CVE-2026-112366 is unresolved:
+the advisory identifies versions through 4.0.7 as affected, and npm has no
+newer release. See https://github.com/advisories/GHSA-fc5q-39xc-vjpf.
+Neither follow-redirects nor webpack-dev-server appears in the source inventory
+or npm installation lock.
 
 ## Generate the genuine npm lock
 
@@ -152,6 +185,7 @@
 python3 prepare-webui-lock.py \
     --webui /path/to/git-bug-0.11.0/webui \
     --tarballs /path/to/downloaded-tarballs \
+    --security-inventory "$PWD/node-sources-full.json" \
     --output-dir .
 ```
 
@@ -159,7 +193,21 @@
 
 - `webui-package.json`, the npm-compatible manifest with pinned direct 
versions.
 - `package-lock.json`, a genuine npm-generated installation lock.
-- `webui-lock-report.json`, the selected source count and unused source 
identities.
+- `node-sources.json`, the reduced build inventory for the `node_modules`
+  service.
+- `webui-lock-report.json`, the selected source count, unused source 
identities,
+  native addon overrides and excluded platform packages.
+
+The RPM compiles one module per native addon (see below), so npm must install a
+single copy of each. If the first resolution yields several versions of an
+addon, the helper overrides it to the highest locked version and resolves
+again. Currently `@tailwindcss/node` pins Lightning CSS 1.32.0 while Vite uses
+1.33.0; the override unifies both on 1.33.0 and changes no other lock entry.
+
+The build inventory keeps every source npm selected, except identities whose
+every lock entry is an optional package restricted by `os`, `cpu` or `libc`.
+npm skips such packages when the registry lacks them. Required packages that
+only list supported platforms, such as `systeminformation`, stay.
 
 The helper verifies the complete tarball inventory before starting npm. It
 rejects selected sources or integrity hashes absent from the pnpm inventory.
@@ -193,7 +241,9 @@
 - Peer-resolution changes relative to pnpm snapshots.
 - Locally generated registry URLs that must not survive into a portable lock.
 - Successful installation with a new empty cache and a fresh local registry.
-- No omitted platform bindings needed on the intended build architectures.
+- Native modules loaded by the build all belonging to addons listed in
+  `native-addons.py`. Any other module would be missing from the build
+  inventory, and its npm loader fails the build.
 
 ### Validated baseline
 
@@ -215,10 +265,11 @@
 tool accepts `--registry-helper node /absolute/path/to/dist/index.js` for this
 case. The final package should use Factory's `local-npm-registry` RPM.
 
-Factory's signed `esbuild-0.28.2-1.1.x86_64.rpm` was verified and unpacked in a
-temporary directory. `ESBUILD_BINARY_PATH` pointed at that executable during
-`npm run build`. Vite successfully transformed 3,546 modules and generated
-`webui/dist`, including compressed assets and Inter webfonts.
+An earlier baseline pointed `ESBUILD_BINARY_PATH` at Factory's esbuild 0.28.2.
+Tracing the Vite 8 build later showed that it never runs esbuild: Rolldown
+bundles and Lightning CSS processes styles. The spec no longer uses esbuild.
+Vite transformed 3,546 modules and generated `webui/dist`, including
+compressed assets and Inter webfonts.
 
 The generated assets were embedded with Go 1.26.8 using the supplied vendor
 archive, `GOTOOLCHAIN=local`, `GOPROXY=off`, `GOSUMDB=off`, and
@@ -241,6 +292,82 @@
 OOM kill; a heap-limited retry did not complete within two minutes. That
 transport is not used by the generated lock or recommended build commands.
 
+## Native addons built from source
+
+The Vite build loads three Node-API modules, verified with `strace`:
+Rolldown's binding, Tailwind CSS oxide and Lightning CSS. npm normally
+installs them as prebuilt binaries in optional platform packages. Those
+packages are not in the build inventory. The RPM compiles the modules from the
+upstream Rust workspaces instead and installs them where the npm loaders look
+first, next to the loading package:
+
+| npm package | Upstream | Cargo package | Installed module |
+|---|---|---|---|
+| `rolldown` | rolldown/rolldown | `rolldown_binding` | 
`rolldown/dist/rolldown-binding.<triple>.node` |
+| `@tailwindcss/oxide` | tailwindlabs/tailwindcss | `tailwind-oxide` | 
`@tailwindcss/oxide/tailwindcss-oxide.<triple>.node` |
+| `lightningcss` | parcel-bundler/lightningcss | `lightningcss_node` | 
`lightningcss/lightningcss.<triple>.node` |
+
+`native-addons.py` holds this table. It reads the addon versions from
+`package-lock.json`, regenerates the marked region of `_service`, and writes
+`native-addons.spec.inc` with the source tags and the `%{native_addons}` list
+used by the spec. Each addon gets three manual services: `tar_scm` checks out
+tag `v<version>`, `recompress` makes `<name>-<version>.tar.zst`, and
+`cargo_vendor` writes `vendor-<name>.tar.zst`. `cargo_vendor` drops non-Linux
+crates (`filter`) and audits the lockfile. Its filter mode cannot
+enforce the lockfile, but the RPM builds with `--locked` against upstream's
+`Cargo.lock`, so any drift fails the build. For this release the vendored
+lockfiles are byte-identical to upstream's.
+
+`cargo_vendor` keeps `update=false` on purpose, and the openSUSE source
+validator warns about it. The compiled modules only run during the RPM build to
+generate the Web UI assets and are not packaged, so keeping the dependency set
+upstream tested matters more here than semver updates of crates. Crate updates
+come with new upstream releases of the addons. The generated `_service` region
+carries a comment explaining this for reviewers.
+
+The RPM build:
+
+- Unpacks only the crates directory of each vendor tarball, keeping upstream's
+  `Cargo.lock` and `.cargo/config.toml` (Lightning CSS sets link flags there).
+- Checks that each source tree's `package.json` has the locked npm name and
+  version.
+- Runs `cargo build --release --offline --locked -p <crate>` with the vendored
+  source replacement given on the command line. It uses plain cargo because
+  `%cargo_build` sets `RUSTFLAGS`, which overrides the project rustflags, and
+  forces full debuginfo. The modules only run during the build and are not
+  packaged.
+- Fails if npm installed any ELF file, then installs the compiled modules.
+
+The locally built modules produced a `webui/dist` byte-identical to the one
+produced with upstream's prebuilt binaries. In the local RPM build (3 jobs)
+the Rust builds of Rolldown, oxide and Lightning CSS took 7, 1 and 5 minutes.
+With 12 jobs they peaked at 1.7, 0.5 and 2.0 GB of memory.
+`_constraints` requests 4 GB of memory and 10 GB of disk on x86_64.
+
+Build requirements: Rust 1.98 or newer (the newest toolchain pinned upstream),
+cargo, CMake, GCC and G++ for mimalloc, make for jemalloc, and zstd. `tar_scm`
+keeps its Git clones in `rolldown/`, `tailwindcss/` and `lightningcss/`;
+`.gitignore` excludes them.
+
+## Updating the Web UI sources
+
+From the packaging directory, with the release unpacked separately:
+
+```sh
+/usr/lib/obs/service/node_modules --input node-sources-full.json \
+    --node-dir /path/to/downloaded-tarballs --download
+python3 prepare-webui-lock.py \
+    --webui /path/to/git-bug-<version>/webui \
+    --tarballs /path/to/downloaded-tarballs \
+    --security-inventory "$PWD/node-sources-full.json" --output-dir .
+python3 native-addons.py
+osc service manualrun
+```
+
+`native-addons.py` lists Rust source tarballs left over from previous versions;
+remove them. `osc service manualrun` also reruns `go_modules`, which rewrites
+`vendor.tar.gz` even when its contents are unchanged.
+
 ## RPM integration and validation
 
 The spec now:
@@ -248,7 +375,8 @@
 - Requires Go 1.26 or newer, matching `go.mod`, instead of the contradictory
   Go 1.24 and 1.25 requirements.
 - Uses Factory Node 24, npm 24, the local registry helper, Python for checks,
-  and esbuild 0.28.2 only when Web UI support is enabled.
+  and the Rust toolchain only when Web UI support is enabled.
+- Compiles the native npm addons from source, as described above.
 - Copies the npm-compatible manifest and lock into `webui/`, installs through
   the loopback registry with lifecycle scripts disabled, and runs the Vite 
build.
 - Uses the vendored Go dependencies and disables Go toolchain/module downloads.
@@ -268,28 +396,25 @@
 The latest local validation ran the actual spec with `rpmbuild -bb --nodeps`,
 inside a network namespace with only loopback enabled, for both the default
 x86-64 variant and `--without webui`. Dependency checks were bypassed because
-the registry helper and signed Factory esbuild RPM were staged locally rather
-than installed system-wide. This is a full local RPM build, not a clean OBS
-buildroot validation.
-
-Both runs produced the main RPM and completion subpackages. The binary 
extracted
-from the Web UI RPM passed the HTTP smoke test after RPM's stripping and 
packaging
-steps. Its runtime requirements contain no Node.js, npm, or esbuild dependency.
+`golang-packaging` was not installed. This is a full local RPM build, not a
+clean OBS buildroot validation.
+
+Both runs produced the main RPM and completion subpackages. The Web UI build
+installed exactly the three compiled modules. Its handler tests and the HTTP
+smoke test passed, and the main RPM has no Node.js or npm runtime dependency.
 The builds emitted an empty debugsource file-list warning, which remains to be
 reviewed separately.
 
-The generated lock and successful RPM builds do not settle native source-build
-requirements, other architectures, or the complete frontend license audit.
+Other architectures and the complete frontend license audit remain open.
 
 ## Remaining implementation stages
 
 1. Review npm/pnpm graph differences beyond the verified source inventory;
    retain the explicit peer-resolution policy.
-2. Prune development dependencies now that the full frontend baseline works,
-   regenerate the npm lock, and reduce the service inventory to the required
-   dependency closure without dropping necessary optional platform bindings.
-3. Decide how to build native modules from source and which architectures can
-   enable the UI. Do not treat npm binary tarballs as source-built tooling.
+2. Prune development dependencies, regenerate the npm lock, and reduce the
+   build inventory further.
+3. Enable the UI on other architectures: add their `napi_triple` and test the
+   Rust builds there.
 4. Complete the license/notice audit, including the two missing upstream 
license
    texts noted above.
 5. Run a clean OBS build with dependency resolution enabled and review the

++++++ _constraints ++++++
<constraints>
  <!-- The x86_64 build compiles the Rust native addons of the Web UI.
       Locally they peaked at up to 2 GB of memory. -->
  <overwrite>
    <conditions>
      <arch>x86_64</arch>
    </conditions>
    <hardware>
      <disk>
        <size unit="G">10</size>
      </disk>
      <physicalmemory>
        <size unit="G">4</size>
      </physicalmemory>
    </hardware>
  </overwrite>
</constraints>

++++++ _scmsync.obsinfo ++++++
--- /var/tmp/diff_new_pack.ktat8e/_old  2026-10-24 22:06:40.434756688 +0200
+++ /var/tmp/diff_new_pack.ktat8e/_new  2026-10-24 22:06:40.438756856 +0200
@@ -1,5 +1,5 @@
-mtime: 1791386993
-commit: b1ac3a8689f5cfac43fc5b6186e293d6d49c163ae62bf5b200bb8117716a3dc3
+mtime: 1792796107
+commit: 626f9c9a9a8aff7dcfe1fa1379c67c633a8e251b58a04b94e3c26cb88144def4
 url: https://src.opensuse.org/mcepl/git-bug
 revision: factory
 

++++++ _service ++++++
--- /var/tmp/diff_new_pack.ktat8e/_old  2026-10-24 22:06:40.465757993 +0200
+++ /var/tmp/diff_new_pack.ktat8e/_new  2026-10-24 22:06:40.469758161 +0200
@@ -14,14 +14,16 @@
     </service>
     <service name="set_version" mode="manual"/ -->
     <service name="go_modules" mode="manual">
-        <!-- v0.11.0 is upstream's latest release. govulncheck reports no
-             reachable vulnerabilities. GO-2026-5932 has no fixed version
-             and concerns x/crypto/openpgp, which git-bug replaced with the
-             maintained ProtonMail fork upstream. Replacing all of x/crypto
-             would also remove packages (such as ssh) still used by go-git. -->
+        <!-- GO-2026-5932 has no fixed version and concerns x/crypto/openpgp,
+             which git-bug replaced with the maintained ProtonMail fork 
upstream.
+             Replacing all of x/crypto would also remove packages (such as ssh)
+             still used by go-git. -->
+        <param name="replace">golang.org/x/net=golang.org/x/[email protected]</param>
     </service>
     <!-- Traditional osc workflow: archive the npm sources and declare each
-         tarball in the generated spec include. No build-time downloads. -->
+         tarball in the generated spec include. No build-time downloads.
+         node-sources.json is the reduced build inventory written by
+         prepare-webui-lock.py: no prebuilt platform binaries. -->
     <service name="node_modules" mode="manual">
         <param name="input">node-sources.json</param>
         <param name="legacy-container">true</param>
@@ -29,5 +31,70 @@
         <param name="output">node_modules.spec.inc</param>
         <param name="source-offset">10000</param>
     </service>
+    <!-- BEGIN native-addons: generated by native-addons.py, do not edit -->
+    <!-- cargo_vendor keeps update=false on purpose. These crates only
+         build Node-API modules that run during the RPM build to generate
+         the Web UI assets; the modules are not packaged. Vendoring
+         upstream's exact Cargo.lock keeps the dependency set upstream
+         tested, and the spec builds in cargo's locked mode against it.
+         Updates come with new upstream releases of the addons. -->
+    <service name="tar_scm" mode="manual">
+        <param name="scm">git</param>
+        <param name="url">https://github.com/rolldown/rolldown.git</param>
+        <param name="revision">v1.2.8</param>
+        <param name="version">1.2.8</param>
+        <param name="filename">rolldown</param>
+    </service>
+    <service name="recompress" mode="manual">
+        <param name="file">rolldown-1.2.8.tar</param>
+        <param name="compression">zst</param>
+    </service>
+    <service name="cargo_vendor" mode="manual">
+        <param name="src">rolldown-1.2.8.tar.zst</param>
+        <param name="tag">rolldown</param>
+        <param name="compression">zst</param>
+        <param name="filter">true</param>
+        <param name="respect-lockfile">true</param>
+        <param name="update">false</param>
+    </service>
+    <service name="tar_scm" mode="manual">
+        <param name="scm">git</param>
+        <param 
name="url">https://github.com/tailwindlabs/tailwindcss.git</param>
+        <param name="revision">v4.3.3</param>
+        <param name="version">4.3.3</param>
+        <param name="filename">tailwindcss</param>
+    </service>
+    <service name="recompress" mode="manual">
+        <param name="file">tailwindcss-4.3.3.tar</param>
+        <param name="compression">zst</param>
+    </service>
+    <service name="cargo_vendor" mode="manual">
+        <param name="src">tailwindcss-4.3.3.tar.zst</param>
+        <param name="tag">tailwindcss</param>
+        <param name="compression">zst</param>
+        <param name="filter">true</param>
+        <param name="respect-lockfile">true</param>
+        <param name="update">false</param>
+    </service>
+    <service name="tar_scm" mode="manual">
+        <param name="scm">git</param>
+        <param 
name="url">https://github.com/parcel-bundler/lightningcss.git</param>
+        <param name="revision">v1.33.0</param>
+        <param name="version">1.33.0</param>
+        <param name="filename">lightningcss</param>
+    </service>
+    <service name="recompress" mode="manual">
+        <param name="file">lightningcss-1.33.0.tar</param>
+        <param name="compression">zst</param>
+    </service>
+    <service name="cargo_vendor" mode="manual">
+        <param name="src">lightningcss-1.33.0.tar.zst</param>
+        <param name="tag">lightningcss</param>
+        <param name="compression">zst</param>
+        <param name="filter">true</param>
+        <param name="respect-lockfile">true</param>
+        <param name="update">false</param>
+    </service>
+    <!-- END native-addons -->
 </services>
 

++++++ build.specials.obscpio ++++++
--- old/.gitignore      2026-10-07 17:29:53.000000000 +0200
+++ new/.gitignore      2026-10-24 00:55:07.000000000 +0200
@@ -9,3 +9,7 @@
 _scmsync.obsinfo
 _service:*
 git-bug-*-build/
+# Git clones cached by the tar_scm services for the native addons
+/rolldown/
+/tailwindcss/
+/lightningcss/

++++++ build.specials.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/.gitignore new/.gitignore
--- old/.gitignore      1970-01-01 01:00:00.000000000 +0100
+++ new/.gitignore      2026-10-24 00:55:07.000000000 +0200
@@ -0,0 +1,15 @@
+_build.*
+build.specials.obscpio
+git-bug/
+*.obscpio
+!node_modules.obscpio
+*.osc
+.osc
+.pbuild
+_scmsync.obsinfo
+_service:*
+git-bug-*-build/
+# Git clones cached by the tar_scm services for the native addons
+/rolldown/
+/tailwindcss/
+/lightningcss/

++++++ native-addons.py ++++++
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Derive the Rust-built native npm addons from the npm lock.

Maintainer-side tool, not an rpmbuild step. It reads the versions of the
native addons from package-lock.json, regenerates their source services in
_service between marker comments, and writes the RPM include that declares
the resulting tarballs. Then run `osc service manualrun` to fetch and vendor.
"""
import argparse
import json
from pathlib import Path
import re
import sys

# npm packages whose prebuilt platform bindings the RPM build replaces with
# modules compiled from these upstream Rust workspaces. The Web UI build loads
# exactly these native modules (verified with strace). Fields:
#   npm: package that loads the module; must be installed at the top level
#   url: upstream Git repository; tags are "v<npm version>"
#   crate: Cargo package that builds the Node-API module
#   library: cdylib produced by `cargo build --release`
#   target: installed module path below webui/, without ".<triple>.node"
#   manifest: package.json in the source tree carrying the same name/version
ADDONS = {
    "rolldown": {
        "npm": "rolldown",
        "url": "https://github.com/rolldown/rolldown.git";,
        "crate": "rolldown_binding",
        "library": "librolldown_binding.so",
        "target": "node_modules/rolldown/dist/rolldown-binding",
        "manifest": "packages/rolldown/package.json",
    },
    "tailwindcss": {
        "npm": "@tailwindcss/oxide",
        "url": "https://github.com/tailwindlabs/tailwindcss.git";,
        "crate": "tailwind-oxide",
        "library": "libtailwind_oxide.so",
        "target": "node_modules/@tailwindcss/oxide/tailwindcss-oxide",
        "manifest": "crates/node/package.json",
    },
    "lightningcss": {
        "npm": "lightningcss",
        "url": "https://github.com/parcel-bundler/lightningcss.git";,
        "crate": "lightningcss_node",
        "library": "liblightningcss_node.so",
        "target": "node_modules/lightningcss/lightningcss",
        "manifest": "package.json",
    },
}
NPM_NAMES = {addon["npm"] for addon in ADDONS.values()}
SOURCE_OFFSET = 20000
BEGIN = "<!-- BEGIN native-addons: generated by native-addons.py, do not edit 
-->"
END = "<!-- END native-addons -->"
# XML comments must not contain "--".
UPDATE_NOTE = """\
    <!-- cargo_vendor keeps update=false on purpose. These crates only
         build Node-API modules that run during the RPM build to generate
         the Web UI assets; the modules are not packaged. Vendoring
         upstream's exact Cargo.lock keeps the dependency set upstream
         tested, and the spec builds in cargo's locked mode against it.
         Updates come with new upstream releases of the addons. -->
"""
VERSION = re.compile(r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)")


class AddonError(ValueError):
    """The npm lock or service file cannot be handled safely."""


def lock_versions(lock):
    """Return every version of each addon npm package found in the lock."""
    found = {name: {} for name in NPM_NAMES}
    for path, entry in lock.get("packages", {}).items():
        name = entry.get("name", path.rsplit("node_modules/", 1)[-1])
        if path and name in found:
            found[name][path] = entry
    return found


def addon_versions(lock):
    """Map addon keys to versions; each must be a single top-level install."""
    found = lock_versions(lock)
    result = {}
    for key, addon in ADDONS.items():
        entries = found[addon["npm"]]
        expected = f"node_modules/{addon['npm']}"
        if set(entries) != {expected}:
            raise AddonError(f"{addon['npm']}: expected only {expected}, found 
{sorted(entries)}")
        entry = entries[expected]
        if entry.get("inBundle") or entry.get("link"):
            raise AddonError(f"{addon['npm']}: bundled or linked package is 
unsupported")
        version = entry.get("version", "")
        if not VERSION.fullmatch(version):
            raise AddonError(f"{addon['npm']}: unsupported version {version!r}")
        result[key] = version
    return result


def services(versions):
    blocks = []
    for key, addon in ADDONS.items():
        version = versions[key]
        blocks.append(f"""\
    <service name="tar_scm" mode="manual">
        <param name="scm">git</param>
        <param name="url">{addon['url']}</param>
        <param name="revision">v{version}</param>
        <param name="version">{version}</param>
        <param name="filename">{key}</param>
    </service>
    <service name="recompress" mode="manual">
        <param name="file">{key}-{version}.tar</param>
        <param name="compression">zst</param>
    </service>
    <service name="cargo_vendor" mode="manual">
        <param name="src">{key}-{version}.tar.zst</param>
        <param name="tag">{key}</param>
        <param name="compression">zst</param>
        <param name="filter">true</param>
        <param name="respect-lockfile">true</param>
        <param name="update">false</param>
    </service>""")
    return f"    {BEGIN}\n" + UPDATE_NOTE + "\n".join(blocks) + f"\n    {END}\n"


def update_service(text, versions):
    """Replace the generated region, or insert it before </services>."""
    region = services(versions)
    pattern = re.compile(rf"^[ \t]*{re.escape(BEGIN)}\n.*?^[ 
\t]*{re.escape(END)}\n",
                         re.DOTALL | re.MULTILINE)
    matches = pattern.findall(text)
    if len(matches) > 1:
        raise AddonError("_service contains more than one generated 
native-addons region")
    if matches:
        return pattern.sub(lambda _: region, text)
    if BEGIN in text or END in text:
        raise AddonError("_service contains an incomplete native-addons region")
    if text.count("</services>") != 1:
        raise AddonError("_service must contain exactly one </services>")
    return text.replace("</services>", region + "</services>")


def spec_include(versions):
    lines = ["# Generated by native-addons.py from package-lock.json; do not 
edit.",
             "# Fields: name,version,crate,library,target,manifest,npm-name"]
    records = []
    for key, addon in ADDONS.items():
        fields = [key, versions[key], addon["crate"], addon["library"],
                  addon["target"], addon["manifest"], addon["npm"]]
        if any("," in field or " " in field for field in fields):
            raise AddonError(f"{key}: fields must not contain commas or spaces")
        records.append(",".join(fields))
    lines.append("%global native_addons " + " ".join(records))
    number = SOURCE_OFFSET
    for key in ADDONS:
        for name in (f"{key}-{versions[key]}.tar.zst", f"vendor-{key}.tar.zst"):
            lines.append(f"Source{number}:{' ' * max(1, 8 - 
len(str(number)))}{name}")
            number += 1
    return "\n".join(lines) + "\n"


def stale_files(directory, versions):
    current = {f"{key}-{version}.tar.zst" for key, version in versions.items()}
    return sorted(path.name for key in ADDONS for path in 
directory.glob(f"{key}-*.tar.zst")
                  if path.name not in current)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--lock", type=Path, default=Path("package-lock.json"))
    parser.add_argument("--service", type=Path, default=Path("_service"))
    parser.add_argument("--spec-include", type=Path, 
default=Path("native-addons.spec.inc"))
    args = parser.parse_args()
    try:
        versions = 
addon_versions(json.loads(args.lock.read_text(encoding="utf-8")))
        service = update_service(args.service.read_text(encoding="utf-8"), 
versions)
        args.service.write_text(service, encoding="utf-8")
        args.spec_include.write_text(spec_include(versions), encoding="utf-8")
    except (AddonError, OSError, ValueError) as exc:
        parser.exit(1, f"error: {exc}\n")
    for key, version in versions.items():
        print(f"{ADDONS[key]['npm']} {version}", file=sys.stderr)
    for name in stale_files(args.service.parent, versions):
        print(f"Stale source, remove after the service run: {name}", 
file=sys.stderr)
    print("Next: osc service manualrun", file=sys.stderr)
    return 0


if __name__ == "__main__":
    sys.exit(main())

++++++ native-addons.spec.inc ++++++
# Generated by native-addons.py from package-lock.json; do not edit.
# Fields: name,version,crate,library,target,manifest,npm-name
%global native_addons 
rolldown,1.2.8,rolldown_binding,librolldown_binding.so,node_modules/rolldown/dist/rolldown-binding,packages/rolldown/package.json,rolldown
 
tailwindcss,4.3.3,tailwind-oxide,libtailwind_oxide.so,node_modules/@tailwindcss/oxide/tailwindcss-oxide,crates/node/package.json,@tailwindcss/oxide
 
lightningcss,1.33.0,lightningcss_node,liblightningcss_node.so,node_modules/lightningcss/lightningcss,package.json,lightningcss
Source20000:   rolldown-1.2.8.tar.zst
Source20001:   vendor-rolldown.tar.zst
Source20002:   tailwindcss-4.3.3.tar.zst
Source20003:   vendor-tailwindcss.tar.zst
Source20004:   lightningcss-1.33.0.tar.zst
Source20005:   vendor-lightningcss.tar.zst

++++++ node-sources-full.json ++++++
++++ 11673 lines (skipped)

++++++ node-sources.json ++++++
++++ 3778 lines (skipped)
++++ between /work/SRC/openSUSE:Factory/git-bug/node-sources.json
++++ and /work/SRC/openSUSE:Factory/.git-bug.new.2252680/node-sources.json

++++++ node_modules.obscpio ++++++
/work/SRC/openSUSE:Factory/git-bug/node_modules.obscpio 
/work/SRC/openSUSE:Factory/.git-bug.new.2252680/node_modules.obscpio differ: 
char 59, line 1

++++++ node_modules.spec.inc ++++++
++++ 2031 lines (skipped)
++++ between /work/SRC/openSUSE:Factory/git-bug/node_modules.spec.inc
++++ and /work/SRC/openSUSE:Factory/.git-bug.new.2252680/node_modules.spec.inc

++++++ package-lock.json ++++++
--- /var/tmp/diff_new_pack.ktat8e/_old  2026-10-24 22:06:40.816772766 +0200
+++ /var/tmp/diff_new_pack.ktat8e/_new  2026-10-24 22:06:40.822773019 +0200
@@ -8694,8 +8694,8 @@
       }
     },
     "node_modules/fast-uri": {
-      "version": "3.1.7",
-      "integrity": 
"sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
+      "version": "3.1.9",
+      "integrity": 
"sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==",
       "funding": [
         {
           "type": "github",
@@ -10055,8 +10055,8 @@
       }
     },
     "node_modules/lightningcss": {
-      "version": "1.32.0",
-      "integrity": 
"sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+      "version": "1.33.0",
+      "integrity": 
"sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
       "dev": true,
       "license": "MPL-2.0",
       "dependencies": {
@@ -10070,22 +10070,22 @@
         "url": "https://opencollective.com/parcel";
       },
       "optionalDependencies": {
-        "lightningcss-android-arm64": "1.32.0",
-        "lightningcss-darwin-arm64": "1.32.0",
-        "lightningcss-darwin-x64": "1.32.0",
-        "lightningcss-freebsd-x64": "1.32.0",
-        "lightningcss-linux-arm-gnueabihf": "1.32.0",
-        "lightningcss-linux-arm64-gnu": "1.32.0",
-        "lightningcss-linux-arm64-musl": "1.32.0",
-        "lightningcss-linux-x64-gnu": "1.32.0",
-        "lightningcss-linux-x64-musl": "1.32.0",
-        "lightningcss-win32-arm64-msvc": "1.32.0",
-        "lightningcss-win32-x64-msvc": "1.32.0"
+        "lightningcss-android-arm64": "1.33.0",
+        "lightningcss-darwin-arm64": "1.33.0",
+        "lightningcss-darwin-x64": "1.33.0",
+        "lightningcss-freebsd-x64": "1.33.0",
+        "lightningcss-linux-arm-gnueabihf": "1.33.0",
+        "lightningcss-linux-arm64-gnu": "1.33.0",
+        "lightningcss-linux-arm64-musl": "1.33.0",
+        "lightningcss-linux-x64-gnu": "1.33.0",
+        "lightningcss-linux-x64-musl": "1.33.0",
+        "lightningcss-win32-arm64-msvc": "1.33.0",
+        "lightningcss-win32-x64-msvc": "1.33.0"
       }
     },
     "node_modules/lightningcss-android-arm64": {
-      "version": "1.32.0",
-      "integrity": 
"sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+      "version": "1.33.0",
+      "integrity": 
"sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
       "cpu": [
         "arm64"
       ],
@@ -10104,8 +10104,8 @@
       }
     },
     "node_modules/lightningcss-darwin-arm64": {
-      "version": "1.32.0",
-      "integrity": 
"sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+      "version": "1.33.0",
+      "integrity": 
"sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
       "cpu": [
         "arm64"
       ],
@@ -10124,8 +10124,8 @@
       }
     },
     "node_modules/lightningcss-darwin-x64": {
-      "version": "1.32.0",
-      "integrity": 
"sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+      "version": "1.33.0",
+      "integrity": 
"sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
       "cpu": [
         "x64"
       ],
@@ -10144,8 +10144,8 @@
       }
     },
     "node_modules/lightningcss-freebsd-x64": {
-      "version": "1.32.0",
-      "integrity": 
"sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+      "version": "1.33.0",
+      "integrity": 
"sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
       "cpu": [
         "x64"
       ],
@@ -10164,8 +10164,8 @@
       }
     },
     "node_modules/lightningcss-linux-arm-gnueabihf": {
-      "version": "1.32.0",
-      "integrity": 
"sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+      "version": "1.33.0",
+      "integrity": 
"sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
       "cpu": [
         "arm"
       ],
@@ -10184,8 +10184,8 @@
       }
     },
     "node_modules/lightningcss-linux-arm64-gnu": {
-      "version": "1.32.0",
-      "integrity": 
"sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+      "version": "1.33.0",
+      "integrity": 
"sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
       "cpu": [
         "arm64"
       ],
@@ -10207,8 +10207,8 @@
       }
     },
     "node_modules/lightningcss-linux-arm64-musl": {
-      "version": "1.32.0",
-      "integrity": 
"sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+      "version": "1.33.0",
+      "integrity": 
"sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
       "cpu": [
         "arm64"
       ],
@@ -10230,8 +10230,8 @@
       }
     },
     "node_modules/lightningcss-linux-x64-gnu": {
-      "version": "1.32.0",
-      "integrity": 
"sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+      "version": "1.33.0",
+      "integrity": 
"sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
       "cpu": [
         "x64"
       ],
@@ -10253,8 +10253,8 @@
       }
     },
     "node_modules/lightningcss-linux-x64-musl": {
-      "version": "1.32.0",
-      "integrity": 
"sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+      "version": "1.33.0",
+      "integrity": 
"sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
       "cpu": [
         "x64"
       ],
@@ -10276,8 +10276,8 @@
       }
     },
     "node_modules/lightningcss-win32-arm64-msvc": {
-      "version": "1.32.0",
-      "integrity": 
"sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+      "version": "1.33.0",
+      "integrity": 
"sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
       "cpu": [
         "arm64"
       ],
@@ -10296,8 +10296,8 @@
       }
     },
     "node_modules/lightningcss-win32-x64-msvc": {
-      "version": "1.32.0",
-      "integrity": 
"sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+      "version": "1.33.0",
+      "integrity": 
"sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
       "cpu": [
         "x64"
       ],
@@ -12532,8 +12532,8 @@
       }
     },
     "node_modules/qs": {
-      "version": "6.16.0",
-      "integrity": 
"sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
+      "version": "6.16.1",
+      "integrity": 
"sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==",
       "license": "BSD-3-Clause",
       "dependencies": {
         "es-define-property": "^1.0.1",
@@ -14637,267 +14637,6 @@
         "vite": ">=3.0.0"
       }
     },
-    "node_modules/vite/node_modules/lightningcss": {
-      "version": "1.33.0",
-      "integrity": 
"sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
-      "dev": true,
-      "license": "MPL-2.0",
-      "dependencies": {
-        "detect-libc": "^2.0.3"
-      },
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel";
-      },
-      "optionalDependencies": {
-        "lightningcss-android-arm64": "1.33.0",
-        "lightningcss-darwin-arm64": "1.33.0",
-        "lightningcss-darwin-x64": "1.33.0",
-        "lightningcss-freebsd-x64": "1.33.0",
-        "lightningcss-linux-arm-gnueabihf": "1.33.0",
-        "lightningcss-linux-arm64-gnu": "1.33.0",
-        "lightningcss-linux-arm64-musl": "1.33.0",
-        "lightningcss-linux-x64-gnu": "1.33.0",
-        "lightningcss-linux-x64-musl": "1.33.0",
-        "lightningcss-win32-arm64-msvc": "1.33.0",
-        "lightningcss-win32-x64-msvc": "1.33.0"
-      }
-    },
-    "node_modules/vite/node_modules/lightningcss-android-arm64": {
-      "version": "1.33.0",
-      "integrity": 
"sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MPL-2.0",
-      "optional": true,
-      "os": [
-        "android"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel";
-      }
-    },
-    "node_modules/vite/node_modules/lightningcss-darwin-arm64": {
-      "version": "1.33.0",
-      "integrity": 
"sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MPL-2.0",
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel";
-      }
-    },
-    "node_modules/vite/node_modules/lightningcss-darwin-x64": {
-      "version": "1.33.0",
-      "integrity": 
"sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MPL-2.0",
-      "optional": true,
-      "os": [
-        "darwin"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel";
-      }
-    },
-    "node_modules/vite/node_modules/lightningcss-freebsd-x64": {
-      "version": "1.33.0",
-      "integrity": 
"sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MPL-2.0",
-      "optional": true,
-      "os": [
-        "freebsd"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel";
-      }
-    },
-    "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": {
-      "version": "1.33.0",
-      "integrity": 
"sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
-      "cpu": [
-        "arm"
-      ],
-      "dev": true,
-      "license": "MPL-2.0",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel";
-      }
-    },
-    "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": {
-      "version": "1.33.0",
-      "integrity": 
"sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "libc": [
-        "glibc"
-      ],
-      "license": "MPL-2.0",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel";
-      }
-    },
-    "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": {
-      "version": "1.33.0",
-      "integrity": 
"sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "libc": [
-        "musl"
-      ],
-      "license": "MPL-2.0",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel";
-      }
-    },
-    "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": {
-      "version": "1.33.0",
-      "integrity": 
"sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "libc": [
-        "glibc"
-      ],
-      "license": "MPL-2.0",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel";
-      }
-    },
-    "node_modules/vite/node_modules/lightningcss-linux-x64-musl": {
-      "version": "1.33.0",
-      "integrity": 
"sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "libc": [
-        "musl"
-      ],
-      "license": "MPL-2.0",
-      "optional": true,
-      "os": [
-        "linux"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel";
-      }
-    },
-    "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": {
-      "version": "1.33.0",
-      "integrity": 
"sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
-      "cpu": [
-        "arm64"
-      ],
-      "dev": true,
-      "license": "MPL-2.0",
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel";
-      }
-    },
-    "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": {
-      "version": "1.33.0",
-      "integrity": 
"sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
-      "cpu": [
-        "x64"
-      ],
-      "dev": true,
-      "license": "MPL-2.0",
-      "optional": true,
-      "os": [
-        "win32"
-      ],
-      "engines": {
-        "node": ">= 12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/parcel";
-      }
-    },
     "node_modules/vitest": {
       "version": "4.1.11",
       "integrity": 
"sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==",

++++++ prepare-webui-lock.py ++++++
--- /var/tmp/diff_new_pack.ktat8e/_old  2026-10-24 22:06:40.859774576 +0200
+++ /var/tmp/diff_new_pack.ktat8e/_new  2026-10-24 22:06:40.863774744 +0200
@@ -5,6 +5,10 @@
 Maintainer-side tool, not an rpmbuild step. Requires Python/PyYAML, Node/npm,
 local-npm-registry, and Linux unshare with unprivileged user namespaces 
enabled.
 The npm resolver runs in a new network namespace with only loopback enabled.
+
+Resolution uses the complete source inventory. The RPM build needs only the
+sources npm selected, minus optional platform-specific packages: the RPM
+compiles the native addons it needs from Rust sources (see native-addons.py).
 """
 import argparse
 import fcntl
@@ -24,7 +28,9 @@
 import yaml
 
 CONVERTER = runpy.run_path(str(Path(__file__).with_name("pnpm-to-obs.py")))
+NATIVE = runpy.run_path(str(Path(__file__).with_name("native-addons.py")))
 Error = CONVERTER["ConversionError"]
+PLATFORM_FIELDS = ("os", "cpu", "libc")
 
 
 def build_manifest(package, lock):
@@ -107,6 +113,44 @@
             "not_selected_by_npm": sorted(set(inventory) - selected)}
 
 
+def native_overrides(lock):
+    """Pin each multi-version native addon to its highest locked version.
+
+    The RPM compiles one module per addon, so npm must install only one copy.
+    """
+    result = {}
+    for name, entries in NATIVE["lock_versions"](lock).items():
+        versions = {entry["version"] for entry in entries.values()}
+        if len(versions) > 1:
+            result[name] = max(versions, key=lambda value: tuple(map(int, 
value.split("."))))
+    return result
+
+
+def build_inventory(lock, inventory):
+    """Return the sources the RPM build installs.
+
+    Keep every source npm selected, except identities whose every lock entry is
+    an optional package restricted to a platform. npm skips such packages when
+    the registry lacks them. Required packages that merely list supported
+    platforms (for example systeminformation) stay.
+    """
+    required = set()
+    platform = set()
+    for path, entry in lock["packages"].items():
+        if not path or entry.get("inBundle"):
+            continue
+        identity = f"{entry.get('name', path.rsplit('node_modules/', 
1)[-1])}@{entry['version']}"
+        if entry.get("optional") and any(field in entry for field in 
PLATFORM_FIELDS):
+            platform.add(identity)
+        else:
+            required.add(identity)
+    excluded = platform - required
+    missing = required - set(inventory)
+    if missing:
+        raise Error(f"Npm selected sources absent from the inventory: 
{sorted(missing)}")
+    return {key: inventory[key] for key in sorted(required)}, sorted(excluded)
+
+
 def enable_loopback():
     # Executed only after unshare creates the private namespace.
     with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
@@ -167,6 +211,9 @@
     parser.add_argument("--registry-helper", nargs="+", 
default=["local-npm-registry"],
                         help="helper command; defaults to local-npm-registry")
     parser.add_argument("--timeout", type=int, default=180)
+    parser.add_argument("--security-inventory", type=Path,
+                        help="reviewed complete inventory with fast-uri/qs 
security updates "
+                             "(node-sources-full.json)")
     parser.add_argument("--_in-namespace", action="store_true", 
help=argparse.SUPPRESS)
     args = parser.parse_args()
     try:
@@ -174,21 +221,47 @@
             result = subprocess.run(["unshare", "-Urn", sys.executable,
                                      str(Path(__file__).resolve()), 
*sys.argv[1:], "--_in-namespace"])
             return result.returncode
+        if args.security_inventory and (args.security_inventory.resolve()
+                                        == (args.output_dir / 
"node-sources.json").resolve()):
+            raise Error("The security inventory must be the complete 
node-sources-full.json; "
+                        "node-sources.json is regenerated as the reduced build 
inventory")
         enable_loopback()
         lock = yaml.load((args.webui / "pnpm-lock.yaml").read_text(),
                          Loader=CONVERTER["UniqueKeyLoader"])
         inventory = CONVERTER["convert"](lock)
+        if args.security_inventory:
+            reviewed = json.loads(args.security_inventory.read_text())
+            removed = {"[email protected]", "[email protected]"}
+            added = {"[email protected]", "[email protected]"}
+            if set(inventory) - set(reviewed) != removed or set(reviewed) - 
set(inventory) != added:
+                raise Error("Security inventory must only replace fast-uri and 
qs")
+            if any(reviewed[key] != value for key, value in inventory.items() 
if key not in removed):
+                raise Error("Security inventory changed unrelated sources")
+            inventory = reviewed
         tarballs = args.tarballs.resolve()
         CONVERTER["verify_downloads"](inventory, tarballs)
         manifest = build_manifest(json.loads((args.webui / 
"package.json").read_text()), lock)
+        if args.security_inventory:
+            manifest["overrides"].update({"fast-uri": "3.1.9", "qs": "6.16.1"})
         npm_lock = resolve(manifest, tarballs, args.registry_helper, 
args.timeout)
+        unify = native_overrides(npm_lock)
+        if unify:
+            print(f"Unifying native addons: {unify}", file=sys.stderr)
+            manifest["overrides"].update(unify)
+            npm_lock = resolve(manifest, tarballs, args.registry_helper, 
args.timeout)
+        NATIVE["addon_versions"](npm_lock)
         report = validate_lock(npm_lock, inventory, tarballs)
+        sources, excluded = build_inventory(npm_lock, inventory)
+        report["native_overrides"] = unify
+        report["build_source_identities"] = len(sources)
+        report["excluded_platform_packages"] = excluded
         args.output_dir.mkdir(parents=True, exist_ok=True)
         for name, value in (("webui-package.json", manifest), 
("package-lock.json", npm_lock),
-                            ("webui-lock-report.json", report)):
+                            ("webui-lock-report.json", report), 
("node-sources.json", sources)):
             (args.output_dir / name).write_text(json.dumps(value, indent=2) + 
"\n")
         print(f"Validated {report['source_identities']} tarball identities and 
"
-              f"{report['bundled_entries']} bundled entries. Output: 
{args.output_dir}")
+              f"{report['bundled_entries']} bundled entries; the build needs "
+              f"{len(sources)}. Output: {args.output_dir}")
         return 0
     except (Error, OSError, ValueError, KeyError, tarfile.TarError,
             subprocess.SubprocessError, yaml.YAMLError) as exc:

++++++ test_native_addons.py ++++++
# SPDX-License-Identifier: MIT
"""Unit tests for native-addons.py. No network access."""
from pathlib import Path
import runpy
import tempfile
import unittest

NATIVE = runpy.run_path(str(Path(__file__).with_name("native-addons.py")))

SERVICE = """<services>
    <service name="go_modules" mode="manual"/>
</services>
"""


def lock(**overrides):
    packages = {"": {"name": "webui"},
                "node_modules/rolldown": {"version": "1.2.8"},
                "node_modules/@tailwindcss/oxide": {"version": "4.3.3"},
                "node_modules/lightningcss": {"version": "1.33.0"},
                "node_modules/lightningcss-linux-x64-gnu": {"version": "1.33.0",
                                                           "optional": True}}
    packages.update(overrides)
    return {"lockfileVersion": 3, "packages": packages}


class NativeAddonTests(unittest.TestCase):
    def test_versions_from_lock(self):
        self.assertEqual(NATIVE["addon_versions"](lock()),
                         {"rolldown": "1.2.8", "tailwindcss": "4.3.3", 
"lightningcss": "1.33.0"})

    def test_reject_duplicate_or_nested_installation(self):
        nested = {"node_modules/vite/node_modules/lightningcss": {"version": 
"1.32.0"}}
        cases = {"duplicate": lock(**nested),
                 "missing": lock(**{"node_modules/rolldown": None}),
                 "bundled": lock(**{"node_modules/rolldown": {"version": 
"1.2.8",
                                                              "inBundle": 
True}}),
                 "prerelease": lock(**{"node_modules/rolldown": {"version": 
"1.2.8-rc.1"}})}
        cases["missing"]["packages"].pop("node_modules/rolldown")
        for name, value in cases.items():
            with self.subTest(name), self.assertRaises(ValueError):
                NATIVE["addon_versions"](value)

    def test_service_region_insert_and_replace(self):
        versions = NATIVE["addon_versions"](lock())
        first = NATIVE["update_service"](SERVICE, versions)
        self.assertIn("go_modules", first)
        self.assertIn("<param name=\"revision\">v1.2.8</param>", first)
        self.assertIn("<param 
name=\"src\">lightningcss-1.33.0.tar.zst</param>", first)
        self.assertIn("keeps update=false on purpose", first)
        self.assertNotIn("--", first.replace("<!--", "").replace("-->", ""))
        self.assertTrue(first.rstrip().endswith("</services>"))
        self.assertEqual(NATIVE["update_service"](first, versions), first)
        newer = NATIVE["update_service"](first, dict(versions, 
rolldown="1.3.0"))
        self.assertIn("v1.3.0", newer)
        self.assertNotIn("v1.2.8", newer)
        self.assertEqual(newer.count(NATIVE["BEGIN"]), 1)

    def test_reject_damaged_region(self):
        versions = NATIVE["addon_versions"](lock())
        damaged = SERVICE.replace("</services>", f"    
{NATIVE['BEGIN']}\n</services>")
        with self.assertRaisesRegex(ValueError, "incomplete"):
            NATIVE["update_service"](damaged, versions)

    def test_spec_include(self):
        text = NATIVE["spec_include"](NATIVE["addon_versions"](lock()))
        self.assertIn("rolldown,1.2.8,rolldown_binding,librolldown_binding.so,"
                      "node_modules/rolldown/dist/rolldown-binding,"
                      "packages/rolldown/package.json,rolldown", text)
        self.assertIn("Source20000:   rolldown-1.2.8.tar.zst\n", text)
        self.assertIn("Source20005:   vendor-lightningcss.tar.zst\n", text)
        self.assertEqual(text.count("%global"), 1)

    def test_stale_files(self):
        with tempfile.TemporaryDirectory() as directory:
            path = Path(directory)
            for name in ("rolldown-1.2.7.tar.zst", "rolldown-1.2.8.tar.zst", 
"other-1.tar.zst"):
                (path / name).touch()
            self.assertEqual(NATIVE["stale_files"](path, {"rolldown": "1.2.8"}),
                             ["rolldown-1.2.7.tar.zst"])


if __name__ == "__main__":
    unittest.main()

++++++ test_prepare_webui_lock.py ++++++
--- /var/tmp/diff_new_pack.ktat8e/_old  2026-10-24 22:06:40.939777943 +0200
+++ /var/tmp/diff_new_pack.ktat8e/_new  2026-10-24 22:06:40.945778195 +0200
@@ -88,6 +88,49 @@
             
lock["packages"]["node_modules/example/node_modules/child"]["version"] = "3.0.0"
             with self.assertRaisesRegex(ValueError, "Bundled manifest 
disagrees"):
                 HELPER["validate_lock"](lock, self.inventory(), path)
+    def test_build_inventory_drops_only_optional_platform_packages(self):
+        inventory = {f"{name}@1.0.0": {"name": name} for name in (
+            "binding-linux", "systeminformation", "plain", "dev-required", 
"unused")}
+        lock = {"packages": {
+            "": {"name": "test"},
+            "node_modules/binding-linux": {"version": "1.0.0", "optional": 
True,
+                                            "dev": True, "os": ["linux"], 
"cpu": ["x64"]},
+            # Required pure-JS packages may still declare supported platforms.
+            "node_modules/systeminformation": {"version": "1.0.0", "os": 
["linux"]},
+            # devOptional is required for a development install.
+            "node_modules/dev-required": {"version": "1.0.0", "devOptional": 
True,
+                                           "cpu": ["x64"]},
+            "node_modules/plain": {"version": "1.0.0"},
+            "node_modules/plain/node_modules/inner": {"version": "9.0.0", 
"inBundle": True}}}
+        sources, excluded = HELPER["build_inventory"](lock, inventory)
+        self.assertEqual(set(sources), {"[email protected]", 
"[email protected]",
+                                        "[email protected]"})
+        self.assertEqual(excluded, ["[email protected]"])
+
+    def test_build_inventory_keeps_identity_required_elsewhere(self):
+        inventory = {"[email protected]": {}}
+        lock = {"packages": {
+            "node_modules/a/node_modules/shared": {"version": "1.0.0", 
"optional": True,
+                                                    "os": ["linux"]},
+            "node_modules/shared": {"version": "1.0.0"}}}
+        sources, excluded = HELPER["build_inventory"](lock, inventory)
+        self.assertEqual((list(sources), excluded), (["[email protected]"], []))
+
+    def test_build_inventory_rejects_unknown_source(self):
+        lock = {"packages": {"node_modules/x": {"version": "1.0.0"}}}
+        with self.assertRaisesRegex(ValueError, "absent from the inventory"):
+            HELPER["build_inventory"](lock, {})
+
+    def test_native_overrides_pick_highest_version(self):
+        lock = {"packages": {
+            "node_modules/lightningcss": {"version": "1.32.0"},
+            "node_modules/vite/node_modules/lightningcss": {"version": 
"1.33.0"},
+            "node_modules/x/node_modules/lightningcss": {"version": "1.9.10"},
+            "node_modules/rolldown": {"version": "1.2.8"}}}
+        self.assertEqual(HELPER["native_overrides"](lock), {"lightningcss": 
"1.33.0"})
+        del lock["packages"]["node_modules/vite/node_modules/lightningcss"]
+        del lock["packages"]["node_modules/x/node_modules/lightningcss"]
+        self.assertEqual(HELPER["native_overrides"](lock), {})
 
 
 if __name__ == "__main__":

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/git-bug/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.git-bug.new.2252680/vendor.tar.gz differ: char 33, 
line 1

++++++ webui-lock-report.json ++++++
--- /var/tmp/diff_new_pack.ktat8e/_old  2026-10-24 22:06:40.997780384 +0200
+++ /var/tmp/diff_new_pack.ktat8e/_new  2026-10-24 22:06:41.001780552 +0200
@@ -1,6 +1,6 @@
 {
-  "source_identities": 1040,
-  "installation_entries": 1088,
+  "source_identities": 1028,
+  "installation_entries": 1076,
   "bundled_entries": 6,
   "not_selected_by_npm": [
     "@adobe/[email protected]",
@@ -87,6 +87,18 @@
     "[email protected]",
     "[email protected]",
     "[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]",
     "[email protected]",
     "[email protected]",
     "[email protected]",
@@ -130,6 +142,180 @@
     "[email protected]",
     "[email protected]",
     "[email protected]"
+  ],
+  "native_overrides": {
+    "lightningcss": "1.33.0"
+  },
+  "build_source_identities": 860,
+  "excluded_platform_packages": [
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@esbuild/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-parser/[email protected]",
+    "@oxc-resolver/[email protected]",
+    "@oxc-resolver/[email protected]",
+    "@oxc-resolver/[email protected]",
+    "@oxc-resolver/[email protected]",
+    "@oxc-resolver/[email protected]",
+    "@oxc-resolver/[email protected]",
+    "@oxc-resolver/[email protected]",
+    "@oxc-resolver/[email protected]",
+    "@oxc-resolver/[email protected]",
+    "@oxc-resolver/[email protected]",
+    "@oxc-resolver/[email protected]",
+    "@oxc-resolver/[email protected]",
+    "@oxc-resolver/[email protected]",
+    "@oxc-resolver/[email protected]",
+    "@oxc-resolver/[email protected]",
+    "@oxc-resolver/[email protected]",
+    "@oxc-resolver/[email protected]",
+    "@oxc-resolver/[email protected]",
+    "@oxc-resolver/[email protected]",
+    "@oxfmt/[email protected]",
+    "@oxfmt/[email protected]",
+    "@oxfmt/[email protected]",
+    "@oxfmt/[email protected]",
+    "@oxfmt/[email protected]",
+    "@oxfmt/[email protected]",
+    "@oxfmt/[email protected]",
+    "@oxfmt/[email protected]",
+    "@oxfmt/[email protected]",
+    "@oxfmt/[email protected]",
+    "@oxfmt/[email protected]",
+    "@oxfmt/[email protected]",
+    "@oxfmt/[email protected]",
+    "@oxfmt/[email protected]",
+    "@oxfmt/[email protected]",
+    "@oxfmt/[email protected]",
+    "@oxfmt/[email protected]",
+    "@oxfmt/[email protected]",
+    "@oxfmt/[email protected]",
+    "@oxlint-tsgolint/[email protected]",
+    "@oxlint-tsgolint/[email protected]",
+    "@oxlint-tsgolint/[email protected]",
+    "@oxlint-tsgolint/[email protected]",
+    "@oxlint-tsgolint/[email protected]",
+    "@oxlint-tsgolint/[email protected]",
+    "@oxlint/[email protected]",
+    "@oxlint/[email protected]",
+    "@oxlint/[email protected]",
+    "@oxlint/[email protected]",
+    "@oxlint/[email protected]",
+    "@oxlint/[email protected]",
+    "@oxlint/[email protected]",
+    "@oxlint/[email protected]",
+    "@oxlint/[email protected]",
+    "@oxlint/[email protected]",
+    "@oxlint/[email protected]",
+    "@oxlint/[email protected]",
+    "@oxlint/[email protected]",
+    "@oxlint/[email protected]",
+    "@oxlint/[email protected]",
+    "@oxlint/[email protected]",
+    "@oxlint/[email protected]",
+    "@oxlint/[email protected]",
+    "@oxlint/[email protected]",
+    "@rolldown/[email protected]",
+    "@rolldown/[email protected]",
+    "@rolldown/[email protected]",
+    "@rolldown/[email protected]",
+    "@rolldown/[email protected]",
+    "@rolldown/[email protected]",
+    "@rolldown/[email protected]",
+    "@rolldown/[email protected]",
+    "@rolldown/[email protected]",
+    "@rolldown/[email protected]",
+    "@rolldown/[email protected]",
+    "@rolldown/[email protected]",
+    "@rolldown/[email protected]",
+    "@rolldown/[email protected]",
+    "@rolldown/[email protected]",
+    "@tailwindcss/[email protected]",
+    "@tailwindcss/[email protected]",
+    "@tailwindcss/[email protected]",
+    "@tailwindcss/[email protected]",
+    "@tailwindcss/[email protected]",
+    "@tailwindcss/[email protected]",
+    "@tailwindcss/[email protected]",
+    "@tailwindcss/[email protected]",
+    "@tailwindcss/[email protected]",
+    "@tailwindcss/[email protected]",
+    "@tailwindcss/[email protected]",
+    "@tailwindcss/[email protected]",
+    "@typescript/[email protected]",
+    "@typescript/[email protected]",
+    "@typescript/[email protected]",
+    "@typescript/[email protected]",
+    "@typescript/[email protected]",
+    "@typescript/[email protected]",
+    "@typescript/[email protected]",
+    "@typescript/[email protected]",
+    "@typescript/[email protected]",
+    "@typescript/[email protected]",
+    "@typescript/[email protected]",
+    "@typescript/[email protected]",
+    "@typescript/[email protected]",
+    "@typescript/[email protected]",
+    "@typescript/[email protected]",
+    "@typescript/[email protected]",
+    "@typescript/[email protected]",
+    "@typescript/[email protected]",
+    "@typescript/[email protected]",
+    "@typescript/[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]",
+    "[email protected]"
   ]
 }
 

++++++ webui-package.json ++++++
--- /var/tmp/diff_new_pack.ktat8e/_old  2026-10-24 22:06:41.023781478 +0200
+++ /var/tmp/diff_new_pack.ktat8e/_new  2026-10-24 22:06:41.027781647 +0200
@@ -91,7 +91,10 @@
   },
   "overrides": {
     "js-yaml@4": "^4.3.2",
-    "postcss-selector-parser": "7.1.6"
+    "postcss-selector-parser": "7.1.6",
+    "fast-uri": "3.1.9",
+    "qs": "6.16.1",
+    "lightningcss": "1.33.0"
   }
 }
 

Reply via email to