Rebrand to Grace Solutions; add README, about_ help, Gitea CI/CD, track Module bin #1

Merged
gsadmin merged 8 commits from dev into main 2026-06-03 01:53:11 +00:00
11 changed files with 327 additions and 82 deletions
Showing only changes of commit 09c577ebd0 - Show all commits
+30
View File
@@ -6,6 +6,36 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos
## Unreleased
## 2026.06.03.0057
- Build produced from commit 7e5209190ac2.
## Unreleased (carried forward)
## 2026.06.03.0056
- Build produced from commit 7e5209190ac2.
## Unreleased (carried forward)
## 2026.06.03.0055
- Build produced from commit 7e5209190ac2.
## Unreleased (carried forward)
## 2026.06.03.0047
- Build produced from commit 7e5209190ac2.
## Unreleased (carried forward)
## 2026.06.03.0046
- Build produced from commit 7e5209190ac2.
## Unreleased (carried forward)
## 2026.06.03.0032
- Build produced from commit c86676010532.
+2 -2
View File
@@ -1,6 +1,6 @@
@{
RootModule = 'PSInfisicalAPI.psm1'
ModuleVersion = '2026.06.03.0032'
ModuleVersion = '2026.06.03.0057'
GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51'
Author = 'Grace Solutions'
CompanyName = 'Grace Solutions'
@@ -27,7 +27,7 @@
LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html'
ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI'
ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.'
CommitHash = 'c86676010532'
CommitHash = '7e5209190ac2'
}
}
}
Binary file not shown.
@@ -89,10 +89,13 @@ namespace PSInfisicalAPI.Cmdlets
throw new InfisicalAuthenticationException("Authentication did not produce an access token.");
}
bool apiVersionExplicitlyBound = MyInvocation.BoundParameters.ContainsKey("ApiVersion");
InfisicalConnection connection = new InfisicalConnection
{
BaseUri = BaseUri,
ApiVersion = ApiVersion,
PinnedApiVersion = apiVersionExplicitlyBound ? ApiVersion : null,
AuthType = authType,
OrganizationId = OrganizationId,
ProjectId = ProjectId,
@@ -16,6 +16,7 @@ namespace PSInfisicalAPI.Cmdlets
[Parameter] public string ProjectId { get; set; }
[Parameter] public string Environment { get; set; }
[Parameter] public string SecretPath { get; set; }
[Parameter] public string ApiVersion { get; set; }
[Parameter] public int? Version { get; set; }
[Parameter] public InfisicalSecretType Type { get; set; } = InfisicalSecretType.Shared;
[Parameter] public SwitchParameter ViewSecretValue { get; set; }
@@ -34,6 +35,7 @@ namespace PSInfisicalAPI.Cmdlets
ProjectId = ProjectId,
Environment = Environment,
SecretPath = SecretPath,
ApiVersion = ApiVersion,
Version = Version,
Type = Type.ToString(),
ViewSecretValue = ViewSecretValue.IsPresent,
@@ -15,6 +15,7 @@ namespace PSInfisicalAPI.Cmdlets
[Parameter] public string ProjectId { get; set; }
[Parameter] public string Environment { get; set; }
[Parameter] public string SecretPath { get; set; }
[Parameter] public string ApiVersion { get; set; }
[Parameter] public SwitchParameter Recursive { get; set; }
[Parameter] public SwitchParameter IncludeImports { get; set; }
[Parameter] public SwitchParameter IncludePersonalOverrides { get; set; }
@@ -34,6 +35,7 @@ namespace PSInfisicalAPI.Cmdlets
ProjectId = ProjectId,
Environment = Environment,
SecretPath = SecretPath,
ApiVersion = ApiVersion,
Recursive = Recursive.IsPresent,
IncludeImports = IncludeImports.IsPresent,
IncludePersonalOverrides = IncludePersonalOverrides.IsPresent,
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Security;
using PSInfisicalAPI.Models;
@@ -8,6 +9,7 @@ namespace PSInfisicalAPI.Connections
{
public Uri BaseUri { get; set; }
public string ApiVersion { get; set; }
public string PinnedApiVersion { get; set; }
public InfisicalAuthType AuthType { get; set; }
public string OrganizationId { get; set; }
public string ProjectId { get; set; }
@@ -17,6 +19,8 @@ namespace PSInfisicalAPI.Connections
public DateTimeOffset? ExpiresAtUtc { get; set; }
public bool IsConnected { get; set; }
public Dictionary<string, string> ResolvedEndpointVersions { get; } = new Dictionary<string, string>(StringComparer.Ordinal);
internal SecureString AccessToken { get; set; }
public override string ToString()
@@ -5,11 +5,13 @@ namespace PSInfisicalAPI.Endpoints
{
public static class InfisicalEndpointRegistry
{
private static readonly Dictionary<string, InfisicalEndpointDefinition> Definitions =
new Dictionary<string, InfisicalEndpointDefinition>
private static readonly Dictionary<string, List<InfisicalEndpointDefinition>> Candidates =
new Dictionary<string, List<InfisicalEndpointDefinition>>
{
{
InfisicalEndpointNames.UniversalAuthLogin,
new List<InfisicalEndpointDefinition>
{
new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.UniversalAuthLogin,
@@ -21,9 +23,12 @@ namespace PSInfisicalAPI.Endpoints
ContainsSecretMaterialInRequest = true,
ContainsSecretMaterialInResponse = true
}
}
},
{
InfisicalEndpointNames.ListSecrets,
new List<InfisicalEndpointDefinition>
{
new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.ListSecrets,
@@ -34,10 +39,24 @@ namespace PSInfisicalAPI.Endpoints
RequiresAuthorization = true,
ContainsSecretMaterialInRequest = false,
ContainsSecretMaterialInResponse = true
},
new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.ListSecrets,
Resource = "Secrets",
Version = "v3",
Method = "GET",
Template = "/api/v3/secrets/raw",
RequiresAuthorization = true,
ContainsSecretMaterialInRequest = false,
ContainsSecretMaterialInResponse = true
}
}
},
{
InfisicalEndpointNames.RetrieveSecret,
new List<InfisicalEndpointDefinition>
{
new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.RetrieveSecret,
@@ -48,24 +67,26 @@ namespace PSInfisicalAPI.Endpoints
RequiresAuthorization = true,
ContainsSecretMaterialInRequest = false,
ContainsSecretMaterialInResponse = true
},
new InfisicalEndpointDefinition
{
Name = InfisicalEndpointNames.RetrieveSecret,
Resource = "Secrets",
Version = "v3",
Method = "GET",
Template = "/api/v3/secrets/raw/{secretName}",
RequiresAuthorization = true,
ContainsSecretMaterialInRequest = false,
ContainsSecretMaterialInResponse = true
}
}
}
};
public static InfisicalEndpointDefinition Get(string name)
{
if (string.IsNullOrEmpty(name))
{
throw new InfisicalConfigurationException("Endpoint name must be provided.");
}
InfisicalEndpointDefinition definition;
if (!Definitions.TryGetValue(name, out definition))
{
throw new InfisicalConfigurationException(string.Concat("Unknown endpoint name: ", name));
}
return definition;
List<InfisicalEndpointDefinition> list = GetCandidatesInternal(name);
return list[0];
}
public static bool TryGet(string name, out InfisicalEndpointDefinition definition)
@@ -76,12 +97,50 @@ namespace PSInfisicalAPI.Endpoints
return false;
}
return Definitions.TryGetValue(name, out definition);
List<InfisicalEndpointDefinition> list;
if (!Candidates.TryGetValue(name, out list) || list == null || list.Count == 0)
{
definition = null;
return false;
}
definition = list[0];
return true;
}
public static IReadOnlyList<InfisicalEndpointDefinition> GetCandidates(string name)
{
return GetCandidatesInternal(name);
}
public static IEnumerable<InfisicalEndpointDefinition> All()
{
return Definitions.Values;
List<InfisicalEndpointDefinition> result = new List<InfisicalEndpointDefinition>();
foreach (List<InfisicalEndpointDefinition> list in Candidates.Values)
{
foreach (InfisicalEndpointDefinition definition in list)
{
result.Add(definition);
}
}
return result;
}
private static List<InfisicalEndpointDefinition> GetCandidatesInternal(string name)
{
if (string.IsNullOrEmpty(name))
{
throw new InfisicalConfigurationException("Endpoint name must be provided.");
}
List<InfisicalEndpointDefinition> list;
if (!Candidates.TryGetValue(name, out list) || list == null || list.Count == 0)
{
throw new InfisicalConfigurationException(string.Concat("Unknown endpoint name: ", name));
}
return list;
}
}
}
+1 -7
View File
@@ -39,13 +39,7 @@ namespace PSInfisicalAPI.Logging
public void Error(string component, string message)
{
string line = InfisicalLogFormatter.FormatNow(InfisicalLogLevel.Error, component, message);
ErrorRecord record = new ErrorRecord(
new InvalidOperationException(message ?? string.Empty),
"PSInfisicalAPI.Error",
ErrorCategory.NotSpecified,
component);
record.ErrorDetails = new ErrorDetails(line);
_cmdlet.WriteError(record);
_cmdlet.WriteWarning(line);
}
}
}
@@ -7,6 +7,7 @@ namespace PSInfisicalAPI.Secrets
public string ProjectId { get; set; }
public string Environment { get; set; }
public string SecretPath { get; set; }
public string ApiVersion { get; set; }
public bool Recursive { get; set; }
public bool? IncludeImports { get; set; }
public bool IncludePersonalOverrides { get; set; }
@@ -22,6 +23,7 @@ namespace PSInfisicalAPI.Secrets
public string ProjectId { get; set; }
public string Environment { get; set; }
public string SecretPath { get; set; }
public string ApiVersion { get; set; }
public int? Version { get; set; }
public string Type { get; set; }
public bool? ViewSecretValue { get; set; }
@@ -32,10 +32,11 @@ namespace PSInfisicalAPI.Secrets
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
if (query == null) { throw new ArgumentNullException(nameof(query)); }
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.ListSecrets);
string resolvedProjectId = FirstNonEmpty(query.ProjectId, connection.ProjectId);
List<KeyValuePair<string, string>> queryParameters = new List<KeyValuePair<string, string>>();
AddIfNotNull(queryParameters, "projectId", FirstNonEmpty(query.ProjectId, connection.ProjectId));
AddIfNotNull(queryParameters, "workspaceId", resolvedProjectId);
AddIfNotNull(queryParameters, "projectId", resolvedProjectId);
AddIfNotNull(queryParameters, "environment", FirstNonEmpty(query.Environment, connection.Environment));
AddIfNotNull(queryParameters, "secretPath", FirstNonEmpty(query.SecretPath, connection.DefaultSecretPath, "/"));
queryParameters.Add(new KeyValuePair<string, string>("recursive", query.Recursive ? "true" : "false"));
@@ -60,13 +61,18 @@ namespace PSInfisicalAPI.Secrets
}
}
Uri uri = InfisicalUriBuilder.Build(connection.BaseUri, definition, null, queryParameters);
InfisicalHttpResponse response = ExecuteAuthorized(connection, definition, "RetrieveSecrets", uri, null);
try
{
_logger.Information(Component, "Attempting to retrieve Infisical secrets. Please Wait...");
EnsureSuccess(response, definition);
InfisicalHttpResponse response = SendWithVersionFallback(
connection,
InfisicalEndpointNames.ListSecrets,
query.ApiVersion,
"RetrieveSecrets",
null,
queryParameters,
null);
InfisicalSecretListResponseDto dto = _serializer.Deserialize<InfisicalSecretListResponseDto>(response.Body);
response.Clear();
@@ -88,12 +94,13 @@ namespace PSInfisicalAPI.Secrets
if (query == null) { throw new ArgumentNullException(nameof(query)); }
if (string.IsNullOrEmpty(query.SecretName)) { throw new InfisicalConfigurationException("SecretName is required."); }
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(InfisicalEndpointNames.RetrieveSecret);
Dictionary<string, string> pathParameters = new Dictionary<string, string> { { "secretName", query.SecretName } };
string resolvedProjectId = FirstNonEmpty(query.ProjectId, connection.ProjectId);
List<KeyValuePair<string, string>> queryParameters = new List<KeyValuePair<string, string>>();
AddIfNotNull(queryParameters, "projectId", FirstNonEmpty(query.ProjectId, connection.ProjectId));
AddIfNotNull(queryParameters, "workspaceId", resolvedProjectId);
AddIfNotNull(queryParameters, "projectId", resolvedProjectId);
AddIfNotNull(queryParameters, "environment", FirstNonEmpty(query.Environment, connection.Environment));
AddIfNotNull(queryParameters, "secretPath", FirstNonEmpty(query.SecretPath, connection.DefaultSecretPath, "/"));
AddIfNotNull(queryParameters, "type", string.IsNullOrEmpty(query.Type) ? "shared" : query.Type.ToLowerInvariant());
@@ -102,13 +109,18 @@ namespace PSInfisicalAPI.Secrets
if (query.ExpandSecretReferences.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("expandSecretReferences", query.ExpandSecretReferences.Value ? "true" : "false")); }
if (query.IncludeImports.HasValue) { queryParameters.Add(new KeyValuePair<string, string>("includeImports", query.IncludeImports.Value ? "true" : "false")); }
Uri uri = InfisicalUriBuilder.Build(connection.BaseUri, definition, pathParameters, queryParameters);
InfisicalHttpResponse response = ExecuteAuthorized(connection, definition, "RetrieveSecret", uri, null);
try
{
_logger.Information(Component, string.Concat("Attempting to retrieve Infisical secret '", query.SecretName, "'. Please Wait..."));
EnsureSuccess(response, definition);
InfisicalHttpResponse response = SendWithVersionFallback(
connection,
InfisicalEndpointNames.RetrieveSecret,
query.ApiVersion,
"RetrieveSecret",
pathParameters,
queryParameters,
null);
InfisicalSecretSingleResponseDto dto = _serializer.Deserialize<InfisicalSecretSingleResponseDto>(response.Body);
response.Clear();
@@ -124,6 +136,160 @@ namespace PSInfisicalAPI.Secrets
}
}
private InfisicalHttpResponse SendWithVersionFallback(
InfisicalConnection connection,
string endpointName,
string perCallApiVersion,
string operationName,
Dictionary<string, string> pathParameters,
List<KeyValuePair<string, string>> queryParameters,
string body)
{
IReadOnlyList<InfisicalEndpointDefinition> allCandidates = InfisicalEndpointRegistry.GetCandidates(endpointName);
string pinned = FirstNonEmpty(perCallApiVersion, connection.PinnedApiVersion);
string cached;
connection.ResolvedEndpointVersions.TryGetValue(endpointName, out cached);
List<InfisicalEndpointDefinition> candidates = OrderCandidates(allCandidates, pinned, cached);
if (candidates.Count == 0)
{
throw new InfisicalConfigurationException(string.Concat(
"No matching endpoint candidate for '", endpointName,
"' with ApiVersion='", pinned ?? string.Empty, "'."));
}
InfisicalApiException lastException = null;
for (int index = 0; index < candidates.Count; index++)
{
InfisicalEndpointDefinition definition = candidates[index];
Uri uri = InfisicalUriBuilder.Build(connection.BaseUri, definition, pathParameters, queryParameters);
InfisicalHttpResponse response = ExecuteAuthorized(connection, definition, operationName, uri, body);
if (response.StatusCode >= 200 && response.StatusCode < 300)
{
connection.ResolvedEndpointVersions[endpointName] = definition.Version;
return response;
}
InfisicalApiException exception = BuildApiException(response, definition);
response.Clear();
bool hasMoreCandidates = (index + 1) < candidates.Count;
bool pinnedHere = !string.IsNullOrEmpty(pinned);
if (!pinnedHere && hasMoreCandidates && IsVersionMismatch(exception))
{
_logger.Warning(Component, string.Concat(
"Endpoint '", endpointName, "' version '", definition.Version,
"' rejected by server (", exception.StatusCode.ToString(CultureInfo.InvariantCulture),
"); falling back to next candidate."));
lastException = exception;
continue;
}
throw exception;
}
throw lastException ?? new InfisicalApiException(string.Concat(
"All API version candidates exhausted for '", endpointName, "'."));
}
private static List<InfisicalEndpointDefinition> OrderCandidates(
IReadOnlyList<InfisicalEndpointDefinition> allCandidates,
string pinned,
string cached)
{
List<InfisicalEndpointDefinition> ordered = new List<InfisicalEndpointDefinition>();
if (!string.IsNullOrEmpty(pinned))
{
foreach (InfisicalEndpointDefinition candidate in allCandidates)
{
if (string.Equals(candidate.Version, pinned, StringComparison.OrdinalIgnoreCase))
{
ordered.Add(candidate);
}
}
return ordered;
}
if (!string.IsNullOrEmpty(cached))
{
foreach (InfisicalEndpointDefinition candidate in allCandidates)
{
if (string.Equals(candidate.Version, cached, StringComparison.OrdinalIgnoreCase))
{
ordered.Add(candidate);
break;
}
}
foreach (InfisicalEndpointDefinition candidate in allCandidates)
{
if (!string.Equals(candidate.Version, cached, StringComparison.OrdinalIgnoreCase))
{
ordered.Add(candidate);
}
}
return ordered;
}
foreach (InfisicalEndpointDefinition candidate in allCandidates)
{
ordered.Add(candidate);
}
return ordered;
}
private static bool IsVersionMismatch(InfisicalApiException exception)
{
string body = exception.SanitizedBody;
bool hasInfisicalErrorEnvelope = !string.IsNullOrEmpty(body)
&& body.IndexOf("\"reqId\"", StringComparison.OrdinalIgnoreCase) >= 0
&& body.IndexOf("\"error\"", StringComparison.OrdinalIgnoreCase) >= 0;
if (exception.StatusCode == 405)
{
return true;
}
if (exception.StatusCode == 404 && !hasInfisicalErrorEnvelope)
{
return true;
}
if (exception.StatusCode == 400 && !string.IsNullOrEmpty(body))
{
if (body.IndexOf("projectSlug", StringComparison.OrdinalIgnoreCase) >= 0 ||
body.IndexOf("workspaceId", StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
}
return false;
}
private static InfisicalApiException BuildApiException(InfisicalHttpResponse response, InfisicalEndpointDefinition definition)
{
InfisicalApiException exception = new InfisicalApiException(string.Concat(
"Infisical API returned ",
response.StatusCode.ToString(CultureInfo.InvariantCulture),
" (", response.ReasonPhrase ?? string.Empty, ")."));
exception.StatusCode = response.StatusCode;
exception.ReasonPhrase = response.ReasonPhrase;
exception.EndpointName = definition.Name;
exception.RequestMethod = definition.Method;
exception.SanitizedBody = response.Body;
return exception;
}
private InfisicalHttpResponse ExecuteAuthorized(InfisicalConnection connection, InfisicalEndpointDefinition definition, string operationName, Uri uri, string body)
{
Dictionary<string, string> headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
@@ -157,23 +323,6 @@ namespace PSInfisicalAPI.Secrets
return _httpClient.Send(request);
}
private static void EnsureSuccess(InfisicalHttpResponse response, InfisicalEndpointDefinition definition)
{
if (response.StatusCode >= 200 && response.StatusCode < 300)
{
return;
}
InfisicalApiException exception = new InfisicalApiException(string.Concat("Infisical API returned ", response.StatusCode.ToString(CultureInfo.InvariantCulture), " (", response.ReasonPhrase ?? string.Empty, ")."));
exception.StatusCode = response.StatusCode;
exception.ReasonPhrase = response.ReasonPhrase;
exception.EndpointName = definition.Name;
exception.RequestMethod = definition.Method;
exception.SanitizedBody = definition.ContainsSecretMaterialInResponse ? "[REDACTED]" : response.Body;
response.Clear();
throw exception;
}
private static void AddIfNotNull(List<KeyValuePair<string, string>> list, string key, string value)
{
if (!string.IsNullOrEmpty(value))