Files
jdescopingtool/PLANS/securestore-auto-init-plan.md
Joseph Doherty 9bf0c29add refactor(configmanager): simplify SecureStore UI with unified info view
Consolidate SecureStoreLockedFormView and SecureStoreUnlockedFormView into
a single SecureStoreInfoFormView that displays store status and metadata.
Simplifies MainWindowViewModel by removing redundant state management.
Also adds design docs for RegexTransformer feature.
2026-01-22 09:40:38 -05:00

16 KiB

Implementation Plan: SecureStore Auto-Initialization & Tree Flattening

Overview

This plan implements automatic SecureStore initialization on config load and flattens the tree hierarchy from "Secure Stores > secrets > [keys]" to "Secure Store > [keys]".

Design Decisions (from brainstorming)

  1. Auto-creation: Fully automatic on config folder load when AutoCreateStore=true
  2. Path updates: Write actual paths back to appsettings.json after creating files
  3. Tree structure: Single "Secure Store" node with secrets as direct children
  4. Authentication: Keyfile-only (remove password option entirely)
  5. Lock concept: Removed - store is always open while config folder is loaded
  6. Icon: Key icon (🔑) for "Secure Store" node
  7. Right panel: Show instructions "Select a secret to edit" when store node selected
  8. Error handling: Show error dialog if keyfile missing/corrupted

Task 1: Update TreeNodeType enum

File: NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/TreeNodeViewModel.cs

Changes:

  • Remove SecureStoresFolder enum value (no longer needed)
  • Keep SecureStore for the single store node
  • Keep Secret for individual secrets

Before (lines 5-13):

public enum TreeNodeType
{
    Folder,
    SettingsSection,
    Pipeline,
    SecureStoresFolder,  // The "Secure Stores" folder
    SecureStore,         // Individual store files
    Secret               // Individual secrets within a store
}

After:

public enum TreeNodeType
{
    Folder,
    SettingsSection,
    Pipeline,
    SecureStore,         // The single secure store node
    Secret               // Individual secrets within a store
}

File: NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/TreeNodeViewModel.cs

Remove these properties (lines 55-79):

  • IsUnlocked property and backing field
  • IsLocked computed property
  • LockIcon computed property

Keep:

  • StorePath and KeyFilePath (needed for store identification)
  • SecretKey (needed for secret nodes)

File: NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/ISecureStoreManager.cs

Remove these methods:

  • CreateStoreWithPassword(string storePath, string password) (lines 30-35)
  • OpenStoreWithPassword(string storePath, string password) (lines 44-49)

File: NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/SecureStoreManager.cs

Remove these methods:

  • CreateStoreWithPassword() (lines 75-96)
  • OpenStoreWithPassword() (lines 120-140)

Task 5: Add EnsureRequiredKeys method to ISecureStoreManager

File: NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/ISecureStoreManager.cs

Add new method:

/// <summary>
/// Ensures all required keys exist in the store, creating blank values for any missing keys.
/// </summary>
/// <param name="requiredKeys">List of keys that must exist.</param>
/// <returns>List of keys that were added.</returns>
IReadOnlyList<string> EnsureRequiredKeys(IEnumerable<string> requiredKeys);

Task 6: Implement EnsureRequiredKeys in SecureStoreManager

File: NEW/src/Utils/JdeScoping.ConfigManager/Services/SecureStore/SecureStoreManager.cs

Add implementation:

/// <inheritdoc />
public IReadOnlyList<string> EnsureRequiredKeys(IEnumerable<string> requiredKeys)
{
    ThrowIfDisposed();

    if (_secretsManager == null)
        throw new InvalidOperationException("No store is currently open.");

    var addedKeys = new List<string>();
    foreach (var key in requiredKeys)
    {
        if (!_keys.Contains(key))
        {
            _logger.LogInformation("Adding missing required key: {Key}", key);
            SetSecret(key, string.Empty);
            addedKeys.Add(key);
        }
    }

    if (addedKeys.Count > 0)
    {
        Save();
    }

    return addedKeys.AsReadOnly();
}

