Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
7 changes: 7 additions & 0 deletions Compendium.sln
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Architecture", "Architectur
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.ArchitectureTests", "tests\Architecture\Compendium.ArchitectureTests\Compendium.ArchitectureTests.csproj", "{D685E8D8-B3DC-4A65-9ED3-675776610190}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Compendium.Adapters.Shared", "src\Adapters\Compendium.Adapters.Shared\Compendium.Adapters.Shared.csproj", "{DE527E82-7509-4E3F-B002-8D53CFAA97DA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -232,6 +234,10 @@ Global
{D685E8D8-B3DC-4A65-9ED3-675776610190}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D685E8D8-B3DC-4A65-9ED3-675776610190}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D685E8D8-B3DC-4A65-9ED3-675776610190}.Release|Any CPU.Build.0 = Release|Any CPU
{DE527E82-7509-4E3F-B002-8D53CFAA97DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DE527E82-7509-4E3F-B002-8D53CFAA97DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DE527E82-7509-4E3F-B002-8D53CFAA97DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DE527E82-7509-4E3F-B002-8D53CFAA97DA}.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 @@ -278,5 +284,6 @@ Global
{C4495E66-500F-4582-B56D-C30ED0F41FF3} = {A0005488-4247-4872-9CD5-95696E47BEA5}
{72B1C880-12A5-4568-85E0-3800536158DC} = {B23A5693-C266-43DC-8D3E-CBB108131762}
{D685E8D8-B3DC-4A65-9ED3-675776610190} = {72B1C880-12A5-4568-85E0-3800536158DC}
{DE527E82-7509-4E3F-B002-8D53CFAA97DA} = {73261E87-8FCA-40B6-940B-E25CBDBE33FB}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@
private readonly ILogger<LemonSqueezyHttpClient> _logger;
private readonly JsonSerializerOptions _jsonOptions;

private static string SanitizeEndpointForLog(string endpoint)
{
if (string.IsNullOrEmpty(endpoint)) return endpoint ?? string.Empty;
var qIdx = endpoint.IndexOf('?');
return qIdx >= 0 ? endpoint[..qIdx] : endpoint;
}

/// <summary>
/// Initializes a new instance of the <see cref="LemonSqueezyHttpClient"/> class.
/// </summary>
Expand Down Expand Up @@ -282,7 +289,7 @@
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling LemonSqueezy API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error calling LemonSqueezy API: {Endpoint}", SanitizeEndpointForLog(endpoint));

Check failure

Code scanning / CodeQL

Clear text storage of sensitive information High

This stores sensitive data returned by
access to parameter licenseKeyId : String
as clear text.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
return Error.Failure("LemonSqueezy.HttpError", ex.Message);
}
}
Expand All @@ -307,7 +314,7 @@
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling LemonSqueezy API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error calling LemonSqueezy API: {Endpoint}", SanitizeEndpointForLog(endpoint));

Check failure

Code scanning / CodeQL

Clear text storage of sensitive information High

This stores sensitive data returned by
access to parameter licenseKeyId : String
as clear text.

Check warning

Code scanning / CodeQL

Exposure of private information Medium

Private data returned by
access to parameter email
is written to an external location.
Private data returned by
access to parameter email
is written to an external location.
Private data returned by
access to parameter email
is written to an external location.
Private data returned by
access to parameter email
is written to an external location.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
return Error.Failure("LemonSqueezy.HttpError", ex.Message);
}
}
Expand Down Expand Up @@ -343,7 +350,7 @@
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling LemonSqueezy API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error calling LemonSqueezy API: {Endpoint}", SanitizeEndpointForLog(endpoint));
return Error.Failure("LemonSqueezy.HttpError", ex.Message);
}
}
Expand Down Expand Up @@ -379,7 +386,7 @@
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling LemonSqueezy API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error calling LemonSqueezy API: {Endpoint}", SanitizeEndpointForLog(endpoint));
return Error.Failure("LemonSqueezy.HttpError", ex.Message);
}
}
Expand All @@ -401,7 +408,7 @@
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling LemonSqueezy API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error calling LemonSqueezy API: {Endpoint}", SanitizeEndpointForLog(endpoint));
return Error.Failure("LemonSqueezy.HttpError", ex.Message);
}
}
Expand Down Expand Up @@ -432,7 +439,7 @@
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling LemonSqueezy License API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error calling LemonSqueezy License API: {Endpoint}", SanitizeEndpointForLog(endpoint));
return Error.Failure("LemonSqueezy.HttpError", ex.Message);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// </copyright>
// -----------------------------------------------------------------------

