diff --git a/README.md b/README.md
index 2bdf6d3..bf64a33 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,8 @@ In `Development`, startup applies the current EF Core migration set and inserts
Set `Conductor:BootstrapDevelopmentDatabase` to `false` to skip the development database bootstrap in test hosts or other controlled startup scenarios.
+Secret descriptors are managed at `/settings/secrets`. The page supports creating, listing, rotating, and deleting GitHub PAT and OpenAI API key descriptors while masking saved values after entry.
+
## Persistence Configuration
The host registers `ConductorDbContext` from `src/Conductor.Infrastructure.Persistence.Sqlite` using the `ConnectionStrings:Conductor` value. The default is:
diff --git a/docs/feature_guides.md b/docs/feature_guides.md
index 6f46baf..d2bf300 100644
--- a/docs/feature_guides.md
+++ b/docs/feature_guides.md
@@ -34,6 +34,8 @@ The secret descriptor list supports GitHub PAT and OpenAI API key credentials as
Descriptors may include validation status, validation timestamp, a short validation message, and validation metadata JSON such as accepted token prefixes and the runtime environment variable used for injection. Plaintext token values are only accepted during create or rotate workflows and must not be returned in descriptor responses.
+Use the secret management page at `/settings/secrets` to add credential descriptors before wiring repositories or Symphony instances to credentials. Enter the value only when creating or rotating the descriptor; after the operation succeeds, Conductor stores the protected payload separately from descriptor metadata and renders only masked placeholders.
+
## Workflow Profile Management
Workflow profiles are managed from `/settings/workflows`. Operators can create a profile, edit an existing profile, mark one profile as the default, and preview the raw `WORKFLOW.md` source before saving.
diff --git a/docs/user_guide.md b/docs/user_guide.md
index acc5135..732311e 100644
--- a/docs/user_guide.md
+++ b/docs/user_guide.md
@@ -10,9 +10,11 @@ Initial user workflows will cover dashboard review, repository import, instance
The dashboard includes a needs-attention panel for active critical and warning items. Each row shows the affected repository or Symphony instance, the current severity, the reason it needs attention, and a link to the source area for follow-up.
-## Secret Review
+## Secret Management
-The Secrets page lists saved credential descriptors for orchestration. GitHub PAT and OpenAI API key descriptors are shown independently, and saved values are rendered only as masked placeholders.
+Open `/settings/secrets` to create and maintain credential descriptors. The page supports GitHub PAT, OpenAI API key, Codex home, and other secret descriptors scoped globally, by project, by repository, or by Symphony instance.
+
+Saved secret values are masked in the descriptor list. Operators can rotate or delete a descriptor from the list, but stored values are not shown again after creation or rotation.
## Manual Instance Registration
diff --git a/src/Conductor.Host/Components/Pages/Secrets.razor b/src/Conductor.Host/Components/Pages/Secrets.razor
index c4809d2..99198f4 100644
--- a/src/Conductor.Host/Components/Pages/Secrets.razor
+++ b/src/Conductor.Host/Components/Pages/Secrets.razor
@@ -1,9 +1,9 @@
@page "/settings/secrets"
-@using System.Data.Common
+@using System.Globalization
@using Conductor.Core.Abstractions.Secrets
-@using Conductor.Core.Application.Secrets
+@using Conductor.Core.Domain.Ids
@using Conductor.Core.Domain.Secrets
-@inject ISecretDescriptorQueryService SecretDescriptorQueryService
+@inject ISecretStore SecretStore
Secrets - Conductor
@@ -12,14 +12,11 @@
Credential vault
Secrets
-
GitHub and OpenAI credentials available to repository orchestration.
+
Create and maintain orchestration credentials while keeping saved values out of the UI.
- @if (descriptors is not null)
- {
- @descriptors.Count descriptors
- }
+ @descriptors.Count descriptors
@@ -33,99 +30,354 @@
}
- @if (loadError is not null)
+ @if (errorMessage is not null)
{
+ Title="Secret operation failed."
+ Message="Review the requested change and try again."
+ Detail="@errorMessage" />
}
- else if (descriptors is null)
+
+ @if (successMessage is not null)
{
-
+
}
- else if (descriptors.Count == 0)
+
+
+
+
+
+
+
+ @if (isLoading)
+ {
+
+ }
+ else if (descriptors.Count == 0)
+ {
+
+ }
+ else
+ {
+
+ }
+
+
+
+
+@code {
+ private static readonly SecretType[] AvailableSecretTypes =
+ [
+ SecretType.GitHubToken,
+ SecretType.OpenAiApiKey,
+ SecretType.CodexHome,
+ SecretType.Other,
+ ];
+
+ private static readonly SecretScopeType[] AvailableScopeTypes =
+ [
+ SecretScopeType.Global,
+ SecretScopeType.Project,
+ SecretScopeType.Repository,
+ SecretScopeType.SymphonyInstance,
+ ];
+
+ private readonly CreateSecretFormModel createModel = new();
+ private readonly Dictionary rotationValues = [];
+ private List descriptors = [];
+ private bool isLoading = true;
+ private string? errorMessage;
+ private string? successMessage;
+
+ private bool CanCreateSecret =>
+ !string.IsNullOrWhiteSpace(createModel.Name)
+ && !string.IsNullOrWhiteSpace(createModel.Value)
+ && (createModel.ScopeType == SecretScopeType.Global || !string.IsNullOrWhiteSpace(createModel.ScopeId));
+
+ protected override async Task OnInitializedAsync()
{
-
+ await LoadSecretsAsync();
}
- else
+
+ private async Task RefreshSecretsAsync()
{
-
-
-
-
- Name
- Type
- Scope
- Value
- Created
- Rotated
-
-
-
- @foreach (SecretDescriptorView descriptor in descriptors)
- {
-
-
- @descriptor.Name
- @descriptor.EnvironmentVariableName
-
-
- @descriptor.SecretTypeLabel
-
-
- @descriptor.ScopeLabel
-
-
-
- @descriptor.MaskedValue
-
-
-
-
- @descriptor.CreatedAtUtc.ToLocalTime().ToString("MMM d, yyyy")
-
-
-
- @if (descriptor.RotatedAtUtc is { } rotatedAtUtc)
- {
-
- @rotatedAtUtc.ToLocalTime().ToString("MMM d, yyyy")
-
- }
- else
- {
- Not rotated
- }
-
-
- }
-
-
-
+ await LoadSecretsAsync();
}
-
-@code {
- private IReadOnlyList? descriptors;
- private string? loadError;
+ private async Task CreateSecretAsync()
+ {
+ ClearMessages();
- protected override async Task OnInitializedAsync()
+ if (!CanCreateSecret)
+ {
+ errorMessage = "Name, value, and a non-global scope ID are required.";
+ return;
+ }
+
+ try
+ {
+ string? scopeId = createModel.ScopeType == SecretScopeType.Global
+ ? null
+ : createModel.ScopeId.Trim();
+
+ await SecretStore.CreateAsync(
+ new CreateSecretRequest(
+ createModel.Name.Trim(),
+ createModel.SecretType,
+ createModel.ScopeType,
+ scopeId,
+ createModel.Value),
+ CancellationToken.None);
+
+ createModel.Reset();
+ successMessage = "Descriptor created. The saved value is masked and cannot be viewed from this page.";
+ await LoadSecretsAsync(clearMessages: false);
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ errorMessage = ex.Message;
+ }
+ }
+
+ private async Task RotateSecretAsync(SecretId secretId)
{
+ ClearMessages();
+ string value = GetRotationValue(secretId);
+
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ errorMessage = "A replacement value is required before rotating a descriptor.";
+ return;
+ }
+
try
{
- descriptors = await SecretDescriptorQueryService.ListAsync(new SecretQuery());
+ await SecretStore.RotateAsync(secretId, new RotateSecretRequest(value), CancellationToken.None);
+ rotationValues.Remove(secretId);
+ successMessage = "Descriptor rotated. The replacement value is masked and cannot be viewed from this page.";
+ await LoadSecretsAsync(clearMessages: false);
}
- catch (Exception ex) when (ex is DbException or InvalidOperationException or IOException or UnauthorizedAccessException)
+ catch (Exception ex) when (ex is not OperationCanceledException)
{
- loadError = ex.Message;
+ errorMessage = ex.Message;
}
}
+ private async Task DeleteSecretAsync(SecretId secretId)
+ {
+ ClearMessages();
+
+ try
+ {
+ await SecretStore.DeleteAsync(secretId, CancellationToken.None);
+ rotationValues.Remove(secretId);
+ successMessage = "Descriptor deleted.";
+ await LoadSecretsAsync(clearMessages: false);
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ errorMessage = ex.Message;
+ }
+ }
+
+ private async Task LoadSecretsAsync(bool clearMessages = true)
+ {
+ if (clearMessages)
+ {
+ ClearMessages();
+ }
+
+ isLoading = true;
+
+ try
+ {
+ descriptors = (await SecretStore.ListAsync(new SecretQuery(), CancellationToken.None)).ToList();
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ errorMessage = ex.Message;
+ }
+ finally
+ {
+ isLoading = false;
+ }
+ }
+
+ private void ClearMessages()
+ {
+ errorMessage = null;
+ successMessage = null;
+ }
+
+ private string GetRotationValue(SecretId secretId) =>
+ rotationValues.TryGetValue(secretId, out string? value) ? value : string.Empty;
+
+ private void SetRotationValue(SecretId secretId, string? value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ rotationValues.Remove(secretId);
+ return;
+ }
+
+ rotationValues[secretId] = value;
+ }
+
+ private static string GetRotationInputName(SecretId secretId) =>
+ $"secret-rotate-{secretId}";
+
+ private static string FormatInstant(DateTimeOffset instant) =>
+ instant.ToLocalTime().ToString("MMM d, yyyy HH:mm", CultureInfo.CurrentCulture);
+
+ private static string GetUpdatedLabel(SecretDescriptor descriptor) =>
+ descriptor.RotatedAtUtc is null ? "Created" : "Rotated";
+
+ private static string GetScopeLabel(SecretDescriptor descriptor) =>
+ string.IsNullOrWhiteSpace(descriptor.ScopeId)
+ ? GetScopeTypeLabel(descriptor.ScopeType)
+ : $"{GetScopeTypeLabel(descriptor.ScopeType)}: {descriptor.ScopeId}";
+
private static string GetSecretTypeClass(SecretType secretType)
{
return secretType switch
@@ -135,4 +387,36 @@
_ => "secret-type-pill",
};
}
+
+ private static string GetScopeTypeLabel(SecretScopeType scopeType) =>
+ scopeType switch
+ {
+ SecretScopeType.Global => "Global",
+ SecretScopeType.Project => "Project",
+ SecretScopeType.Repository => "Repository",
+ SecretScopeType.SymphonyInstance => "Symphony instance",
+ _ => "Global",
+ };
+
+ private sealed class CreateSecretFormModel
+ {
+ public string Name { get; set; } = string.Empty;
+
+ public SecretType SecretType { get; set; } = SecretType.GitHubToken;
+
+ public SecretScopeType ScopeType { get; set; } = SecretScopeType.Global;
+
+ public string ScopeId { get; set; } = string.Empty;
+
+ public string Value { get; set; } = string.Empty;
+
+ public void Reset()
+ {
+ Name = string.Empty;
+ SecretType = SecretType.GitHubToken;
+ ScopeType = SecretScopeType.Global;
+ ScopeId = string.Empty;
+ Value = string.Empty;
+ }
+ }
}
diff --git a/src/Conductor.Host/wwwroot/app.css b/src/Conductor.Host/wwwroot/app.css
index 2cbbcc2..10bebe7 100644
--- a/src/Conductor.Host/wwwroot/app.css
+++ b/src/Conductor.Host/wwwroot/app.css
@@ -1851,6 +1851,208 @@ h2 {
background: #14261f;
}
+.secret-management {
+ display: grid;
+ gap: 24px;
+ max-width: 1240px;
+}
+
+.secret-management-header,
+.secret-list-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+}
+
+.secret-management-summary {
+ margin: 8px 0 0;
+ color: #aeb9c5;
+}
+
+.secret-management-body {
+ display: grid;
+ grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
+ gap: 18px;
+ align-items: start;
+}
+
+.secret-form-panel,
+.secret-list-panel {
+ display: grid;
+ gap: 18px;
+ padding: 24px;
+ border: 1px solid #2b3138;
+ border-radius: 8px;
+ background: #151a1f;
+}
+
+.secret-form {
+ display: grid;
+ gap: 14px;
+}
+
+.secret-form-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.secret-form label {
+ display: grid;
+ gap: 7px;
+}
+
+.secret-form label span {
+ color: #aeb9c5;
+ font-size: 0.82rem;
+ font-weight: 800;
+ text-transform: uppercase;
+}
+
+.secret-form input,
+.secret-form select,
+.secret-actions input {
+ width: 100%;
+ min-height: 42px;
+ border: 1px solid #3b4652;
+ border-radius: 6px;
+ background: #111417;
+ color: #e6edf3;
+ font: inherit;
+}
+
+.secret-form input,
+.secret-actions input {
+ padding: 0 12px;
+}
+
+.secret-form select {
+ padding: 0 10px;
+}
+
+.secret-form input:disabled {
+ color: #74808d;
+ background: #1b2228;
+}
+
+.secret-form input:focus,
+.secret-form select:focus,
+.secret-actions input:focus {
+ outline: 2px solid #9cc2ff;
+ outline-offset: 2px;
+}
+
+.secret-primary-action,
+.secret-secondary-action,
+.secret-danger-action {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 40px;
+ padding: 0 14px;
+ border: 1px solid transparent;
+ border-radius: 6px;
+ font: inherit;
+ font-size: 0.88rem;
+ font-weight: 800;
+ cursor: pointer;
+}
+
+.secret-primary-action {
+ background: #f2c14e;
+ color: #171b20;
+}
+
+.secret-secondary-action {
+ border-color: #3b4652;
+ background: #232a31;
+ color: #dbe6ee;
+}
+
+.secret-danger-action {
+ border-color: #904247;
+ background: #452022;
+ color: #ffb4ab;
+}
+
+.secret-primary-action:disabled,
+.secret-secondary-action:disabled,
+.secret-danger-action:disabled {
+ cursor: not-allowed;
+ opacity: 0.55;
+}
+
+.secret-table-wrap {
+ overflow-x: auto;
+}
+
+.secret-table {
+ width: 100%;
+ min-width: 940px;
+ border-collapse: collapse;
+}
+
+.secret-table th,
+.secret-table td {
+ padding: 16px 14px;
+ border-top: 1px solid #2b3138;
+ vertical-align: top;
+ text-align: left;
+}
+
+.secret-table thead th {
+ border-top: 0;
+ color: #aeb9c5;
+ font-size: 0.8rem;
+ font-weight: 800;
+ text-transform: uppercase;
+}
+
+.secret-name,
+.secret-scope,
+.secret-masked-value {
+ display: block;
+ color: #ffffff;
+ font-weight: 800;
+ overflow-wrap: anywhere;
+}
+
+.secret-masked-value {
+ width: fit-content;
+ max-width: 100%;
+ padding: 6px 9px;
+ border: 1px solid #3b4652;
+ border-radius: 6px;
+ background: #111417;
+ letter-spacing: 0.08em;
+}
+
+.secret-table code,
+.secret-table small {
+ display: block;
+ margin-top: 6px;
+ color: #aeb9c5;
+ font-size: 0.82rem;
+ overflow-wrap: anywhere;
+}
+
+.secret-table code {
+ width: fit-content;
+ max-width: 100%;
+ padding: 4px 6px;
+ border-radius: 4px;
+ background: #232a31;
+ color: #f2c14e;
+}
+
+.secret-actions {
+ display: grid;
+ grid-template-columns: minmax(160px, 1fr) auto auto;
+ gap: 8px;
+ align-items: center;
+}
+
@keyframes ui-state-spin {
to {
transform: rotate(360deg);
@@ -1886,12 +2088,15 @@ h2 {
.registration-form,
.credential-form-row,
.form-fields,
- .repository-metadata-grid,
- .import-form-grid,
- .workflow-profile-editor,
- .workflow-profile-form-grid,
- .live-activity-header,
- .startup-panel dl {
+ .repository-metadata-grid,
+ .import-form-grid,
+ .secret-management-body,
+ .workflow-profile-editor,
+ .workflow-profile-form-grid,
+ .live-activity-header,
+ .secret-management-header,
+ .secret-list-header,
+ .startup-panel dl {
grid-template-columns: 1fr;
}
@@ -1915,6 +2120,15 @@ h2 {
min-width: 760px;
}
+ .secret-form-grid,
+ .secret-actions {
+ grid-template-columns: 1fr;
+ }
+
+ .secret-table {
+ min-width: 820px;
+ }
+
.metric-grid {
grid-template-columns: 1fr;
}
diff --git a/tests/Conductor.Blazor.Tests/SecretManagementPageTests.cs b/tests/Conductor.Blazor.Tests/SecretManagementPageTests.cs
index 6d6715f..456d125 100644
--- a/tests/Conductor.Blazor.Tests/SecretManagementPageTests.cs
+++ b/tests/Conductor.Blazor.Tests/SecretManagementPageTests.cs
@@ -1,6 +1,6 @@
using Bunit;
using Conductor.Core.Abstractions.Secrets;
-using Conductor.Core.Application.Secrets;
+using Conductor.Core.Domain;
using Conductor.Core.Domain.Ids;
using Conductor.Core.Domain.Secrets;
using Conductor.Host.Components.Pages;
@@ -14,17 +14,16 @@ public sealed class SecretManagementPageTests
public void Secrets_Renders_OpenAi_Key_Descriptor_With_Masked_Value()
{
using BunitContext context = new();
- DateTimeOffset createdAtUtc = DateTimeOffset.Parse("2026-04-29T00:10:00Z");
- SecretDescriptor descriptor = new(
- SecretId.New(),
- "Production OpenAI key",
- SecretType.OpenAiApiKey,
- SecretScopeType.Global,
- scopeId: null,
- createdAtUtc);
-
- context.Services.AddSingleton(
- new StaticSecretDescriptorQueryService([SecretDescriptorView.FromDescriptor(descriptor)]));
+ RecordingSecretStore store = new(
+ new SecretDescriptor(
+ SecretId.New(),
+ "Production OpenAI key",
+ SecretType.OpenAiApiKey,
+ SecretScopeType.Global,
+ scopeId: null,
+ DateTimeOffset.Parse("2026-04-29T00:10:00Z")));
+ store.SetRawValue("Production OpenAI key", "sk-existing-secret-value");
+ context.Services.AddSingleton(store);
IRenderedComponent page = context.Render();
@@ -33,15 +32,14 @@ public void Secrets_Renders_OpenAi_Key_Descriptor_With_Masked_Value()
Assert.Contains("OpenAI API key", page.Markup, StringComparison.Ordinal);
Assert.Contains("OPENAI_API_KEY", page.Markup, StringComparison.Ordinal);
Assert.Contains(SecretTypeMetadata.MaskedDisplayValue, page.Markup, StringComparison.Ordinal);
- Assert.DoesNotContain("sk-", page.Markup, StringComparison.OrdinalIgnoreCase);
+ Assert.DoesNotContain("sk-existing-secret-value", page.Markup, StringComparison.Ordinal);
}
[Fact]
public void Secrets_Renders_Empty_State_When_No_Descriptors_Exist()
{
using BunitContext context = new();
- context.Services.AddSingleton(
- new StaticSecretDescriptorQueryService([]));
+ context.Services.AddSingleton(new RecordingSecretStore());
IRenderedComponent page = context.Render();
@@ -50,14 +48,148 @@ public void Secrets_Renders_Empty_State_When_No_Descriptors_Exist()
Assert.Contains("OpenAI API key", page.Markup, StringComparison.Ordinal);
}
- private sealed class StaticSecretDescriptorQueryService(IReadOnlyList descriptors)
- : ISecretDescriptorQueryService
+ [Fact]
+ public void Secrets_Creates_Rotates_And_Deletes_Without_Rendering_Values()
+ {
+ using BunitContext context = new();
+ RecordingSecretStore store = new();
+ context.Services.AddSingleton(store);
+
+ IRenderedComponent page = context.Render();
+
+ page.Find("input[name='secret-name']").Input("Release Portal PAT");
+ page.Find("input[name='secret-value']").Input("ghp_created-secret-value");
+ page.Find("form[data-secret-create]").Submit();
+
+ page.WaitForAssertion(() =>
+ {
+ CreateSecretRequest createdRequest = Assert.IsType(store.CreatedRequest);
+ Assert.Equal("ghp_created-secret-value", createdRequest.Value);
+ Assert.Contains("Release Portal PAT", page.Markup, StringComparison.Ordinal);
+ Assert.DoesNotContain("ghp_created-secret-value", page.Markup, StringComparison.Ordinal);
+ });
+
+ SecretId createdId = store.CreatedSecretId;
+ page.Find($"input[name='secret-rotate-{createdId}']").Input("ghp_rotated-secret-value");
+ page.FindAll("button")
+ .Single(button => button.TextContent.Contains("Rotate", StringComparison.Ordinal))
+ .Click();
+
+ page.WaitForAssertion(() =>
+ {
+ Assert.Equal(createdId, store.RotatedSecretId);
+ RotateSecretRequest rotatedRequest = Assert.IsType(store.RotatedRequest);
+ Assert.Equal("ghp_rotated-secret-value", rotatedRequest.Value);
+ Assert.DoesNotContain("ghp_rotated-secret-value", page.Markup, StringComparison.Ordinal);
+ });
+
+ page.FindAll("button")
+ .Single(button => button.TextContent.Contains("Delete", StringComparison.Ordinal))
+ .Click();
+
+ page.WaitForAssertion(() =>
+ {
+ Assert.Equal(createdId, store.DeletedSecretId);
+ Assert.Contains("No secret descriptors yet.", page.Markup, StringComparison.Ordinal);
+ Assert.DoesNotContain("Release Portal PAT", page.Markup, StringComparison.Ordinal);
+ });
+ }
+
+ private sealed class RecordingSecretStore : ISecretStore
{
- public Task> ListAsync(
+ private readonly List descriptors;
+ private readonly Dictionary rawValues = [];
+
+ public RecordingSecretStore(params SecretDescriptor[] descriptors)
+ {
+ this.descriptors = descriptors.ToList();
+ }
+
+ public CreateSecretRequest? CreatedRequest { get; private set; }
+
+ public SecretId CreatedSecretId { get; private set; }
+
+ public RotateSecretRequest? RotatedRequest { get; private set; }
+
+ public SecretId RotatedSecretId { get; private set; }
+
+ public SecretId DeletedSecretId { get; private set; }
+
+ public Task CreateAsync(CreateSecretRequest request, CancellationToken cancellationToken)
+ {
+ CreatedRequest = request;
+ CreatedSecretId = SecretId.New();
+ var descriptor = new SecretDescriptor(
+ CreatedSecretId,
+ request.Name,
+ request.SecretType,
+ request.ScopeType,
+ request.ScopeId,
+ DateTimeOffset.Parse("2026-04-29T01:20:00Z"));
+
+ descriptors.Add(descriptor);
+ rawValues[descriptor.Id] = request.Value;
+
+ return Task.FromResult(descriptor);
+ }
+
+ public Task RotateAsync(
+ SecretId secretId,
+ RotateSecretRequest request,
+ CancellationToken cancellationToken)
+ {
+ RotatedSecretId = secretId;
+ RotatedRequest = request;
+ rawValues[secretId] = request.Value;
+ descriptors.Single(descriptor => descriptor.Id == secretId)
+ .MarkRotated(DateTimeOffset.Parse("2026-04-29T01:30:00Z"));
+
+ return Task.CompletedTask;
+ }
+
+ public Task ResolveAsync(
+ SecretReference reference,
+ CancellationToken cancellationToken)
+ {
+ return Task.FromResult(new ResolvedSecret(reference.SecretId, rawValues[reference.SecretId]));
+ }
+
+ public Task ResolveAsync(
+ SecretResolutionRequest request,
+ CancellationToken cancellationToken)
+ {
+ SecretDescriptor? descriptor = descriptors.FirstOrDefault(secret => secret.SecretType == request.SecretType);
+
+ return Task.FromResult(descriptor is null
+ ? null
+ : new ResolvedSecret(descriptor.Id, rawValues[descriptor.Id]));
+ }
+
+ public Task> ListAsync(
SecretQuery query,
- CancellationToken cancellationToken = default)
+ CancellationToken cancellationToken)
+ {
+ return Task.FromResult>(
+ descriptors
+ .Where(descriptor => query.SecretType is null || descriptor.SecretType == query.SecretType)
+ .Where(descriptor => query.ScopeType is null || descriptor.ScopeType == query.ScopeType)
+ .OrderBy(descriptor => descriptor.Name, StringComparer.Ordinal)
+ .ToList());
+ }
+
+ public Task DeleteAsync(SecretId secretId, CancellationToken cancellationToken)
+ {
+ DeletedSecretId = secretId;
+ descriptors.RemoveAll(descriptor => descriptor.Id == secretId);
+ rawValues.Remove(secretId);
+
+ return Task.CompletedTask;
+ }
+
+ public void SetRawValue(string name, string value)
{
- return Task.FromResult(descriptors);
+ SecretDescriptor descriptor = descriptors.Single(secret => secret.Name == name);
+ rawValues[descriptor.Id] = value;
}
}
}