Task 7: Delete locked/unlocked form files

Delete these files entirely:

ViewModels:

  • NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SecureStoreLockedFormViewModel.cs
  • NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SecureStoreUnlockedFormViewModel.cs

Views:

  • NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreLockedFormView.axaml
  • NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreLockedFormView.axaml.cs
  • NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreUnlockedFormView.axaml
  • NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreUnlockedFormView.axaml.cs

Task 8: Create SecureStoreInfoFormViewModel

File: NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/Forms/SecureStoreInfoFormViewModel.cs (new file)

Content:

namespace JdeScoping.ConfigManager.ViewModels.Forms;

/// <summary>
/// View model for the SecureStore info panel shown when the store node is selected.
/// </summary>
public class SecureStoreInfoFormViewModel : ViewModelBase
{
    /// <summary>
    /// Gets the instruction text to display.
    /// </summary>
    public string InstructionText => "Select a secret from the tree to view or edit its value.";

    /// <summary>
    /// Gets the store path for display.
    /// </summary>
    public string StorePath { get; }

    /// <summary>
    /// Gets the key file path for display.
    /// </summary>
    public string KeyFilePath { get; }

    /// <summary>
    /// Gets the number of secrets in the store.
    /// </summary>
    public int SecretCount { get; }

    public SecureStoreInfoFormViewModel(string storePath, string keyFilePath, int secretCount)
    {
        StorePath = storePath;
        KeyFilePath = keyFilePath;
        SecretCount = secretCount;
    }
}

Task 9: Create SecureStoreInfoFormView

File: NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreInfoFormView.axaml (new file)

Content: Simple panel with instruction text and optional store info display.

File: NEW/src/Utils/JdeScoping.ConfigManager/Views/Forms/SecureStoreInfoFormView.axaml.cs (new file)

Content: Code-behind for the view.


Task 10: Update MainWindowViewModel - Remove lock/unlock commands and state

File: NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs

Remove:

  • _openStores dictionary (line 41)
  • _selectedStoreNode field (line 42)
  • UnlockStoreCommand and related methods
  • LockStoreCommand and related methods
  • RaiseSecureStoreCommandsCanExecuteChanged() method
  • CreateLockedStoreFormViewModel() method
  • CreateUnlockedStoreFormViewModel() method
  • FindParentStoreNode() method (if only used for lock state)

Task 11: Update MainWindowViewModel - Add auto-initialization logic

File: NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs

Add new method InitializeSecureStoreAsync():