using System.Diagnostics;
using Compendium.Adapters.LemonSqueezy.Configuration;
using Compendium.Adapters.LemonSqueezy.Http;
using Compendium.Adapters.LemonSqueezy.Http.Models;
Expand Down Expand Up @@ -130,7 +131,7 @@ public async Task<Result<BillingCustomer>> GetCustomerByEmailAsync(
{
ArgumentNullException.ThrowIfNull(email);

_logger.LogDebug("Getting customer by email {Email}", email);
_logger.LogDebug("Getting customer by email (activity {ActivityId})", Activity.Current?.Id);

var result = await _httpClient.ListCustomersAsync(email, cancellationToken);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@
private readonly ILogger<ListmonkHttpClient> _logger;
private readonly JsonSerializerOptions _jsonOptions;

private static string SanitizeEndpointForLog(string endpoint)
{
if (string.IsNullOrEmpty(endpoint)) return endpoint ?? string.Empty;
var qIdx = endpoint.IndexOf('?');
return qIdx >= 0 ? endpoint[..qIdx] : endpoint;
}

/// <summary>
/// Initializes a new instance of the <see cref="ListmonkHttpClient"/> class.
/// </summary>
Expand Down Expand Up @@ -341,7 +348,7 @@
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", SanitizeEndpointForLog(endpoint));
return Error.Failure("Listmonk.HttpError", ex.Message);
}
}
Expand All @@ -366,7 +373,7 @@
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", SanitizeEndpointForLog(endpoint));
return Error.Failure("Listmonk.HttpError", ex.Message);
}
}
Expand All @@ -391,7 +398,7 @@
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", SanitizeEndpointForLog(endpoint));

Check warning

Code scanning / CodeQL

Exposure of private information Medium

Private data returned by
access to parameter email
is written to an external location.
Private data returned by
access to local variable encodedEmail
is written to an external location.
Private data returned by
access to parameter email
is written to an external location.
Private data returned by
access to parameter email
is written to an external location.
Private data returned by
access to parameter email
is written to an external location.
Private data returned by
access to parameter email
is written to an external location.
Private data returned by
access to parameter email
is written to an external location.
Private data returned by
access to parameter email
is written to an external location.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
return Error.Failure("Listmonk.HttpError", ex.Message);
}
}
Expand Down Expand Up @@ -423,7 +430,7 @@
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", SanitizeEndpointForLog(endpoint));
return Error.Failure("Listmonk.HttpError", ex.Message);
}
}
Expand Down Expand Up @@ -455,7 +462,7 @@
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", SanitizeEndpointForLog(endpoint));
return Error.Failure("Listmonk.HttpError", ex.Message);
}
}
Expand All @@ -479,7 +486,7 @@
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", SanitizeEndpointForLog(endpoint));
return Error.Failure("Listmonk.HttpError", ex.Message);
}
}
Expand All @@ -501,7 +508,7 @@
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", endpoint);
_logger.LogError(ex, "HTTP error calling Listmonk API: {Endpoint}", SanitizeEndpointForLog(endpoint));
return Error.Failure("Listmonk.HttpError", ex.Message);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// </copyright>
// -----------------------------------------------------------------------

