# version = null -> whatever the lock holds; `lock.sh ` bumps it # version = "x.y.z" -> hard pin; lock.sh resolves exactly that version and # eval fails if the lock disagrees { config, lib, pkgs, ... }: let inherit (lib) mapAttrs' mapAttrsToList nameValuePair optionalAttrs; cfg = config.ryan.zen; exts = lib.filterAttrs (_: e: e.enable) cfg.extensions; zen = config.programs.zen-browser; lockFile = ./extensions.lock.json; lock = lib.importJSON lockFile; # Validated lock entry. Throws (not assertions) so the message surfaces no # matter which consumer forces the policy first (HM wrapper, darwin defaults). locked = name: e: let l = lock.${name} or (throw "zen extension '${name}' has no entry in extensions.lock.json; run modules/zen/lock.sh"); in if l.id != e.id then throw "zen extension '${name}': lock has id ${l.id}, config has ${e.id}; re-run lock.sh" else if e.version != null && l.version != e.version then throw "zen extension '${name}': pinned ${e.version}, lock has ${l.version}; re-run lock.sh" else l; xpi = name: e: let l = locked name e; in pkgs.fetchurl { name = "zen-ext-${name}-${l.version}.xpi"; inherit (l) url hash; }; settingsFor = name: e: { install_url = "file://${xpi name e}"; installation_mode = e.mode; updates_disabled = true; } // optionalAttrs (e.privateBrowsing != null) { private_browsing = e.privateBrowsing; } // e.extraSettings; pins = pkgs.writeText "zen-extension-pins.json" (builtins.toJSON (mapAttrsToList (name: e: { inherit (e) id; version = (locked name e).version; }) exts)); jq = lib.getExe pkgs.jq; sort = "${pkgs.coreutils}/bin/sort"; in { config = lib.mkIf (cfg.enable && exts != { }) { programs.zen-browser.policies = { # Locks extensions.update.enabled=false; belt to updates_disabled's braces. ExtensionUpdate = false; ExtensionSettings = mapAttrs' (name: e: nameValuePair e.id (settingsFor name e)) exts // optionalAttrs cfg.exclusive { "*".installation_mode = "blocked"; }; }; # Explicit GC root for every pinned XPI, independent of how the policy # gets serialized on each platform. Also a handy place to inspect them. home.file.".local/share/zen-extensions".source = pkgs.linkFarm "zen-extensions" (mapAttrsToList (name: e: { name = "${name}.xpi"; path = xpi name e; }) exts); # The policy engine refuses downgrades (installed > pinned is cancelled in # PoliciesHelpers installAddonFromURL). For exactly that case, delete the # installed XPI while Zen is closed; the next start drops it from the DB # and the policy installs the pin. Upgrades are left to the policy engine. programs.zen-browser.activationFragments.default = [{ requiresLock = true; skipSubject = "zen extension downgrade check"; text = '' prof="${zen.profilesPath}/${zen.profiles.default.path}" if [ -f "$prof/extensions.json" ]; then while IFS=$'\t' read -r id want have; do [ -n "$have" ] && [ "$have" != "$want" ] || continue newest="$(printf '%s\n%s\n' "$want" "$have" | ${sort} -V | tail -n1)" [ "$newest" = "$have" ] || continue echo "zen-extensions: $id installed $have > pinned $want; removing installed XPI" [[ -v DRY_RUN ]] || rm -f "$prof/extensions/$id.xpi" done < <(${jq} -r --slurpfile ej "$prof/extensions.json" ' .[] | . as $p | [ $p.id, $p.version, ([ $ej[0].addons[] | select(.id == $p.id and .location == "app-profile") | .version ][0] // "") ] | @tsv' ${pins}) fi ''; }]; }; }