< Summary

Information
Class: AsiBackbone.Core.Signing.CanonicalPayloadBuilder
Assembly: AsiBackbone.Core
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/Signing/CanonicalPayloadBuilder.cs
Line coverage
100%
Covered lines: 175
Uncovered lines: 0
Coverable lines: 175
Total lines: 281
Line coverage: 100%
Branch coverage
90%
Covered branches: 29
Total branches: 32
Branch coverage: 90.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/Signing/CanonicalPayloadBuilder.cs

#LineLine coverage
 1using System.Globalization;
 2using AsiBackbone.Core.Audit;
 3using AsiBackbone.Core.Emissions;
 4using AsiBackbone.Core.Outbox;
 5using AsiBackbone.Core.Serialization;
 6
 7namespace AsiBackbone.Core.Signing;
 8
 9/// <summary>
 10/// Builds deterministic, provider-neutral signing payloads for AsiBackbone governance artifacts.
 11/// </summary>
 12public static class CanonicalPayloadBuilder
 13{
 14    /// <summary>
 15    /// Builds a canonical payload for audit residue.
 16    /// </summary>
 17    public static CanonicalPayload ForAuditResidue(IAsiBackboneAuditResidue residue, CanonicalPayloadOptions? options = 
 18    {
 1019        ArgumentNullException.ThrowIfNull(residue);
 1020        CanonicalPayloadOptions effectiveOptions = options ?? CanonicalPayloadOptions.Default;
 1021        string auditResidueId = GetAuditResidueId(residue);
 22
 1023        return CanonicalPayload.Create(
 1024            CanonicalArtifactTypes.AuditResidue,
 1025            auditResidueId,
 1026            residue.SchemaVersion,
 1027            effectiveOptions.CanonicalizationVersion,
 1028            BuildAuditResidueContent(residue, effectiveOptions, auditResidueId));
 29    }
 30
 31    /// <summary>
 32    /// Builds a canonical payload for a persistence-ready audit ledger record.
 33    /// </summary>
 34    public static CanonicalPayload ForAuditLedgerRecord(AuditLedgerRecord record, CanonicalPayloadOptions? options = nul
 35    {
 2236        ArgumentNullException.ThrowIfNull(record);
 2237        CanonicalPayloadOptions effectiveOptions = options ?? CanonicalPayloadOptions.Default;
 38
 2239        SortedDictionary<string, object?> content = BuildAuditResidueContent(record, effectiveOptions, record.AuditResid
 2240        content["acknowledgmentId"] = record.AcknowledgmentId;
 2241        content["capabilityGrantId"] = record.CapabilityTokenId;
 2242        content["handshakeId"] = record.HandshakeId;
 2243        content["previousRecordHash"] = record.PreviousRecordHash;
 2244        content["recordedUtc"] = FormatUtc(record.RecordedUtc);
 2245        content["recordId"] = record.RecordId;
 46
 2247        return CanonicalPayload.Create(
 2248            CanonicalArtifactTypes.AuditLedgerRecord,
 2249            record.RecordId,
 2250            record.SchemaVersion,
 2251            effectiveOptions.CanonicalizationVersion,
 2252            content);
 53    }
 54
 55    /// <summary>
 56    /// Builds a canonical payload for an audit residue lifecycle event.
 57    /// </summary>
 58    public static CanonicalPayload ForAuditResidueLifecycleEvent(AuditResidueLifecycleEvent lifecycleEvent, CanonicalPay
 59    {
 1060        ArgumentNullException.ThrowIfNull(lifecycleEvent);
 1061        CanonicalPayloadOptions effectiveOptions = options ?? CanonicalPayloadOptions.Default;
 62
 1063        SortedDictionary<string, object?> content = new(StringComparer.Ordinal)
 1064        {
 1065            ["auditResidueId"] = lifecycleEvent.AuditResidueId,
 1066            ["correlationId"] = lifecycleEvent.CorrelationId,
 1067            ["eventId"] = lifecycleEvent.EventId,
 1068            ["metadata"] = FilterMetadata(lifecycleEvent.Metadata, effectiveOptions),
 1069            ["occurredUtc"] = FormatUtc(lifecycleEvent.OccurredUtc),
 1070            ["operationName"] = lifecycleEvent.OperationName,
 1071            ["outcome"] = lifecycleEvent.Outcome,
 1072            ["stage"] = lifecycleEvent.Stage.ToString(),
 1073            ["stageSequence"] = lifecycleEvent.StageSequence,
 1074            ["traceId"] = lifecycleEvent.TraceId
 1075        };
 76
 1077        return CanonicalPayload.Create(
 1078            CanonicalArtifactTypes.AuditResidueLifecycleEvent,
 1079            lifecycleEvent.EventId,
 1080            AsiBackboneSchemaVersions.StableArtifactsV1,
 1081            effectiveOptions.CanonicalizationVersion,
 1082            content);
 83    }
 84
 85    /// <summary>
 86    /// Builds a canonical payload for a governance emission envelope.
 87    /// </summary>
 88    public static CanonicalPayload ForGovernanceEmissionEnvelope(GovernanceEmissionEnvelope envelope, CanonicalPayloadOp
 89    {
 2090        ArgumentNullException.ThrowIfNull(envelope);
 2091        CanonicalPayloadOptions effectiveOptions = options ?? CanonicalPayloadOptions.Default;
 92
 2093        return CanonicalPayload.Create(
 2094            CanonicalArtifactTypes.GovernanceEmissionEnvelope,
 2095            envelope.EnvelopeId,
 2096            envelope.SchemaVersion,
 2097            effectiveOptions.CanonicalizationVersion,
 2098            BuildGovernanceEmissionEnvelopeContent(envelope, effectiveOptions));
 99    }
 100
 101    /// <summary>
 102    /// Builds a canonical payload for a durable governance outbox entry.
 103    /// </summary>
 104    public static CanonicalPayload ForGovernanceOutboxEntry(GovernanceOutboxEntry entry, CanonicalPayloadOptions? option
 105    {
 16106        ArgumentNullException.ThrowIfNull(entry);
 16107        CanonicalPayloadOptions effectiveOptions = options ?? CanonicalPayloadOptions.Default;
 108
 16109        SortedDictionary<string, object?> content = new(StringComparer.Ordinal)
 16110        {
 16111            ["createdUtc"] = FormatUtc(entry.CreatedUtc),
 16112            ["deadLetterReason"] = entry.DeadLetterReason,
 16113            ["envelope"] = BuildGovernanceEmissionEnvelopeContent(entry.Envelope, effectiveOptions),
 16114            ["lastError"] = BuildGovernanceEmissionErrorContent(entry.LastError),
 16115            ["maxRetryCount"] = entry.MaxRetryCount,
 16116            ["metadata"] = FilterMetadata(entry.Metadata, effectiveOptions),
 16117            ["nextRetryUtc"] = FormatUtc(entry.NextRetryUtc),
 16118            ["outboxEntryId"] = entry.OutboxEntryId,
 16119            ["providerName"] = entry.ProviderName,
 16120            ["providerRecordId"] = entry.ProviderRecordId,
 16121            ["retryCount"] = entry.RetryCount,
 16122            ["status"] = entry.Status.ToString(),
 16123            ["updatedUtc"] = FormatUtc(entry.UpdatedUtc)
 16124        };
 125
 16126        return CanonicalPayload.Create(
 16127            CanonicalArtifactTypes.GovernanceOutboxEntry,
 16128            entry.OutboxEntryId,
 16129            entry.Envelope.SchemaVersion,
 16130            effectiveOptions.CanonicalizationVersion,
 16131            content);
 132    }
 133
 134    private static SortedDictionary<string, object?> BuildAuditResidueContent(
 135        IAsiBackboneAuditResidue residue,
 136        CanonicalPayloadOptions options,
 137        string auditResidueId)
 138    {
 32139        return new SortedDictionary<string, object?>(StringComparer.Ordinal)
 32140        {
 32141            ["actorDisplayName"] = residue.ActorDisplayName,
 32142            ["actorId"] = residue.ActorId,
 32143            ["actorType"] = residue.ActorType.ToString(),
 32144            ["auditResidueId"] = auditResidueId,
 32145            ["constraintCount"] = residue.ConstraintCount,
 32146            ["constraintSetHash"] = residue.ConstraintSetHash,
 32147            ["correlationId"] = residue.CorrelationId,
 32148            ["decisionLatencyMs"] = residue.DecisionLatencyMs,
 32149            ["decisionStage"] = residue.DecisionStage,
 32150            ["emitterProvider"] = residue.EmitterProvider,
 32151            ["emitterStatus"] = residue.EmitterStatus,
 32152            ["eventId"] = residue.EventId,
 32153            ["gatewayExecutionId"] = residue.GatewayExecutionId,
 32154            ["metadata"] = FilterMetadata(residue.Metadata, options),
 32155            ["occurredUtc"] = FormatUtc(residue.OccurredUtc),
 32156            ["operationName"] = residue.OperationName,
 32157            ["organizationHash"] = residue.OrganizationHash,
 32158            ["outboxSequence"] = residue.OutboxSequence,
 32159            ["outcome"] = residue.Outcome,
 32160            ["parentSpanId"] = residue.ParentSpanId,
 32161            ["policyHash"] = residue.PolicyHash,
 32162            ["policyScope"] = residue.PolicyScope,
 32163            ["policyVersion"] = residue.PolicyVersion,
 32164            ["reasonCodes"] = NormalizeStringSet(residue.ReasonCodes),
 32165            ["riskScore"] = residue.RiskScore,
 32166            ["schemaVersion"] = residue.SchemaVersion,
 32167            ["spanId"] = residue.SpanId,
 32168            ["tenantHash"] = residue.TenantHash,
 32169            ["traceId"] = residue.TraceId
 32170        };
 171    }
 172
 173    private static SortedDictionary<string, object?> BuildGovernanceEmissionEnvelopeContent(GovernanceEmissionEnvelope e
 174    {
 36175        return new SortedDictionary<string, object?>(StringComparer.Ordinal)
 36176        {
 36177            ["actorId"] = envelope.ActorId,
 36178            ["auditResidueId"] = envelope.AuditResidueId,
 36179            ["correlationId"] = envelope.CorrelationId,
 36180            ["createdUtc"] = FormatUtc(envelope.CreatedUtc),
 36181            ["decisionStage"] = envelope.DecisionStage,
 36182            ["emitterProvider"] = envelope.EmitterProvider,
 36183            ["emitterStatus"] = envelope.EmitterStatus,
 36184            ["envelopeId"] = envelope.EnvelopeId,
 36185            ["eventId"] = envelope.EventId,
 36186            ["eventType"] = envelope.EventType.ToString(),
 36187            ["gatewayExecutionId"] = envelope.GatewayExecutionId,
 36188            ["lifecycleStage"] = envelope.LifecycleStage?.ToString(),
 36189            ["lifecycleStageSequence"] = envelope.LifecycleStageSequence,
 36190            ["metadata"] = FilterMetadata(envelope.Metadata, options),
 36191            ["occurredUtc"] = FormatUtc(envelope.OccurredUtc),
 36192            ["operationName"] = envelope.OperationName,
 36193            ["outboxSequence"] = envelope.OutboxSequence,
 36194            ["outcome"] = envelope.Outcome,
 36195            ["parentSpanId"] = envelope.ParentSpanId,
 36196            ["payload"] = BuildGovernanceEmissionPayloadContent(envelope.Payload, options),
 36197            ["policyHash"] = envelope.PolicyHash,
 36198            ["policyVersion"] = envelope.PolicyVersion,
 36199            ["schemaVersion"] = envelope.SchemaVersion,
 36200            ["spanId"] = envelope.SpanId,
 36201            ["traceId"] = envelope.TraceId
 36202        };
 203    }
 204
 205    private static SortedDictionary<string, object?>? BuildGovernanceEmissionPayloadContent(GovernanceEmissionPayload? p
 206    {
 36207        return payload is null
 36208            ? null
 36209            : new SortedDictionary<string, object?>(StringComparer.Ordinal)
 36210            {
 36211                ["contentHash"] = payload.ContentHash,
 36212                ["contentType"] = payload.ContentType,
 36213                ["metadata"] = FilterMetadata(payload.Metadata, options),
 36214                ["payloadType"] = payload.PayloadType,
 36215                ["schemaVersion"] = payload.SchemaVersion,
 36216                ["sizeBytes"] = payload.SizeBytes
 36217            };
 218    }
 219
 220    private static SortedDictionary<string, object?>? BuildGovernanceEmissionErrorContent(GovernanceEmissionError? error
 221    {
 16222        return error is null
 16223            ? null
 16224            : new SortedDictionary<string, object?>(StringComparer.Ordinal)
 16225            {
 16226                ["code"] = error.Code,
 16227                ["isRetryable"] = error.IsRetryable,
 16228                ["message"] = error.Message,
 16229                ["providerErrorCode"] = error.ProviderErrorCode,
 16230                ["providerName"] = error.ProviderName
 16231            };
 232    }
 233
 234    private static SortedDictionary<string, object?> FilterMetadata(IReadOnlyDictionary<string, string>? metadata, Canon
 235    {
 122236        SortedDictionary<string, object?> filteredMetadata = new(StringComparer.Ordinal);
 237
 122238        if (metadata is null || metadata.Count == 0)
 239        {
 78240            return filteredMetadata;
 241        }
 242
 248243        foreach (KeyValuePair<string, string> item in metadata)
 244        {
 80245            if (!options.AllowsMetadataKey(item.Key))
 246            {
 247                continue;
 248            }
 249
 46250            filteredMetadata[item.Key.Trim()] = item.Value?.Trim() ?? string.Empty;
 251        }
 252
 44253        return filteredMetadata;
 254    }
 255
 256    private static string[] NormalizeStringSet(IEnumerable<string> values)
 257    {
 32258        return [.. values
 50259            .Where(value => !string.IsNullOrWhiteSpace(value))
 48260            .Select(value => value.Trim())
 32261            .Distinct(StringComparer.Ordinal)
 52262            .OrderBy(value => value, StringComparer.Ordinal)];
 263    }
 264
 265    private static string? FormatUtc(DateTimeOffset? timestamp)
 266    {
 16267        return timestamp.HasValue ? FormatUtc(timestamp.Value) : null;
 268    }
 269
 270    private static string FormatUtc(DateTimeOffset timestamp)
 271    {
 172272        return timestamp.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss.fffffff'Z'", CultureInfo.InvariantCulture);
 273    }
 274
 275    private static string GetAuditResidueId(IAsiBackboneAuditResidue residue)
 276    {
 10277        return string.IsNullOrWhiteSpace(residue.AuditResidueId)
 10278            ? residue.EventId
 10279            : residue.AuditResidueId;
 280    }
 281}

Methods/Properties

ForAuditResidue(AsiBackbone.Core.Audit.IAsiBackboneAuditResidue,AsiBackbone.Core.Signing.CanonicalPayloadOptions)
ForAuditLedgerRecord(AsiBackbone.Core.Audit.AuditLedgerRecord,AsiBackbone.Core.Signing.CanonicalPayloadOptions)
ForAuditResidueLifecycleEvent(AsiBackbone.Core.Audit.AuditResidueLifecycleEvent,AsiBackbone.Core.Signing.CanonicalPayloadOptions)
ForGovernanceEmissionEnvelope(AsiBackbone.Core.Emissions.GovernanceEmissionEnvelope,AsiBackbone.Core.Signing.CanonicalPayloadOptions)
ForGovernanceOutboxEntry(AsiBackbone.Core.Outbox.GovernanceOutboxEntry,AsiBackbone.Core.Signing.CanonicalPayloadOptions)
BuildAuditResidueContent(AsiBackbone.Core.Audit.IAsiBackboneAuditResidue,AsiBackbone.Core.Signing.CanonicalPayloadOptions,System.String)
BuildGovernanceEmissionEnvelopeContent(AsiBackbone.Core.Emissions.GovernanceEmissionEnvelope,AsiBackbone.Core.Signing.CanonicalPayloadOptions)
BuildGovernanceEmissionPayloadContent(AsiBackbone.Core.Emissions.GovernanceEmissionPayload,AsiBackbone.Core.Signing.CanonicalPayloadOptions)
BuildGovernanceEmissionErrorContent(AsiBackbone.Core.Emissions.GovernanceEmissionError)
FilterMetadata(System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>,AsiBackbone.Core.Signing.CanonicalPayloadOptions)
NormalizeStringSet(System.Collections.Generic.IEnumerable`1<System.String>)
FormatUtc(System.Nullable`1<System.DateTimeOffset>)
FormatUtc(System.DateTimeOffset)
GetAuditResidueId(AsiBackbone.Core.Audit.IAsiBackboneAuditResidue)