using System.Diagnostics;
using Compendium.Adapters.Listmonk.Configuration;
using Compendium.Adapters.Listmonk.Http;
using Compendium.Adapters.Listmonk.Http.Models;
Expand Down Expand Up @@ -40,7 +41,7 @@ public async Task<Result<Subscriber>> SubscribeAsync(
{
ArgumentNullException.ThrowIfNull(request);

_logger.LogInformation("Subscribing {Email} to newsletter", request.Email);
_logger.LogInformation("Subscribing to newsletter (activity {ActivityId})", Activity.Current?.Id);

var listIds = new List<int>();

Expand Down Expand Up @@ -74,12 +75,12 @@ public async Task<Result<Subscriber>> SubscribeAsync(

if (result.IsFailure)
{
_logger.LogWarning("Failed to subscribe {Email}: {Error}", request.Email, result.Error.Message);
_logger.LogWarning("Failed to subscribe (activity {ActivityId}): {Error}", Activity.Current?.Id, result.Error.Message);
return result.Error;
}

var subscriber = MapToSubscriber(result.Value);
_logger.LogInformation("Subscribed {Email} with ID {SubscriberId}", request.Email, subscriber.Id);
_logger.LogInformation("Subscribed with ID {SubscriberId}", subscriber.Id);

return subscriber;
}
Expand All @@ -92,7 +93,7 @@ public async Task<Result> UnsubscribeAsync(
{
ArgumentNullException.ThrowIfNull(email);

_logger.LogInformation("Unsubscribing {Email} from list {ListId}", email, listId ?? "all");
_logger.LogInformation("Unsubscribing from list {ListId} (activity {ActivityId})", listId ?? "all", Activity.Current?.Id);

// First, find the subscriber by email
var subscriberResult = await _httpClient.GetSubscriberByEmailAsync(email, cancellationToken);
Expand All @@ -111,12 +112,12 @@ public async Task<Result> UnsubscribeAsync(

if (result.IsFailure)
{
_logger.LogWarning("Failed to unsubscribe {Email} from list {ListId}: {Error}",
email, listId, result.Error.Message);
_logger.LogWarning("Failed to unsubscribe {SubscriberId} from list {ListId}: {Error}",
subscriberId, listId, result.Error.Message);
}
else
{
_logger.LogInformation("Unsubscribed {Email} from list {ListId}", email, listId);
_logger.LogInformation("Unsubscribed {SubscriberId} from list {ListId}", subscriberId, listId);
}

return result;
Expand All @@ -133,11 +134,11 @@ public async Task<Result> UnsubscribeAsync(

if (result.IsFailure)
{
_logger.LogWarning("Failed to unsubscribe {Email}: {Error}", email, result.Error.Message);
_logger.LogWarning("Failed to unsubscribe {SubscriberId}: {Error}", subscriberId, result.Error.Message);
}
else
{
_logger.LogInformation("Unsubscribed {Email} from all lists", email);
_logger.LogInformation("Unsubscribed {SubscriberId} from all lists", subscriberId);
}

return result.IsSuccess ? Result.Success() : result.Error;
Expand All @@ -151,7 +152,7 @@ public async Task<Result<Subscriber>> GetSubscriberAsync(
{
ArgumentNullException.ThrowIfNull(email);

_logger.LogDebug("Getting subscriber by email {Email}", email);
_logger.LogDebug("Getting subscriber by email (activity {ActivityId})", Activity.Current?.Id);

var result = await _httpClient.GetSubscriberByEmailAsync(email, cancellationToken);

Expand All @@ -172,7 +173,7 @@ public async Task<Result> UpdateSubscriberAttributesAsync(
ArgumentNullException.ThrowIfNull(email);
ArgumentNullException.ThrowIfNull(attributes);

_logger.LogInformation("Updating attributes for subscriber {Email}", email);
_logger.LogInformation("Updating subscriber attributes (activity {ActivityId})", Activity.Current?.Id);

// First, find the subscriber by email
var subscriberResult = await _httpClient.GetSubscriberByEmailAsync(email, cancellationToken);
Expand All @@ -191,12 +192,12 @@ public async Task<Result> UpdateSubscriberAttributesAsync(

if (result.IsFailure)
{
_logger.LogWarning("Failed to update attributes for {Email}: {Error}",
email, result.Error.Message);
_logger.LogWarning("Failed to update attributes for {SubscriberId}: {Error}",
subscriberResult.Value.Id, result.Error.Message);
}
else
{
_logger.LogInformation("Updated attributes for subscriber {Email}", email);
_logger.LogInformation("Updated attributes for subscriber {SubscriberId}", subscriberResult.Value.Id);
}

return result.IsSuccess ? Result.Success() : result.Error;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<AssemblyName>Compendium.Adapters.Shared</AssemblyName>
<RootNamespace>Compendium.Adapters.Shared</RootNamespace>
<PackageId>Compendium.Adapters.Shared</PackageId>
<Description>Shared utilities for Compendium adapters: PII masking helpers, logging conventions.</Description>
</PropertyGroup>

</Project>
30 changes: 30 additions & 0 deletions src/Adapters/Compendium.Adapters.Shared/Logging/PiiMasking.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// -----------------------------------------------------------------------
// <copyright file="PiiMasking.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.Adapters.Shared.Logging;

/// <summary>
/// PII masking helpers for log statements. Use sparingly — prefer non-PII identifiers
/// (subscriber_id, customer_id, activity_id) over masked PII per GDPR data-minimization.
/// </summary>
public static class PiiMasking
{
/// <summary>
/// Masks an email for logging: "john.doe@acme.com" → "j***@acme.com".
/// Returns "&lt;empty&gt;" or "&lt;null&gt;" for non-values.
/// Use only when subscriber_id/customer_id is unavailable AND email correlation is required for debugging.
/// </summary>
/// <param name="email">The email to mask.</param>
/// <returns>Masked email, or a placeholder for empty/invalid input.</returns>
public static string MaskEmail(string? email)
{
if (string.IsNullOrWhiteSpace(email)) return "<empty>";
var atIndex = email.IndexOf('@');
if (atIndex <= 0) return "***";
return $"{email[0]}***{email[atIndex..]}";
Comment on lines +17 to +28

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML docs say this returns "" or "" for non-values, but the implementation returns "" for both null and whitespace, and returns "***" for invalid formats (no '@'). Please align the docs with the actual behavior, or update the implementation to distinguish null vs empty (and document the invalid-email case).

Copilot uses AI. Check for mistakes.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// </copyright>
// -----------------------------------------------------------------------

using System.Diagnostics;
using Compendium.Adapters.Stripe.Configuration;
using Stripe.Checkout;

Expand Down Expand Up @@ -131,7 +132,7 @@ public async Task<Result<BillingCustomer>> GetCustomerByEmailAsync(
}
catch (StripeException ex)
{
_logger.LogError(ex, "Stripe customer lookup by email failed for {Email}", email);
_logger.LogError(ex, "Stripe customer lookup by email failed (activity {ActivityId})", Activity.Current?.Id);
return Result.Failure<BillingCustomer>(
Error.Failure("Billing.Stripe.GetCustomerByEmailFailed", ex.Message));
}
Expand Down Expand Up @@ -184,7 +185,7 @@ public async Task<Result<BillingCustomer>> UpsertCustomerAsync(
}
catch (StripeException ex)
{
_logger.LogError(ex, "Stripe customer upsert failed for {Email}", request.Email);
_logger.LogError(ex, "Stripe customer upsert failed (activity {ActivityId})", Activity.Current?.Id);
return Result.Failure<BillingCustomer>(
Error.Failure("Billing.Stripe.UpsertCustomerFailed", ex.Message));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ public async Task<Result<IdentityUser>> GetUserByEmailAsync(

// POM-170: debug logs also flow into centralised log stores, so use the
// short hash here as well.
_logger.LogDebug("Getting user by email (hash {EmailHashPrefix})", HashPrefix(email));
_logger.LogDebug("Getting user by email (hash {HashPrefix})", HashPrefix(email));

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This placeholder was renamed to {HashPrefix} here, but the same file still contains log templates using {EmailHashPrefix} (e.g., in CreateUserAsync). That leaves {Email placeholders in adapter logs and contradicts the PR description/verification grep. Please update the remaining templates to use {HashPrefix} as well for consistency and to keep the grep clean.

Copilot uses AI. Check for mistakes.

var searchRequest = new ZitadelUserSearchRequest
{
Expand Down
Loading