`hasOwnKey`, `isUnsafeObjectKey`, `copyObject` and `shellQuote` were written out in full in up to seven files each. AGENTS.md called that legitimate because QML and JS files share no function scope. The scoping constraint is real, but it only forces each file to declare the name; it does not force each file to carry the body. The repo already crosses every surface, config pages included, with eleven `.pragma library` modules. Move the bodies into Guards.js and leave a one-line delegation behind. Call sites are untouched, which is why the diff is narrow for 124 of them. Unifying turned up two divergences the copies had been hiding: * `isUnsafeObjectKey` existed in two forms. main.qml, ProviderNormalizer.js and configProviders.qml coerced the key with `String(key || "")` first; CostPresentation.js and CommandLedger.js compared the raw value, so a non-string key that stringifies to `__proto__` would have slipped past. Those two are mine, from the previous refactors. The shared version keeps the coercing form. * configDisplay.qml inlined its own `copyObject` and three bare `Object.prototype.hasOwnProperty.call` checks. They carried the right guard, so this is a consolidation and not a fix. tests/tst_guards.qml adds 18 cases for logic that had never been executed by a test: a payload shadowing `hasOwnProperty`, prototype-polluting keys surviving a copy, and shell quoting against `$(...)`, backticks, `&&` and a quote breakout. test_security_regressions.sh keeps requiring the declaration in every calling file, and now also fails if a guard implementation appears anywhere outside Guards.js. Verified by re-inlining `hasOwnKey` in ProviderIdentity.js: the check exits 1 and names the file.
Simone committed on 8/19/2026, 1:04:59 PMparent63f5fd6
13 files changedLine totals unavailable