From 275ac3bf3fa34ae708d3aa9a007addd9fa89c919 Mon Sep 17 00:00:00 2001 From: elwrcl Date: Tue, 25 Aug 2026 06:01:18 +0300 Subject: [PATCH] uhooo --- README.md | 35 ++++++++++++++--- modules/home-manager.nix | 83 ++++++++++++++++++++++++++++++++-------- 2 files changed, 95 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 8bb93e5..b332480 100644 --- a/README.md +++ b/README.md @@ -81,11 +81,8 @@ Then configure it: package = helium.packages.${pkgs.system}.helium-widevine; extensions = [ - # uBlock Origin — see "Obtaining extensions" below - { - id = "cjpalhdlnbpafiamejdnhcphjbkeiagm"; - hash = "sha256-FIbmYVj8cmXce7Vq4h7d2nOjmk4RkCnABmC4y5NDyGk="; - } + # uBlock Origin + { id = "cjpalhdlnbpafiamejdnhcphjbkeiagm"; } ]; # Added to the wrapper @@ -118,7 +115,8 @@ The following options are available under `programs.helium`: | :--------------- | :----------------- | :--------------------- | :------------------------------------------------- | | `enable` | boolean | `false` | Whether to enable the Helium browser module. | | `package` | package | `self.packages.helium` | The helium package to use. | -| `extensions` | list of submodules | `[]` | List of extensions to install `{ id, hash }`. | +| `extensions` | list of submodules | `[]` | Extensions to install: `{ id, hash }` (`hash` only needed in `unpacked` mode). | +| `extensionInstallMode` | `"policy"` or `"unpacked"` | `"policy"` | How extensions are installed — see below. | | `extraFlags` | list of strings | `[]` | Command line arguments passed to the wrapper. | | `extraPolicies` | attribute set | `{}` | Raw Chromium policies to apply. | | `preferences` | attribute set | `{}` | Json that will be merged into XDG Config. | @@ -157,8 +155,33 @@ find all the json keys and values inside the helium browser by typing } ``` +## Extension install modes + +`extensionInstallMode = "policy"` (the default) force-installs every entry in +`extensions` through the `ExtensionInstallForcelist` Chromium policy. The +browser downloads and installs each extension itself, so: + +- the real Web Store ID and signature are kept — native messaging hosts such as + KeePassXC-Browser match on that ID and only work this way, +- the install is recorded in the profile, so each extension's `onInstalled` + handler runs once instead of on every launch, +- extensions keep updating themselves. + +This mode needs the NixOS module, which is what writes the policy file into +`/etc/chromium/policies/managed/`. No `hash` is required. + +`extensionInstallMode = "unpacked"` pins the CRXs in the Nix store and hands +them to the browser with `--load-extension`. It is fully reproducible and needs +no NixOS module, but Chromium treats command-line extensions as freshly +installed on every start — every extension re-runs its `onInstalled` handler, +which for many extensions means a welcome tab on each launch — and it derives +the extension ID from the load path unless the manifest carries a `key`. Each +entry needs a `hash` in this mode; see below. + ## Obtaining extensions +Only needed for `extensionInstallMode = "unpacked"`. + > [!WARNING] > Be wary that if you are rate-limited that the file will be empty and the build will fail diff --git a/modules/home-manager.nix b/modules/home-manager.nix index 92e94cb..ff82c79 100644 --- a/modules/home-manager.nix +++ b/modules/home-manager.nix @@ -48,14 +48,23 @@ let rm -rf $out/_metadata ''; - resolvedExtensions = map (spec: { - inherit (spec) id; - unpacked = unpackExtension { inherit (spec) id hash; }; - }) cfg.extensions; + unpackedMode = cfg.extensionInstallMode == "unpacked"; + + resolvedExtensions = lib.optionals unpackedMode ( + map (spec: { + inherit (spec) id; + unpacked = unpackExtension { inherit (spec) id hash; }; + }) cfg.extensions + ); + + webStoreUpdateUrl = "https://clients2.google.com/service/update2/crx"; policyAttrs = { ExtensionInstallAllowlist = map (ext: ext.id) cfg.extensions; } + // lib.optionalAttrs (!unpackedMode && cfg.extensions != [ ]) { + ExtensionInstallForcelist = map (ext: "${ext.id};${webStoreUpdateUrl}") cfg.extensions; + } // cfg.extraPolicies; extensionRoot = "${config.xdg.dataHome}/helium/extensions"; @@ -103,12 +112,42 @@ in lib.types.submodule { options = { id = lib.mkOption { type = lib.types.str; }; - hash = lib.mkOption { type = lib.types.str; }; + hash = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + CRX hash, only needed when extensionInstallMode is "unpacked". + ''; + }; }; } ); default = [ ]; }; + extensionInstallMode = lib.mkOption { + type = lib.types.enum [ + "policy" + "unpacked" + ]; + default = "policy"; + description = '' + How `extensions` reach the browser. + + "policy" force-installs them through the ExtensionInstallForcelist + Chromium policy. The browser downloads each extension itself, so the + Web Store IDs and signatures are preserved (native messaging hosts + such as KeePassXC depend on the real ID), the install is recorded in + the profile, and extensions keep updating themselves. Requires the + NixOS module so the policy file lands in /etc/chromium/policies. + + "unpacked" pins the CRXs in the Nix store and passes them via + --load-extension. It is reproducible and works without the NixOS + module, but Chromium treats command-line extensions as freshly + installed on every start, which re-fires each extension's onInstalled + handler (welcome tabs on every launch), and derives IDs from the load + path unless the manifest carries a "key". + ''; + }; prodversion = lib.mkOption { type = lib.types.str; default = "151.0.0.0"; @@ -157,6 +196,14 @@ in }; config = lib.mkIf cfg.enable { + assertions = map (ext: { + assertion = ext.hash != null; + message = '' + programs.helium.extensions."${ext.id}".hash is required when + programs.helium.extensionInstallMode = "unpacked". + ''; + }) (lib.optionals unpackedMode cfg.extensions); + home = { packages = [ heliumWithFlags @@ -165,18 +212,20 @@ in ]; activation = { - # Refresh the writable copies the --load-extension flag points at. The - # tree is rebuilt from scratch so extensions that were removed from the - # config, or whose store path changed, do not linger. - heliumExtensions = lib.mkIf (resolvedExtensions != [ ]) ( - lib.hm.dag.entryAfter [ "writeBoundary" ] '' - run rm -rf ${lib.escapeShellArg extensionRoot} - run mkdir -p ${lib.escapeShellArg extensionRoot} - ${lib.concatMapStringsSep "\n" (ext: '' - run cp -r ${ext.unpacked} ${lib.escapeShellArg "${extensionRoot}/${ext.id}"} - '') resolvedExtensions} - run chmod -R u+w ${lib.escapeShellArg extensionRoot} - '' + heliumExtensions = lib.hm.dag.entryAfter [ "writeBoundary" ] ( + if resolvedExtensions != [ ] then + '' + run rm -rf ${lib.escapeShellArg extensionRoot} + run mkdir -p ${lib.escapeShellArg extensionRoot} + ${lib.concatMapStringsSep "\n" (ext: '' + run cp -r ${ext.unpacked} ${lib.escapeShellArg "${extensionRoot}/${ext.id}"} + '') resolvedExtensions} + run chmod -R u+w ${lib.escapeShellArg extensionRoot} + '' + else + '' + run rm -rf ${lib.escapeShellArg extensionRoot} + '' ); heliumPreferences = lib.mkIf (cfg.preferences != { }) (