From 894d806af0ccfd5376d220f8b57a57584fa66239 Mon Sep 17 00:00:00 2001 From: coreybutler <770982+coreybutler@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:04:23 -0500 Subject: [PATCH 1/4] feat: register sparse Appx identity for toast collections Per-user Add-AppxPackage, no extra UAC. Unregister before community payload delete. --- src/bootstrap/identity.go | 108 +++++++++++++++++++++++++++++++ src/bootstrap/identity_other.go | 14 ++++ src/bootstrap/identity_test.go | 42 ++++++++++++ src/bootstrap/init.go | 1 + src/bootstrap/migration.go | 2 + src/cmd/main.go | 24 +++++++ src/commands/alias/add.go | 1 + src/commands/alias/remove.go | 1 + src/commands/cfg/set.go | 1 + src/commands/firewall/helpers.go | 3 + src/commands/malware/malware.go | 1 + src/commands/pin.go | 2 + src/installer/activation.go | 1 + src/installer/installer.go | 1 + src/installer/uninstaller.go | 1 + src/link/link.go | 1 + 16 files changed, 204 insertions(+) create mode 100644 src/bootstrap/identity.go create mode 100644 src/bootstrap/identity_other.go create mode 100644 src/bootstrap/identity_test.go diff --git a/src/bootstrap/identity.go b/src/bootstrap/identity.go new file mode 100644 index 0000000..d16f26f --- /dev/null +++ b/src/bootstrap/identity.go @@ -0,0 +1,108 @@ +//go:build windows + +package bootstrap + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + + prefs "common/preferences" + "common/registry" +) + +const ( + CommunityAppxName = "AuthorSoftware.NVMWindows" + CertifiedAppxName = "AuthorSoftware.NVMWindows.Certified" + identityMsixName = "NVMWindows.msix" + retiredValueName = "CommunityIdentityRetired" +) + +var runAppxCommand = func(command string) error { + ps := filepath.Join(os.Getenv("SystemRoot"), "System32", "WindowsPowerShell", "v1.0", "powershell.exe") + cmd := exec.Command(ps, "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command) + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: 0x08000000} + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%w: %s", err, strings.TrimSpace(string(out))) + } + return nil +} + +// RegisterSparseIdentity registers the per-user identity .msix against programRoot. +// Failure is non-fatal for callers; toasts fall back to AUMID-only. +func RegisterSparseIdentity(programRoot string) error { + programRoot = filepath.Clean(strings.TrimSpace(programRoot)) + if programRoot == "" { + return fmt.Errorf("program root is empty") + } + msix := filepath.Join(programRoot, "identity", identityMsixName) + if _, err := os.Stat(msix); err != nil { + return err + } + cmd := fmt.Sprintf( + "Add-AppxPackage -Path %s -ExternalLocation %s -ErrorAction Stop", + psSingleQuote(msix), + psSingleQuote(programRoot), + ) + return runAppxCommand(cmd) +} + +// UnregisterSparseIdentity removes a sparse identity package. Missing package is success. +func UnregisterSparseIdentity(name string) error { + name = strings.TrimSpace(name) + if name == "" { + name = CommunityAppxName + } + cmd := fmt.Sprintf( + "Get-AppxPackage -Name %s | Remove-AppxPackage -ErrorAction SilentlyContinue", + psSingleQuote(name), + ) + return runAppxCommand(cmd) +} + +// MaybeRemoveStaleCommunityIdentity drops a leftover community Appx after +// community→certified when the MSI impersonated CA ran as SYSTEM. +func MaybeRemoveStaleCommunityIdentity() { + root, err := ProgramRoot() + if err != nil || !isMachineProgramRoot(root) { + return + } + if communityIdentityRetired() { + return + } + _ = UnregisterSparseIdentity(CommunityAppxName) + _ = markCommunityIdentityRetired() +} + +func isMachineProgramRoot(root string) bool { + low := strings.ToLower(filepath.Clean(root)) + return strings.Contains(low, `\program files\`) || strings.Contains(low, `\program files (x86)\`) +} + +func communityIdentityRetired() bool { + root := strings.TrimSpace(prefs.ROOT) + if root == "" { + return false + } + v, exists, err := registry.GetBool(root + "/" + retiredValueName) + if err != nil || !exists { + return false + } + return v +} + +func markCommunityIdentityRetired() error { + root := strings.TrimSpace(prefs.ROOT) + if root == "" { + return nil + } + return registry.PutBool(true, root+"/"+retiredValueName) +} + +func psSingleQuote(path string) string { + return "'" + strings.ReplaceAll(path, "'", "''") + "'" +} diff --git a/src/bootstrap/identity_other.go b/src/bootstrap/identity_other.go new file mode 100644 index 0000000..37e1962 --- /dev/null +++ b/src/bootstrap/identity_other.go @@ -0,0 +1,14 @@ +//go:build !windows + +package bootstrap + +const ( + CommunityAppxName = "AuthorSoftware.NVMWindows" + CertifiedAppxName = "AuthorSoftware.NVMWindows.Certified" +) + +func RegisterSparseIdentity(string) error { return nil } + +func UnregisterSparseIdentity(string) error { return nil } + +func MaybeRemoveStaleCommunityIdentity() {} diff --git a/src/bootstrap/identity_test.go b/src/bootstrap/identity_test.go new file mode 100644 index 0000000..586d03e --- /dev/null +++ b/src/bootstrap/identity_test.go @@ -0,0 +1,42 @@ +//go:build windows + +package bootstrap + +import ( + "strings" + "testing" +) + +func TestPsSingleQuote(t *testing.T) { + if got := psSingleQuote(`C:\a'b`); got != `'C:\a''b'` { + t.Fatalf("got %q", got) + } +} + +func TestIsMachineProgramRoot(t *testing.T) { + if !isMachineProgramRoot(`C:\Program Files\Author Software\nvm`) { + t.Fatal("Program Files should match") + } + if !isMachineProgramRoot(`C:\Program Files (x86)\Author Software\nvm`) { + t.Fatal("Program Files (x86) should match") + } + if isMachineProgramRoot(`C:\Users\a\AppData\Local\Author Software\nvm`) { + t.Fatal("AppData must not match") + } +} + +func TestUnregisterSparseIdentityCommand(t *testing.T) { + prev := runAppxCommand + t.Cleanup(func() { runAppxCommand = prev }) + var got string + runAppxCommand = func(command string) error { + got = command + return nil + } + if err := UnregisterSparseIdentity(CommunityAppxName); err != nil { + t.Fatal(err) + } + if !strings.Contains(got, CommunityAppxName) || !strings.Contains(got, "Remove-AppxPackage") { + t.Fatalf("command = %q", got) + } +} diff --git a/src/bootstrap/init.go b/src/bootstrap/init.go index 1fb1421..11b4289 100644 --- a/src/bootstrap/init.go +++ b/src/bootstrap/init.go @@ -91,6 +91,7 @@ func EnsureUserProfileInitialized() error { if err := MaintainShimDirectory(); err != nil { fmt.Fprintf(os.Stderr, "nvm: warning: shim maintenance skipped: %v\n", err) } + MaybeRemoveStaleCommunityIdentity() return nil } diff --git a/src/bootstrap/migration.go b/src/bootstrap/migration.go index 8a17351..6d2212e 100644 --- a/src/bootstrap/migration.go +++ b/src/bootstrap/migration.go @@ -74,6 +74,8 @@ func cleanupLegacyUserPayload(dataRoot string) error { return err } + _ = UnregisterSparseIdentity(CommunityAppxName) + for _, path := range []string{ legacyNvmExe, filepath.Join(dataRoot, "utils"), diff --git a/src/cmd/main.go b/src/cmd/main.go index 609d113..96434c9 100644 --- a/src/cmd/main.go +++ b/src/cmd/main.go @@ -107,6 +107,28 @@ func main() { os.Exit(1) } return + case "--register-sparse-identity": + // Inno/MSI: per-user Add-AppxPackage of the identity .msix. Fail-open. + settings.Load() + root, err := bootstrap.ProgramRoot() + if err != nil { + fmt.Fprint(os.Stderr, err.Error()) + os.Exit(0) + } + if err := bootstrap.RegisterSparseIdentity(root); err != nil { + fmt.Fprintf(os.Stderr, "sparse identity register skipped: %v\n", err) + } + return + case "--unregister-sparse-identity": + settings.Load() + name := bootstrap.CommunityAppxName + if len(os.Args) >= 3 && strings.TrimSpace(os.Args[2]) != "" { + name = strings.TrimSpace(os.Args[2]) + } + if err := bootstrap.UnregisterSparseIdentity(name); err != nil { + fmt.Fprintf(os.Stderr, "sparse identity unregister skipped: %v\n", err) + } + return case "--retire-community-payload": // Impersonated MSI CA: drop community nvm.exe/utils/.icons and the HKCU // uninstall entry during install, not on first nvm launch. @@ -288,6 +310,7 @@ func main() { newRoot, oldRoot, ) + // Critical setup toast: no collection (reserved nvm.setup). if err := notify.Send(settings.AppId, "NVM storage location updated", msg); err != nil { fmt.Fprint(os.Stderr, err.Error()) os.Exit(1) @@ -305,6 +328,7 @@ func main() { "Uninstall the old Node.js at %s. NVM for Windows left it in place during this upgrade. Leaving it installed causes conflicts.", nodePath, ) + // Critical setup toast: no collection (reserved nvm.setup). if err := notify.Send(settings.AppId, "Old Node.js still installed", officialMsg); err != nil { fmt.Fprint(os.Stderr, err.Error()) os.Exit(1) diff --git a/src/commands/alias/add.go b/src/commands/alias/add.go index 4656f59..718a7a8 100644 --- a/src/commands/alias/add.go +++ b/src/commands/alias/add.go @@ -100,6 +100,7 @@ func (c *Add) Run() error { msg := fmt.Sprintf("%q now refers to v%s", c.Name, version) fmt.Println(msg) if !system.IsAppInForeground() { + // Critical activity toast: no collection (reserved nvm.activity). go notify.Send(settings.AppId, "", msg) } diff --git a/src/commands/alias/remove.go b/src/commands/alias/remove.go index 111446f..b4a1a3e 100644 --- a/src/commands/alias/remove.go +++ b/src/commands/alias/remove.go @@ -59,6 +59,7 @@ func (c *Remove) Run() error { msg := fmt.Sprintf("%d alias%s removed successfully.", count, suffix) fmt.Println(msg) if !system.IsAppInForeground() { + // Critical activity toast: no collection (reserved nvm.activity). go notify.Send(settings.AppId, "", msg) } diff --git a/src/commands/cfg/set.go b/src/commands/cfg/set.go index d4ebd4a..1dec170 100644 --- a/src/commands/cfg/set.go +++ b/src/commands/cfg/set.go @@ -123,6 +123,7 @@ func (s *Set) Run() error { notifyLines = append(notifyLines, fmt.Sprintf("%s set to %s", key, displayValue(key, value))) } if len(notifyLines) > 0 && !system.IsAppInForeground() { + // Critical activity toast: no collection (reserved nvm.activity). go notify.Send(settings.AppId, "", strings.Join(notifyLines, ", ")) } diff --git a/src/commands/firewall/helpers.go b/src/commands/firewall/helpers.go index d7e0bde..4789a26 100644 --- a/src/commands/firewall/helpers.go +++ b/src/commands/firewall/helpers.go @@ -389,6 +389,7 @@ func trustNoAnswerMessage(name, why string) string { func notifyTrustNoAnswer(name, why string) { msg := trustNoAnswerMessage(name, why) fmt.Fprintln(os.Stderr, msg) + // Critical firewall toast: no collection (reserved nvm.firewall). _ = notify.Send(settings.AppId, "NVM Firewall", msg) } @@ -423,6 +424,7 @@ func (n *NotifyChanged) Run() error { return fmt.Errorf("module name required") } msg := fmt.Sprintf("A change to module '%s' was automatically trusted.", name) + // Critical firewall toast: no collection (reserved nvm.firewall). _ = notify.Send(settings.AppId, "NVM Firewall", msg) // NVM4406 audit emitted by proxy at decision site. return nil @@ -480,6 +482,7 @@ func promptTrustToast(name string) error { defer os.Remove(rspPath) msg := fmt.Sprintf("Untrusted module '%s' changed. Do you trust this module?", name) + // Critical firewall toast: no collection (reserved nvm.firewall). _ = notify.Send( settings.AppId, "NVM Firewall", diff --git a/src/commands/malware/malware.go b/src/commands/malware/malware.go index d1eed29..4fbf78d 100644 --- a/src/commands/malware/malware.go +++ b/src/commands/malware/malware.go @@ -91,6 +91,7 @@ func (c *NotifyBlock) Run() error { if license.IsCommunityBuild() && corporatePC() { body += " Certified keeps a record of blocks like this for your security tools." } + // Critical security toast: no collection (reserved nvm.security). return notify.SendWithAudio(settings.AppId, "Malicious package blocked", body, "ms-winsoundevent:Notification.Reminder") } diff --git a/src/commands/pin.go b/src/commands/pin.go index 55d3e87..c395070 100644 --- a/src/commands/pin.go +++ b/src/commands/pin.go @@ -172,6 +172,7 @@ func (c *Pin) Run() error { msg := fmt.Sprintf("Successfully pinned %s Node.js engine to Node.js v%s (with npm v%s)", c.File, node_version, npm_version) fmt.Println(msg) if !system.IsAppInForeground() { + // Critical activity toast: no collection (reserved nvm.activity). go notify.Send(settings.AppId, "", msg) } @@ -183,6 +184,7 @@ func (c *Pin) Run() error { msg := fmt.Sprintf("Successfully pinned %s Node.js version to v%s", c.File, node_version) fmt.Println(msg) if !system.IsAppInForeground() { + // Critical activity toast: no collection (reserved nvm.activity). go notify.Send(settings.AppId, "", msg) } } diff --git a/src/installer/activation.go b/src/installer/activation.go index 7fe1848..2952db7 100644 --- a/src/installer/activation.go +++ b/src/installer/activation.go @@ -114,6 +114,7 @@ func ActivateVersion(version string) error { msg := fmt.Sprintf("Now using Node.js v%s by default.", version) fmt.Fprintln(os.Stdout, msg) if !system.IsAppInForeground() { + // Critical activity toast: no collection (reserved nvm.activity). go notify.Send(settings.AppId, "", msg) } diff --git a/src/installer/installer.go b/src/installer/installer.go index bb27423..353854a 100644 --- a/src/installer/installer.go +++ b/src/installer/installer.go @@ -280,6 +280,7 @@ func install( title = fmt.Sprintf("Node.js v%s Installation Failed", nodeVersion) message = processErr.Error() } + // Critical activity toast: no collection (reserved nvm.activity). go func() { _ = notify.Send(settings.AppId, title, message) }() }() diff --git a/src/installer/uninstaller.go b/src/installer/uninstaller.go index c0dabbe..68338d0 100644 --- a/src/installer/uninstaller.go +++ b/src/installer/uninstaller.go @@ -109,6 +109,7 @@ func Uninstall(cfg UninstallConfig) error { // Apps launches with no console — always toast so the user gets feedback. if len(notifyMsgs) > 0 && (cfg.FromApps || !system.IsAppInForeground()) { + // Critical activity toast: no collection (reserved nvm.activity). go notify.Send(settings.AppId, "", strings.Join(notifyMsgs, "; ")) } diff --git a/src/link/link.go b/src/link/link.go index c339cf2..17c4580 100644 --- a/src/link/link.go +++ b/src/link/link.go @@ -80,6 +80,7 @@ func Link(source, symlink string) error { if no_devmode { // Trigger a notification to open settings to enable developer mode + // Critical setup toast: no collection (reserved nvm.setup). notify.Send(settings.AppId, "Help Enabling Symlinks", "Developer Mode is required to use symlinks.", notify.Action{Label: "Open Developer Settings", URL: "ms-settings:developers"}, ) From 449bca48970358c5e9b7a0aa6f157ccb5aa220de Mon Sep 17 00:00:00 2001 From: coreybutler <770982+coreybutler@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:58:50 -0500 Subject: [PATCH 2/4] feat: provision certified sparse identity for all users Elevated MSI registers AllUsers and provisioned packages. First Program Files launch retries per-user register. --- src/bootstrap/identity.go | 76 +++++++++++++++++++++++++++++++-- src/bootstrap/identity_other.go | 6 +++ src/bootstrap/identity_test.go | 42 ++++++++++++++++++ src/bootstrap/init.go | 3 ++ src/cmd/main.go | 22 ++++++++++ 5 files changed, 145 insertions(+), 4 deletions(-) diff --git a/src/bootstrap/identity.go b/src/bootstrap/identity.go index d16f26f..a75c530 100644 --- a/src/bootstrap/identity.go +++ b/src/bootstrap/identity.go @@ -15,10 +15,11 @@ import ( ) const ( - CommunityAppxName = "AuthorSoftware.NVMWindows" - CertifiedAppxName = "AuthorSoftware.NVMWindows.Certified" - identityMsixName = "NVMWindows.msix" - retiredValueName = "CommunityIdentityRetired" + CommunityAppxName = "AuthorSoftware.NVMWindows" + CertifiedAppxName = "AuthorSoftware.NVMWindows.Certified" + identityMsixName = "NVMWindows.msix" + retiredValueName = "CommunityIdentityRetired" + registeredValueName = "CertifiedIdentityRegistered" ) var runAppxCommand = func(command string) error { @@ -51,6 +52,37 @@ func RegisterSparseIdentity(programRoot string) error { return runAppxCommand(cmd) } +// ProvisionSparseIdentity registers the identity for all users (elevated MSI). +func ProvisionSparseIdentity(programRoot string) error { + programRoot = filepath.Clean(strings.TrimSpace(programRoot)) + if programRoot == "" { + return fmt.Errorf("program root is empty") + } + msix := filepath.Join(programRoot, "identity", identityMsixName) + if _, err := os.Stat(msix); err != nil { + return err + } + cmd := fmt.Sprintf( + "$msix = %s; $root = %s; Add-AppxPackage -Path $msix -ExternalLocation $root -AllUsers -ErrorAction SilentlyContinue; if (-not $?) { Add-AppxPackage -Path $msix -ExternalLocation $root -ErrorAction SilentlyContinue }; try { Add-AppxProvisionedPackage -Online -PackagePath $msix -SkipLicense -ErrorAction SilentlyContinue | Out-Null } catch {}", + psSingleQuote(msix), + psSingleQuote(programRoot), + ) + return runAppxCommand(cmd) +} + +// DeprovisionSparseIdentity drops per-user, all-user, and provisioned packages. +func DeprovisionSparseIdentity(name string) error { + name = strings.TrimSpace(name) + if name == "" { + name = CertifiedAppxName + } + cmd := fmt.Sprintf( + "$n = %s; Get-AppxPackage -AllUsers -Name $n -ErrorAction SilentlyContinue | Remove-AppxPackage -AllUsers -ErrorAction SilentlyContinue; Get-AppxPackage -Name $n -ErrorAction SilentlyContinue | Remove-AppxPackage -ErrorAction SilentlyContinue; Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -eq $n } | ForEach-Object { Remove-AppxProvisionedPackage -Online -PackageName $_.PackageName -ErrorAction SilentlyContinue }", + psSingleQuote(name), + ) + return runAppxCommand(cmd) +} + // UnregisterSparseIdentity removes a sparse identity package. Missing package is success. func UnregisterSparseIdentity(name string) error { name = strings.TrimSpace(name) @@ -78,6 +110,22 @@ func MaybeRemoveStaleCommunityIdentity() { _ = markCommunityIdentityRetired() } +// MaybeRegisterCertifiedIdentity registers the Program Files identity .msix once +// per user when the MSI impersonated CA ran as SYSTEM. +func MaybeRegisterCertifiedIdentity() { + root, err := ProgramRoot() + if err != nil || !isMachineProgramRoot(root) { + return + } + if certifiedIdentityRegistered() { + return + } + if err := RegisterSparseIdentity(root); err != nil { + return + } + _ = markCertifiedIdentityRegistered() +} + func isMachineProgramRoot(root string) bool { low := strings.ToLower(filepath.Clean(root)) return strings.Contains(low, `\program files\`) || strings.Contains(low, `\program files (x86)\`) @@ -103,6 +151,26 @@ func markCommunityIdentityRetired() error { return registry.PutBool(true, root+"/"+retiredValueName) } +func certifiedIdentityRegistered() bool { + root := strings.TrimSpace(prefs.ROOT) + if root == "" { + return false + } + v, exists, err := registry.GetBool(root + "/" + registeredValueName) + if err != nil || !exists { + return false + } + return v +} + +func markCertifiedIdentityRegistered() error { + root := strings.TrimSpace(prefs.ROOT) + if root == "" { + return nil + } + return registry.PutBool(true, root+"/"+registeredValueName) +} + func psSingleQuote(path string) string { return "'" + strings.ReplaceAll(path, "'", "''") + "'" } diff --git a/src/bootstrap/identity_other.go b/src/bootstrap/identity_other.go index 37e1962..875bbd1 100644 --- a/src/bootstrap/identity_other.go +++ b/src/bootstrap/identity_other.go @@ -9,6 +9,12 @@ const ( func RegisterSparseIdentity(string) error { return nil } +func ProvisionSparseIdentity(string) error { return nil } + +func DeprovisionSparseIdentity(string) error { return nil } + func UnregisterSparseIdentity(string) error { return nil } func MaybeRemoveStaleCommunityIdentity() {} + +func MaybeRegisterCertifiedIdentity() {} diff --git a/src/bootstrap/identity_test.go b/src/bootstrap/identity_test.go index 586d03e..61a09c9 100644 --- a/src/bootstrap/identity_test.go +++ b/src/bootstrap/identity_test.go @@ -3,6 +3,8 @@ package bootstrap import ( + "os" + "path/filepath" "strings" "testing" ) @@ -40,3 +42,43 @@ func TestUnregisterSparseIdentityCommand(t *testing.T) { t.Fatalf("command = %q", got) } } + +func TestProvisionSparseIdentityCommand(t *testing.T) { + prev := runAppxCommand + t.Cleanup(func() { runAppxCommand = prev }) + root := t.TempDir() + if err := os.MkdirAll(filepath.Join(root, "identity"), 0o755); err != nil { + t.Fatal(err) + } + msix := filepath.Join(root, "identity", identityMsixName) + if err := os.WriteFile(msix, []byte("msix"), 0o644); err != nil { + t.Fatal(err) + } + var got string + runAppxCommand = func(command string) error { + got = command + return nil + } + if err := ProvisionSparseIdentity(root); err != nil { + t.Fatal(err) + } + if !strings.Contains(got, "-AllUsers") || !strings.Contains(got, "Add-AppxProvisionedPackage") { + t.Fatalf("command = %q", got) + } +} + +func TestDeprovisionSparseIdentityCommand(t *testing.T) { + prev := runAppxCommand + t.Cleanup(func() { runAppxCommand = prev }) + var got string + runAppxCommand = func(command string) error { + got = command + return nil + } + if err := DeprovisionSparseIdentity(CertifiedAppxName); err != nil { + t.Fatal(err) + } + if !strings.Contains(got, CertifiedAppxName) || !strings.Contains(got, "Remove-AppxProvisionedPackage") { + t.Fatalf("command = %q", got) + } +} diff --git a/src/bootstrap/init.go b/src/bootstrap/init.go index 11b4289..d53e4ae 100644 --- a/src/bootstrap/init.go +++ b/src/bootstrap/init.go @@ -92,6 +92,7 @@ func EnsureUserProfileInitialized() error { fmt.Fprintf(os.Stderr, "nvm: warning: shim maintenance skipped: %v\n", err) } MaybeRemoveStaleCommunityIdentity() + MaybeRegisterCertifiedIdentity() return nil } @@ -155,6 +156,8 @@ func EnsureUserProfileInitialized() error { _ = settings.SeedAnnouncementWatermarksIfEmpty(settings.Put) ensureSyncScheduledTask() + MaybeRemoveStaleCommunityIdentity() + MaybeRegisterCertifiedIdentity() return nil } diff --git a/src/cmd/main.go b/src/cmd/main.go index 96434c9..7fc6671 100644 --- a/src/cmd/main.go +++ b/src/cmd/main.go @@ -129,6 +129,28 @@ func main() { fmt.Fprintf(os.Stderr, "sparse identity unregister skipped: %v\n", err) } return + case "--provision-sparse-identity": + // Elevated MSI: AllUsers + provisioned. Fail-open. + settings.Load() + root, err := bootstrap.ProgramRoot() + if err != nil { + fmt.Fprint(os.Stderr, err.Error()) + os.Exit(0) + } + if err := bootstrap.ProvisionSparseIdentity(root); err != nil { + fmt.Fprintf(os.Stderr, "sparse identity provision skipped: %v\n", err) + } + return + case "--deprovision-sparse-identity": + settings.Load() + name := bootstrap.CertifiedAppxName + if len(os.Args) >= 3 && strings.TrimSpace(os.Args[2]) != "" { + name = strings.TrimSpace(os.Args[2]) + } + if err := bootstrap.DeprovisionSparseIdentity(name); err != nil { + fmt.Fprintf(os.Stderr, "sparse identity deprovision skipped: %v\n", err) + } + return case "--retire-community-payload": // Impersonated MSI CA: drop community nvm.exe/utils/.icons and the HKCU // uninstall entry during install, not on first nvm launch. From cbd4e9d0db73a042650eff9fdc97cba51cd0f79f Mon Sep 17 00:00:00 2001 From: coreybutler <770982+coreybutler@users.noreply.github.com> Date: Fri, 23 Oct 2026 21:20:58 -0500 Subject: [PATCH 3/4] feat: drop leftover Certified identity on community downgrade If Program Files nvm.exe is gone, unregister AuthorSoftware.NVMWindows.Certified. Leave it if Certified is still installed. --- src/bootstrap/identity.go | 102 +++++++++++++++++++++++++++++--- src/bootstrap/identity_other.go | 2 + src/bootstrap/identity_test.go | 12 ++++ src/bootstrap/init.go | 2 + 4 files changed, 111 insertions(+), 7 deletions(-) diff --git a/src/bootstrap/identity.go b/src/bootstrap/identity.go index a75c530..c0672c9 100644 --- a/src/bootstrap/identity.go +++ b/src/bootstrap/identity.go @@ -15,11 +15,12 @@ import ( ) const ( - CommunityAppxName = "AuthorSoftware.NVMWindows" - CertifiedAppxName = "AuthorSoftware.NVMWindows.Certified" - identityMsixName = "NVMWindows.msix" - retiredValueName = "CommunityIdentityRetired" - registeredValueName = "CertifiedIdentityRegistered" + CommunityAppxName = "AuthorSoftware.NVMWindows" + CertifiedAppxName = "AuthorSoftware.NVMWindows.Certified" + identityMsixName = "NVMWindows.msix" + retiredValueName = "CommunityIdentityRetired" + registeredValueName = "CertifiedIdentityRegistered" + certifiedRetiredValueName = "CertifiedIdentityRetired" ) var runAppxCommand = func(command string) error { @@ -80,7 +81,11 @@ func DeprovisionSparseIdentity(name string) error { "$n = %s; Get-AppxPackage -AllUsers -Name $n -ErrorAction SilentlyContinue | Remove-AppxPackage -AllUsers -ErrorAction SilentlyContinue; Get-AppxPackage -Name $n -ErrorAction SilentlyContinue | Remove-AppxPackage -ErrorAction SilentlyContinue; Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -eq $n } | ForEach-Object { Remove-AppxProvisionedPackage -Online -PackageName $_.PackageName -ErrorAction SilentlyContinue }", psSingleQuote(name), ) - return runAppxCommand(cmd) + err := runAppxCommand(cmd) + if strings.EqualFold(name, CertifiedAppxName) { + _ = clearCertifiedIdentityRegistered() + } + return err } // UnregisterSparseIdentity removes a sparse identity package. Missing package is success. @@ -93,7 +98,11 @@ func UnregisterSparseIdentity(name string) error { "Get-AppxPackage -Name %s | Remove-AppxPackage -ErrorAction SilentlyContinue", psSingleQuote(name), ) - return runAppxCommand(cmd) + err := runAppxCommand(cmd) + if strings.EqualFold(name, CertifiedAppxName) { + _ = clearCertifiedIdentityRegistered() + } + return err } // MaybeRemoveStaleCommunityIdentity drops a leftover community Appx after @@ -124,6 +133,49 @@ func MaybeRegisterCertifiedIdentity() { return } _ = markCertifiedIdentityRegistered() + _ = clearCertifiedIdentityRetired() +} + +// MaybeRemoveStaleCertifiedIdentity drops leftover Certified Appx after +// certified→community when MSI deprovision ran as SYSTEM. Leaves Certified +// identity alone if Program Files nvm.exe is still present. +func MaybeRemoveStaleCertifiedIdentity() { + root, err := ProgramRoot() + if err != nil || !shouldRemoveStaleCertifiedIdentity(root, certifiedProgramInstalled()) { + return + } + if certifiedIdentityRetired() { + return + } + _ = UnregisterSparseIdentity(CertifiedAppxName) + _ = markCertifiedIdentityRetired() +} + +func shouldRemoveStaleCertifiedIdentity(programRoot string, certifiedStillInstalled bool) bool { + if isMachineProgramRoot(programRoot) { + return false + } + return !certifiedStillInstalled +} + +func certifiedProgramInstalled() bool { + for _, p := range certifiedNvmExePaths() { + if _, err := os.Stat(p); err == nil { + return true + } + } + return false +} + +func certifiedNvmExePaths() []string { + var out []string + if pf := strings.TrimSpace(os.Getenv("ProgramFiles")); pf != "" { + out = append(out, filepath.Join(pf, "Author Software", "nvm", "nvm.exe")) + } + if pf := strings.TrimSpace(os.Getenv("ProgramFiles(x86)")); pf != "" { + out = append(out, filepath.Join(pf, "Author Software", "nvm", "nvm.exe")) + } + return out } func isMachineProgramRoot(root string) bool { @@ -171,6 +223,42 @@ func markCertifiedIdentityRegistered() error { return registry.PutBool(true, root+"/"+registeredValueName) } +func clearCertifiedIdentityRegistered() error { + root := strings.TrimSpace(prefs.ROOT) + if root == "" { + return nil + } + return registry.Del(root + "/" + registeredValueName) +} + +func certifiedIdentityRetired() bool { + root := strings.TrimSpace(prefs.ROOT) + if root == "" { + return false + } + v, exists, err := registry.GetBool(root + "/" + certifiedRetiredValueName) + if err != nil || !exists { + return false + } + return v +} + +func markCertifiedIdentityRetired() error { + root := strings.TrimSpace(prefs.ROOT) + if root == "" { + return nil + } + return registry.PutBool(true, root+"/"+certifiedRetiredValueName) +} + +func clearCertifiedIdentityRetired() error { + root := strings.TrimSpace(prefs.ROOT) + if root == "" { + return nil + } + return registry.Del(root + "/" + certifiedRetiredValueName) +} + func psSingleQuote(path string) string { return "'" + strings.ReplaceAll(path, "'", "''") + "'" } diff --git a/src/bootstrap/identity_other.go b/src/bootstrap/identity_other.go index 875bbd1..f30cad5 100644 --- a/src/bootstrap/identity_other.go +++ b/src/bootstrap/identity_other.go @@ -18,3 +18,5 @@ func UnregisterSparseIdentity(string) error { return nil } func MaybeRemoveStaleCommunityIdentity() {} func MaybeRegisterCertifiedIdentity() {} + +func MaybeRemoveStaleCertifiedIdentity() {} diff --git a/src/bootstrap/identity_test.go b/src/bootstrap/identity_test.go index 61a09c9..dff1ce2 100644 --- a/src/bootstrap/identity_test.go +++ b/src/bootstrap/identity_test.go @@ -67,6 +67,18 @@ func TestProvisionSparseIdentityCommand(t *testing.T) { } } +func TestShouldRemoveStaleCertifiedIdentity(t *testing.T) { + if shouldRemoveStaleCertifiedIdentity(`C:\Program Files\Author Software\nvm`, false) { + t.Fatal("Program Files install must keep Certified identity") + } + if shouldRemoveStaleCertifiedIdentity(`C:\Users\a\AppData\Local\Author Software\nvm`, true) { + t.Fatal("must not strip Certified identity while Program Files nvm.exe exists") + } + if !shouldRemoveStaleCertifiedIdentity(`C:\Users\a\AppData\Local\Author Software\nvm`, false) { + t.Fatal("AppData install after MSI removal should drop leftover Certified identity") + } +} + func TestDeprovisionSparseIdentityCommand(t *testing.T) { prev := runAppxCommand t.Cleanup(func() { runAppxCommand = prev }) diff --git a/src/bootstrap/init.go b/src/bootstrap/init.go index d53e4ae..251ff3a 100644 --- a/src/bootstrap/init.go +++ b/src/bootstrap/init.go @@ -93,6 +93,7 @@ func EnsureUserProfileInitialized() error { } MaybeRemoveStaleCommunityIdentity() MaybeRegisterCertifiedIdentity() + MaybeRemoveStaleCertifiedIdentity() return nil } @@ -158,6 +159,7 @@ func EnsureUserProfileInitialized() error { ensureSyncScheduledTask() MaybeRemoveStaleCommunityIdentity() MaybeRegisterCertifiedIdentity() + MaybeRemoveStaleCertifiedIdentity() return nil } From ea40394cc7627f84a08383c9588367d0858aff31 Mon Sep 17 00:00:00 2001 From: coreybutler <770982+coreybutler@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:10:31 -0500 Subject: [PATCH 4/4] fix: clone common beside cli workspace for go.mod replace --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0baea1c..5e62180 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,6 +36,7 @@ jobs: filters: | nvm: - 'src/**' + - '.github/**' github: - '.github/**' - 'LICENSE' @@ -127,7 +128,7 @@ jobs: - name: Checkout common modules shell: pwsh run: | - $dest = Join-Path (Split-Path (Split-Path $env:GITHUB_WORKSPACE -Parent) -Parent) "common" + $dest = Join-Path (Split-Path $env:GITHUB_WORKSPACE -Parent) "common" if (-not (Test-Path (Join-Path $dest "settings\go.mod"))) { git clone --depth 1 https://github.com/nvm-windows/common.git $dest }