fix(transport): carry Transport + OAuth2 authority/scope on SmtpConfigDto (OAuth2 fields were silently dropped)

This commit is contained in:
Joseph Doherty
2026-08-10 06:43:43 -04:00
parent 2f217f5742
commit 8df15b34b8
6 changed files with 159 additions and 2 deletions
@@ -266,6 +266,12 @@ public sealed class ArtifactDiff
AddIfDifferent(changes, "MaxConcurrentConnections", existing.MaxConcurrentConnections, incoming.MaxConcurrentConnections);
AddIfDifferent(changes, "MaxRetries", existing.MaxRetries, incoming.MaxRetries);
AddIfDifferent(changes, "RetryDelay", existing.RetryDelay.ToString(), incoming.RetryDelay.ToString());
// Non-sensitive delivery metadata: diffed by value (unlike the credential,
// which is presence-only below). Both sides null on pre-EWS bundles, so a
// legacy configuration still classifies Identical.
AddIfDifferent(changes, "OAuth2Authority", existing.OAuth2Authority, incoming.OAuth2Authority);
AddIfDifferent(changes, "OAuth2Scope", existing.OAuth2Scope, incoming.OAuth2Scope);
AddIfDifferent(changes, "Transport", existing.Transport, incoming.Transport);
var existingHasSecret = !string.IsNullOrEmpty(existing.Credentials);
var incomingHasSecret = incoming.Secrets is not null && incoming.Secrets.Values.ContainsKey("Credentials");
@@ -3437,6 +3437,11 @@ public sealed class BundleImporter : IBundleImporter
target.MaxConcurrentConnections = dto.MaxConcurrentConnections;
target.MaxRetries = dto.MaxRetries;
target.RetryDelay = dto.RetryDelay;
// Non-sensitive delivery metadata; null on a bundle written before these
// were carried, which is the entity's "provider defaults / Smtp" state.
target.OAuth2Authority = dto.OAuth2Authority;
target.OAuth2Scope = dto.OAuth2Scope;
target.Transport = dto.Transport;
target.Credentials = dto.Secrets?.Values.TryGetValue("Credentials", out var cred) == true ? cred : null;
}
@@ -285,6 +285,12 @@ public sealed record NotificationRecipientDto(
// WhenWritingNull policy keeps it out of email-only bundles' JSON.
string? PhoneNumber = null);
/// <summary>
/// An email delivery configuration (the <c>SmtpConfiguration</c> entity). Every
/// non-sensitive field is carried directly and only the credential (SMTP password
/// or OAuth2 client secret) rides inside <see cref="Secrets"/>, so a future
/// "share without secrets" export can drop it as a unit.
/// </summary>
public sealed record SmtpConfigDto(
string Host,
int Port,
@@ -295,7 +301,17 @@ public sealed record SmtpConfigDto(
int MaxConcurrentConnections,
int MaxRetries,
TimeSpan RetryDelay,
SecretsBlock? Secrets);
SecretsBlock? Secrets,
// OAuth2 token-endpoint authority and scope: endpoint URLs, not secrets, so
// they belong on the public DTO rather than in the SecretsBlock. Trailing +
// nullable so a bundle written before these were carried deserializes them as
// null (= provider defaults); WhenWritingNull keeps them out of the JSON of
// configurations that don't set them.
string? OAuth2Authority = null,
string? OAuth2Scope = null,
// Delivery transport ("Smtp" or "Ews"); null means Smtp. Trailing + nullable
// for the same additive-evolution reason — no bundleFormatVersion bump.
string? Transport = null);
/// <summary>
/// An SMS provider configuration (the <c>SmsConfiguration</c> entity). Mirrors
@@ -186,7 +186,12 @@ public sealed class EntitySerializer
MaxConcurrentConnections: smtp.MaxConcurrentConnections,
MaxRetries: smtp.MaxRetries,
RetryDelay: smtp.RetryDelay,
Secrets: secrets);
Secrets: secrets,
// Non-sensitive delivery metadata: the OAuth2 endpoint pair and
// the Smtp/Ews transport selector travel as plain DTO fields.
OAuth2Authority: smtp.OAuth2Authority,
OAuth2Scope: smtp.OAuth2Scope,
Transport: smtp.Transport);
}).ToList(),
// Inbound API keys are not transported between environments:
// the bundle carries API methods only. ApiMethod.ApprovedApiKeyIds is also
@@ -486,6 +491,11 @@ public sealed class EntitySerializer
MaxConcurrentConnections = dto.MaxConcurrentConnections,
MaxRetries = dto.MaxRetries,
RetryDelay = dto.RetryDelay,
// Null on a bundle written before these fields were carried, which
// is exactly the entity's "use the provider defaults / Smtp" state.
OAuth2Authority = dto.OAuth2Authority,
OAuth2Scope = dto.OAuth2Scope,
Transport = dto.Transport,
})
.ToList();