@namespace ZB.MOM.WW.Secrets.Ui.Components @implements IDisposable @inject ISecretStore Store @inject ISecretCipher Cipher @inject IAuditWriter Audit @inject IServiceProvider Services @* Components/RevealButton.razor — a policy-gated, audited reveal. The plaintext is loaded and shown only AFTER the click, straight from the store+cipher (a human reveal shows the CURRENT value, distinct from any cached machine resolve). The value never enters an audit field. *@ @if (!_revealed) { } else {
@if (_value is not null) { @_value } else { @(_error ?? "Unavailable.") }
} @code { private readonly CancellationTokenSource _cts = new(); private bool _revealed; private string? _value; private string? _error; private bool _busy; /// The secret whose plaintext this button reveals. [Parameter] public SecretName Name { get; set; } /// The cascading authentication state, used as the fallback source of the audit actor. [CascadingParameter] private Task? AuthState { get; set; } private async Task RevealAsync() { _busy = true; AuditOutcome outcome; try { StoredSecret? row = await Store.GetAsync(Name, _cts.Token); if (row is null) { _value = null; _error = "Secret not found."; outcome = AuditOutcome.Failure; } else { _value = Cipher.Decrypt(row); _error = null; outcome = AuditOutcome.Success; } } catch (SecretDecryptionException) { _value = null; _error = "Unable to decrypt secret."; outcome = AuditOutcome.Failure; } catch (Exception) { // Any other unexpected store/cipher fault (e.g. master key unavailable, timeout) must // still be audited as a Failure and surfaced to the user, not left to tear down the // circuit. The value is never shown. _value = null; _error = "Unable to reveal secret."; outcome = AuditOutcome.Failure; } finally { _busy = false; _revealed = true; } string actor = await SecretActorResolver.ResolveAsync(Services, AuthState); try { await Audit.WriteAsync( new AuditEvent { EventId = Guid.NewGuid(), OccurredAtUtc = DateTimeOffset.UtcNow, Actor = actor, Action = "secret.reveal", Outcome = outcome, Category = "Secrets", Target = Name.Value, DetailsJson = outcome == AuditOutcome.Failure ? "{\"reason\":\"error\"}" : null, }, _cts.Token); } catch { // Best-effort audit; never let an audit-write fault surface into the event pipeline. } } private void Hide() { _revealed = false; _value = null; _error = null; } /// public void Dispose() { _cts.Cancel(); _cts.Dispose(); } }