/// <summary>
/// Initializes the SecureStore automatically on config load.
/// Creates the store if it doesn't exist and AutoCreateStore is true.
/// Opens the store and ensures all required keys exist.
/// </summary>
private async Task InitializeSecureStoreAsync()
{
    if (_appSettings?.SecureStore == null)
        return;

    if (string.IsNullOrEmpty(ConfigFolderPath) || ConfigFolderPath == "No folder selected")
        return;

    var secureStoreConfig = _appSettings.SecureStore;

    // Resolve paths relative to config folder
    var storePath = Path.IsPathRooted(secureStoreConfig.StorePath)
        ? secureStoreConfig.StorePath
        : Path.Combine(ConfigFolderPath, secureStoreConfig.StorePath);

    var keyFilePath = Path.IsPathRooted(secureStoreConfig.KeyFilePath)
        ? secureStoreConfig.KeyFilePath
        : Path.Combine(ConfigFolderPath, secureStoreConfig.KeyFilePath);

    try
    {
        if (!File.Exists(storePath))
        {
            if (!secureStoreConfig.AutoCreateStore)
            {
                _logger?.LogWarning("SecureStore not found and AutoCreateStore is false");
                return;
            }

            // Create new store with keyfile
            _logger?.LogInformation("Creating SecureStore at {StorePath}", storePath);
            _secureStoreManager.CreateStore(storePath, keyFilePath);

            // Update appsettings.json with actual paths
            secureStoreConfig.StorePath = GetRelativePath(ConfigFolderPath, storePath);
            secureStoreConfig.KeyFilePath = GetRelativePath(ConfigFolderPath, keyFilePath);
            await SaveAppSettingsAsync();
        }
        else
        {
            // Open existing store
            if (!File.Exists(keyFilePath))
            {
                await _dialogService!.ShowErrorAsync(
                    "SecureStore Error",
                    $"Key file not found: {keyFilePath}\n\nThe SecureStore cannot be opened without its key file.");
                return;
            }

            _secureStoreManager.OpenStore(storePath, keyFilePath);
        }

        // Ensure all required keys exist
        if (secureStoreConfig.RequiredKeys?.Count > 0)
        {
            var addedKeys = _secureStoreManager.EnsureRequiredKeys(secureStoreConfig.RequiredKeys);
            if (addedKeys.Count > 0)
            {
                _logger?.LogInformation("Added {Count} missing required keys", addedKeys.Count);
            }
        }
    }
    catch (Exception ex)
    {
        _logger?.LogError(ex, "Failed to initialize SecureStore");
        await _dialogService!.ShowErrorAsync(
            "SecureStore Error",
            $"Failed to initialize SecureStore:\n\n{ex.Message}");
    }
}

private static string GetRelativePath(string basePath, string fullPath)
{
    var baseUri = new Uri(basePath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar);
    var fullUri = new Uri(fullPath);
    return Uri.UnescapeDataString(baseUri.MakeRelativeUri(fullUri).ToString().Replace('/', Path.DirectorySeparatorChar));
}

Task 12: Update MainWindowViewModel - Modify BuildTreeNodes

File: NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs

Replace the Secure Stores section in BuildTreeNodes() (around lines 464-467):

Before:

// Secure Stores folder
var secureStoresFolder = new TreeNodeViewModel("Secure Stores", "🔑", TreeNodeType.SecureStoresFolder) { IsExpanded = true };
LoadConfiguredSecureStore(secureStoresFolder);
TreeNodes.Add(secureStoresFolder);

After:

// Secure Store node (single store, secrets as direct children)
var secureStoreNode = CreateSecureStoreNode();
if (secureStoreNode != null)
{
    TreeNodes.Add(secureStoreNode);
}

Add new method CreateSecureStoreNode():

private TreeNodeViewModel? CreateSecureStoreNode()
{
    if (_appSettings?.SecureStore == null)
        return null;

    if (string.IsNullOrEmpty(ConfigFolderPath) || ConfigFolderPath == "No folder selected")
        return null;

    var storePath = Path.IsPathRooted(_appSettings.SecureStore.StorePath)
        ? _appSettings.SecureStore.StorePath
        : Path.Combine(ConfigFolderPath, _appSettings.SecureStore.StorePath);

    var keyFilePath = Path.IsPathRooted(_appSettings.SecureStore.KeyFilePath)
        ? _appSettings.SecureStore.KeyFilePath
        : Path.Combine(ConfigFolderPath, _appSettings.SecureStore.KeyFilePath);

    var storeNode = new TreeNodeViewModel("Secure Store", "🔑", TreeNodeType.SecureStore)
    {
        StorePath = storePath,
        KeyFilePath = keyFilePath,
        SectionKey = storePath,
        IsExpanded = true
    };

    // Add secrets as direct children if store is open
    if (_secureStoreManager.IsStoreOpen)
    {
        foreach (var key in _secureStoreManager.GetKeys().OrderBy(k => k))
        {
            var secretNode = new TreeNodeViewModel(key, "🔐", TreeNodeType.Secret)
            {
                SecretKey = key
            };
            storeNode.Children.Add(secretNode);
        }
    }

    return storeNode;
}

Task 13: Update MainWindowViewModel - Modify OnSelectedNodeChanged

