feat(secrets-cli): CRUD flows for the interactive console

This commit is contained in:
Joseph Doherty
2026-07-19 09:37:46 -04:00
parent 138e2c1006
commit fe3ea684f5
5 changed files with 456 additions and 0 deletions
@@ -0,0 +1,57 @@
using Spectre.Console;
using ZB.MOM.WW.Secrets.Abstractions;
namespace ZB.MOM.WW.Secrets.Cli.Interactive.Flows;
/// <summary>
/// Reveals a secret's decrypted plaintext on screen — the one flow whose purpose is to surface a value.
/// It gates the disclosure behind an explicit confirmation: on decline nothing is printed; on accept the
/// value is decrypted with the session cipher and written exactly once (no caching, no resolver). Requires
/// a KEK-capable session.
/// </summary>
public sealed class RevealSecretFlow : IInteractiveFlow
{
/// <inheritdoc />
public string Title => "Get (reveal) a secret";
/// <inheritdoc />
public bool RequiresKek => true;
/// <inheritdoc />
public async Task RunAsync(IAnsiConsole console, SecretsSession session, CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(console);
ArgumentNullException.ThrowIfNull(session);
var name = new SecretName(console.Prompt(
new TextPrompt<string>("Secret [green]name[/]:").Validate(NameValidation)));
StoredSecret? row = await session.Store.GetAsync(name, ct).ConfigureAwait(false);
if (row is null || row.IsDeleted)
{
console.MarkupLineInterpolated($"[red]No secret named '{name.Value}'.[/]");
return;
}
if (!console.Prompt(new ConfirmationPrompt("Reveal the value on screen?") { DefaultValue = false }))
{
return;
}
console.WriteLine(session.Cipher!.Decrypt(row));
}
// Re-prompts on an invalid name by surfacing the SecretName constructor's own diagnostic.
private static ValidationResult NameValidation(string raw)
{
try
{
_ = new SecretName(raw);
return ValidationResult.Success();
}
catch (ArgumentException ex)
{
return ValidationResult.Error(ex.Message);
}
}
}