Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ permissions:
jobs:
build-test:
runs-on: dotnet
timeout-minutes: 20
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
with:
Expand Down
28 changes: 28 additions & 0 deletions Compendium.sln
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Adapters.GitHub"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Adapters.GitHub.Tests", "tests\Unit\Compendium.Adapters.GitHub.Tests\Compendium.Adapters.GitHub.Tests.csproj", "{5FE1601C-93E1-4E92-9AC9-521AA26592CB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Abstractions.Secrets", "src\Abstractions\Compendium.Abstractions.Secrets\Compendium.Abstractions.Secrets.csproj", "{7A60AF89-1328-4BC4-96F9-9D7113D49F1A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Abstractions.Secrets.Tests", "tests\Unit\Compendium.Abstractions.Secrets.Tests\Compendium.Abstractions.Secrets.Tests.csproj", "{BC2DB8A6-9C0D-4EBC-87D3-86031B6273EE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Adapters.Scaleway.SecretManager", "src\Adapters\Compendium.Adapters.Scaleway.SecretManager\Compendium.Adapters.Scaleway.SecretManager.csproj", "{7EB70764-89D0-4855-BB8C-8B7F03268D4D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Adapters.Scaleway.SecretManager.Tests", "tests\Unit\Compendium.Adapters.Scaleway.SecretManager.Tests\Compendium.Adapters.Scaleway.SecretManager.Tests.csproj", "{18986C9E-5284-4356-A5FE-0F642CBA1F3D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -496,6 +504,22 @@ Global
{5FE1601C-93E1-4E92-9AC9-521AA26592CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5FE1601C-93E1-4E92-9AC9-521AA26592CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5FE1601C-93E1-4E92-9AC9-521AA26592CB}.Release|Any CPU.Build.0 = Release|Any CPU
{7A60AF89-1328-4BC4-96F9-9D7113D49F1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7A60AF89-1328-4BC4-96F9-9D7113D49F1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7A60AF89-1328-4BC4-96F9-9D7113D49F1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7A60AF89-1328-4BC4-96F9-9D7113D49F1A}.Release|Any CPU.Build.0 = Release|Any CPU
{BC2DB8A6-9C0D-4EBC-87D3-86031B6273EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BC2DB8A6-9C0D-4EBC-87D3-86031B6273EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BC2DB8A6-9C0D-4EBC-87D3-86031B6273EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BC2DB8A6-9C0D-4EBC-87D3-86031B6273EE}.Release|Any CPU.Build.0 = Release|Any CPU
{7EB70764-89D0-4855-BB8C-8B7F03268D4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7EB70764-89D0-4855-BB8C-8B7F03268D4D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7EB70764-89D0-4855-BB8C-8B7F03268D4D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7EB70764-89D0-4855-BB8C-8B7F03268D4D}.Release|Any CPU.Build.0 = Release|Any CPU
{18986C9E-5284-4356-A5FE-0F642CBA1F3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{18986C9E-5284-4356-A5FE-0F642CBA1F3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{18986C9E-5284-4356-A5FE-0F642CBA1F3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{18986C9E-5284-4356-A5FE-0F642CBA1F3D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{FE421F00-7FFD-4666-A961-F1FF325ECD34} = {E35C8F52-5000-4427-9589-AEB5987C1AC6}
Expand Down Expand Up @@ -585,5 +609,9 @@ Global
{C0C1EAC1-5C33-4451-8CD6-2521A8D13557} = {6E0B453A-55CF-4567-ADBD-50CFB84CE629}
{4D9C4249-DD0F-4F07-B9FA-A42DAF5092CE} = {73261E87-8FCA-40B6-940B-E25CBDBE33FB}
{5FE1601C-93E1-4E92-9AC9-521AA26592CB} = {6E0B453A-55CF-4567-ADBD-50CFB84CE629}
{7A60AF89-1328-4BC4-96F9-9D7113D49F1A} = {DE85A2F8-C0BA-47FD-8E9A-7D782FD6D3E2}
{BC2DB8A6-9C0D-4EBC-87D3-86031B6273EE} = {6E0B453A-55CF-4567-ADBD-50CFB84CE629}
{7EB70764-89D0-4855-BB8C-8B7F03268D4D} = {73261E87-8FCA-40B6-940B-E25CBDBE33FB}
{18986C9E-5284-4356-A5FE-0F642CBA1F3D} = {6E0B453A-55CF-4567-ADBD-50CFB84CE629}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// -----------------------------------------------------------------------
// <copyright file="SecretVaultCapabilities.cs" company="Sassy Solutions">
// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
// See LICENSE in the project root for license information.
// </copyright>
// -----------------------------------------------------------------------

namespace Compendium.Abstractions.Secrets.Capabilities;

/// <summary>
/// The support level an adapter declares for a <see cref="SecretVaultCapability"/>.
/// </summary>
public enum SecretVaultCapabilityLevel
{
/// <summary>The capability is not available on this provider.</summary>
None,

/// <summary>The capability is available with limitations (documented in the adapter's CAPABILITIES.md).</summary>
Partial,

/// <summary>The capability is fully supported.</summary>
Full,
}

/// <summary>
/// An adapter's declared support for a single capability.
/// </summary>
/// <param name="Level">The support level.</param>
/// <param name="Limitation">
/// A short human-readable limitation note, required when <paramref name="Level"/>
/// is <see cref="SecretVaultCapabilityLevel.Partial"/> or when a conceptually
/// available capability is declared <see cref="SecretVaultCapabilityLevel.None"/>
/// for adapter-specific reasons.
/// </param>
public sealed record SecretVaultCapabilitySupport(SecretVaultCapabilityLevel Level, string? Limitation = null);

/// <summary>
/// The declarative capability matrix of a secret-vault adapter. Consumers use
/// it to drive UI affordances (hide or disable unsupported features with a
/// reason) and adapters use <see cref="EnsureSupported"/> to fail uniformly.
/// </summary>
public sealed record SecretVaultCapabilities
{
/// <summary>
/// Gets the provider identifier these capabilities describe (matches
/// <see cref="ISecretVault.Provider"/>).
/// </summary>
public required string Provider { get; init; }

/// <summary>
/// Gets the declared support per capability. Capabilities absent from the
/// dictionary are treated as <see cref="SecretVaultCapabilityLevel.None"/>.
/// </summary>
public required IReadOnlyDictionary<SecretVaultCapability, SecretVaultCapabilitySupport> Entries { get; init; }

/// <summary>
/// Returns whether the capability is available at any level
/// (<see cref="SecretVaultCapabilityLevel.Partial"/> or <see cref="SecretVaultCapabilityLevel.Full"/>).
/// </summary>
public bool Supports(SecretVaultCapability capability) =>
Entries.TryGetValue(capability, out var support) && support.Level != SecretVaultCapabilityLevel.None;

/// <summary>
/// Returns success when the capability is available, otherwise the standard
/// <c>SecretVault.CapabilityNotSupported</c> failure. Adapters call this at
/// the top of optional-capability methods so every provider fails identically.
/// </summary>
public Result EnsureSupported(SecretVaultCapability capability) =>
Supports(capability)
? Result.Success()
: Result.Failure(SecretVaultErrors.NotSupported(
Provider,
capability,
Entries.TryGetValue(capability, out var support) ? support.Limitation : null));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// -----------------------------------------------------------------------
// <copyright file="SecretVaultCapability.cs" company="Sassy Solutions">
// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
// See LICENSE in the project root for license information.
// </copyright>
// -----------------------------------------------------------------------

namespace Compendium.Abstractions.Secrets.Capabilities;

/// <summary>
/// The optional capabilities a secret-vault adapter can declare. Consumers use
/// the declared matrix to drive UI affordances and to select providers;
/// adapters use <see cref="SecretVaultCapabilities.EnsureSupported"/> to fail
/// uniformly when an undeclared capability is invoked.
/// </summary>
public enum SecretVaultCapability
{
/// <summary>
/// Versions are immutable once written: the material of a revision can
/// never change, so a revision reference is a stable rollback anchor.
/// This is the core contract; adapters that cannot guarantee it must not
/// declare it, and callers requiring stable history must refuse them.
/// </summary>
ImmutableVersions,

/// <summary>
/// Individual revisions can be disabled (kill-switch for a leaked value)
/// and re-enabled without destroying the material.
/// </summary>
VersionEnableDisable,

/// <summary>
/// Individual revisions can be permanently destroyed (retention/cost
/// control). Destroyed revisions read as not found.
/// </summary>
VersionDestroy,

/// <summary>
/// Secrets are organized in a hierarchical path namespace that supports
/// prefix listing.
/// </summary>
PathHierarchy,

/// <summary>
/// Secrets carry provider-side key/value tags usable as list filters.
/// </summary>
Tags,

/// <summary>
/// The provider accepts payloads larger than 64 KiB per version.
/// </summary>
LargePayload,

/// <summary>
/// The provider supports ephemeral (auto-expiring) secrets. Not surfaced
/// by the v1 ports; declared for capability-driven UI only.
/// </summary>
EphemeralSecrets,

/// <summary>
/// The provider can rotate secret material server-side on a schedule. Not
/// surfaced by the v1 ports; declared for capability-driven UI only.
/// </summary>
ServerSideRotation,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AssemblyName>Compendium.Abstractions.Secrets</AssemblyName>
<RootNamespace>Compendium.Abstractions.Secrets</RootNamespace>
<PackageId>Compendium.Abstractions.Secrets</PackageId>
<Description>Secret-vault abstractions for Compendium Framework: the ISecretVault facade with container and immutable-version ports, a provider-neutral connection/credential model, and declarative per-adapter capabilities. Versions are addressed by explicit revision (never "latest"), which is what makes value history and rollback stable across providers. Provider-agnostic ports for Scaleway Secret Manager, HashiCorp Vault, cloud key vaults, and in-house stores.</Description>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\Core\Compendium.Core\Compendium.Core.csproj" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Compendium.Abstractions.Secrets.Tests" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// -----------------------------------------------------------------------
// <copyright file="SecretVaultConnection.cs" company="Sassy Solutions">
// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
// See LICENSE in the project root for license information.
// </copyright>
// -----------------------------------------------------------------------

namespace Compendium.Abstractions.Secrets.Connections;

/// <summary>
/// Everything an adapter needs to reach one vault tenancy on one provider.
/// Passed explicitly to every port method so adapters stay stateless
/// singletons and a single adapter instance can serve many tenancies
/// (platform-owned today, per-tenant or BYO-vault later) without contract
/// changes.
/// </summary>
public sealed record SecretVaultConnection
{
/// <summary>
/// Gets the provider identifier this connection targets (matches
/// <see cref="ISecretVault.Provider"/>).
/// </summary>
public required string Provider { get; init; }

/// <summary>
/// Gets the provider-side tenancy container the connection is scoped to
/// (Scaleway: the project id; Vault: a namespace). <see langword="null"/>
/// when the provider has no tenancy concept or the adapter's configured
/// default applies.
/// </summary>
public string? Tenancy { get; init; }

/// <summary>
/// Gets the provider region hosting the tenancy, when regional
/// (e.g. <c>"fr-par"</c>). <see langword="null"/> selects the adapter's
/// configured default.
/// </summary>
public string? Region { get; init; }

/// <summary>
/// Gets the credential material used to authenticate.
/// </summary>
public required SecretVaultCredential Credential { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// -----------------------------------------------------------------------
// <copyright file="SecretVaultCredential.cs" company="Sassy Solutions">
// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
// See LICENSE in the project root for license information.
// </copyright>
// -----------------------------------------------------------------------

namespace Compendium.Abstractions.Secrets.Connections;

/// <summary>
/// The credential material of a <see cref="SecretVaultConnection"/>, as a
/// closed union. Token-bearing members redact their material in
/// <c>ToString()</c> so a logged connection never leaks a secret.
/// </summary>
public abstract record SecretVaultCredential
{
private SecretVaultCredential()
{
}

/// <summary>
/// A durable API token or key (Scaleway IAM API key secret, Vault token,
/// cloud key-vault access key).
/// </summary>
/// <param name="Token">The token material. Redacted in <c>ToString()</c>.</param>
public sealed record ApiToken(string Token) : SecretVaultCredential
{
/// <inheritdoc />
public override string ToString() => "ApiToken(***)";
}

/// <summary>
/// No credential material: for in-memory and null vaults, or adapters that
/// authenticate ambiently (workload identity, instance profile).
/// </summary>
public sealed record None : SecretVaultCredential;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// -----------------------------------------------------------------------
// <copyright file="GlobalUsings.cs" company="Sassy Solutions">
// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
// See LICENSE in the project root for license information.
// </copyright>
// -----------------------------------------------------------------------

global using Compendium.Core.Results;
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// -----------------------------------------------------------------------
// <copyright file="ISecretVault.cs" company="Sassy Solutions">
// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
// See LICENSE in the project root for license information.
// </copyright>
// -----------------------------------------------------------------------

using Compendium.Abstractions.Secrets.Capabilities;
using Compendium.Abstractions.Secrets.Services;

namespace Compendium.Abstractions.Secrets;

/// <summary>
/// The secret-vault facade: one instance per provider adapter, carrying the
/// provider discriminator, the declarative capability matrix, and the
/// concern-scoped ports. Consumers resolve <c>IEnumerable&lt;ISecretVault&gt;</c>
/// and dispatch on <see cref="Provider"/> (the sub-ports are also registered
/// individually for single-concern consumers).
/// </summary>
/// <remarks>
/// Implementations are stateless singletons: all tenancy state travels in the
/// <see cref="Connections.SecretVaultConnection"/> passed to every method, so
/// a single adapter instance serves any number of tenancies concurrently.
/// </remarks>
public interface ISecretVault
{
/// <summary>
/// Gets the provider identifier used for dispatch (e.g. <c>"scaleway"</c>,
/// <c>"vault"</c>, <c>"inmemory"</c>).
/// </summary>
string Provider { get; }

/// <summary>
/// Gets the adapter's declared capability matrix.
/// </summary>
SecretVaultCapabilities Capabilities { get; }

/// <summary>Gets the secret container lifecycle port.</summary>
ISecretContainerService Secrets { get; }

/// <summary>Gets the immutable-version port.</summary>
ISecretVersionService Versions { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// -----------------------------------------------------------------------
// <copyright file="CreateVaultSecret.cs" company="Sassy Solutions">
// Copyright (c) 2026 Sassy Solutions. Licensed under the MIT License.
// See LICENSE in the project root for license information.
// </copyright>
// -----------------------------------------------------------------------

namespace Compendium.Abstractions.Secrets.Model;

/// <summary>
/// The request to create a secret container (which holds versions, but no
/// material yet — write the first version with
/// <see cref="Services.ISecretVersionService.AddAsync"/>).
/// </summary>
public sealed record CreateVaultSecret
{
/// <summary>
/// Gets the secret name, unique within its <see cref="Path"/>. Diagnostic
/// and console-facing only — the provider-side id returned at creation is
/// the identity.
/// </summary>
public required string Name { get; init; }

/// <summary>
/// Gets the hierarchical path the secret lives under.
/// </summary>
public required SecretScopePath Path { get; init; }

/// <summary>
/// Gets an optional human-readable description.
/// </summary>
public string? Description { get; init; }

/// <summary>
/// Gets optional provider-side tags (list filters, reconciliation
/// markers). Adapters without the <see cref="Capabilities.SecretVaultCapability.Tags"/>
/// capability ignore them.
/// </summary>
public IReadOnlyDictionary<string, string>? Tags { get; init; }
}
Loading
Loading