File: NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs

Update the switch statement in OnSelectedNodeChanged() (around lines 530-557):

Remove:

  • case TreeNodeType.SecureStoresFolder: block
  • Lock/unlock logic in case TreeNodeType.SecureStore: block

Replace with:

case TreeNodeType.SecureStore:
    SelectedFormViewModel = CreateSecureStoreInfoFormViewModel();
    return;

case TreeNodeType.Secret:
    SelectedFormViewModel = CreateSecretFormViewModel(_selectedNode);
    return;

Add method:

private SecureStoreInfoFormViewModel CreateSecureStoreInfoFormViewModel()
{
    var storePath = _appSettings?.SecureStore?.StorePath ?? "Unknown";
    var keyFilePath = _appSettings?.SecureStore?.KeyFilePath ?? "Unknown";
    var secretCount = _secureStoreManager.IsStoreOpen ? _secureStoreManager.GetKeys().Count : 0;

    return new SecureStoreInfoFormViewModel(storePath, keyFilePath, secretCount);
}

Task 14: Update MainWindowViewModel - Call InitializeSecureStoreAsync on load

File: NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs

Find the method that loads the config folder (likely LoadConfigFolderAsync or similar).

Add call to InitializeSecureStoreAsync() after loading appsettings.json but before building tree nodes:

// After loading appsettings
_appSettings = await _configFileService.LoadAppSettingsAsync(appSettingsPath, ct);

// Initialize SecureStore (auto-create if needed, open, sync required keys)
await InitializeSecureStoreAsync();

// Then build tree nodes
BuildTreeNodes();

Task 15: Remove LoadConfiguredSecureStore method

File: NEW/src/Utils/JdeScoping.ConfigManager/ViewModels/MainWindowViewModel.cs

Delete the LoadConfiguredSecureStore() method entirely (lines 474-514).

This is replaced by CreateSecureStoreNode() and InitializeSecureStoreAsync().


Task 16: Update DataTemplates for form views

File: NEW/src/Utils/JdeScoping.ConfigManager/Views/MainWindow.axaml (or wherever DataTemplates are defined)

Remove:

  • DataTemplate for SecureStoreLockedFormViewModel
  • DataTemplate for SecureStoreUnlockedFormViewModel

Add:

  • DataTemplate for SecureStoreInfoFormViewModel

Task 17: Update tests

File: NEW/tests/JdeScoping.ConfigManager.Tests/ViewModels/MainWindowViewModelTests.cs

Update or remove tests related to:

  • SecureStoresFolder node type
  • Lock/unlock commands
  • Password-based store operations

Add tests for:

  • Auto-creation of SecureStore on config load
  • Sync of missing required keys
  • Flattened tree structure

Task 18: Build and verify

dotnet build NEW/JdeScoping.slnx
dotnet test NEW/JdeScoping.slnx

Execution Order

  1. Tasks 1-2: TreeNodeViewModel changes (enum, remove lock properties)
  2. Tasks 3-6: SecureStoreManager interface and implementation changes
  3. Tasks 7-9: Delete old form files, create new info form
  4. Tasks 10-15: MainWindowViewModel changes (largest task)
  5. Task 16: Update XAML DataTemplates
  6. Task 17: Update tests
  7. Task 18: Build and verify

Risk Areas

  1. Missing references: Removing lock/unlock commands may break menu items or toolbar buttons
  2. View DataTemplates: Must update form selector to use new SecureStoreInfoFormViewModel
  3. Test coverage: Existing tests may expect lock/unlock behavior

Verification Checklist

  • App builds without errors
  • All tests pass
  • Opening a config folder without SecureStore creates it automatically
  • Opening a config folder with existing SecureStore opens it
  • Missing required keys are added as blank values
  • Tree shows "Secure Store" with secrets directly underneath
  • Clicking "Secure Store" shows instruction panel
  • Clicking a secret shows the secret editor
  • Error dialog shown when keyfile is missing