< 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
84%
Covered branches: 27
Total branches: 32
Branch coverage: 84.3%
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    {
 219        ArgumentNullException.ThrowIfNull(residue);
 220        CanonicalPayloadOptions effectiveOptions = options ?? CanonicalPayloadOptions.Default;
 221        string auditResidueId = GetAuditResidueId(residue);
 22
 223        return CanonicalPayload.Create(
 224            CanonicalArtifactTypes.AuditResidue,
 225            auditResidueId,
 226            residue.SchemaVersion,
 227            effectiveOptions.CanonicalizationVersion,
 228            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    {
 1136        ArgumentNullException.ThrowIfNull(record);
 1137        CanonicalPayloadOptions effectiveOptions = options ?? CanonicalPayloadOptions.Default;
 38
 1139        SortedDictionary<string, object?> content = BuildAuditResidueContent(record, effectiveOptions, record.AuditResid
 1140        content["acknowledgmentId"] = record.AcknowledgmentId;
 1141        content["capabilityGrantId"] = record.CapabilityTokenId;
 1142        content["handshakeId"] = record.HandshakeId;
 1143        content["previousRecordHash"] = record.PreviousRecordHash;
 1144        content["recordedUtc"] = FormatUtc(record.RecordedUtc);
 1145        content["recordId"] = record.RecordId;
 46
 1147        return CanonicalPayload.Create(
 1148            CanonicalArtifactTypes.AuditLedgerRecord,
 1149            record.RecordId,
 1150            record.SchemaVersion,
 1151            effectiveOptions.CanonicalizationVersion,
 1152            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    {
 260        ArgumentNullException.ThrowIfNull(lifecycleEvent);
 261        CanonicalPayloadOptions effectiveOptions = options ?? CanonicalPayloadOptions.Default;
 62
 263        SortedDictionary<string, object?> content = new(StringComparer.Ordinal)
 264        {
 265            ["auditResidueId"] = lifecycleEvent.AuditResidueId,
 266            ["correlationId"] = lifecycleEvent.CorrelationId,
 267            ["eventId"] = lifecycleEvent.EventId,
 268            ["metadata"] = FilterMetadata(lifecycleEvent.Metadata, effectiveOptions),
 269            ["occurredUtc"] = FormatUtc(lifecycleEvent.OccurredUtc),
 270            ["operationName"] = lifecycleEvent.OperationName,
 271            ["outcome"] = lifecycleEvent.Outcome,
 272            ["stage"] = lifecycleEvent.Stage.ToString(),
 273            ["stageSequence"] = lifecycleEvent.StageSequence,
 274            ["traceId"] = lifecycleEvent.TraceId
 275        };
 76
 277        return CanonicalPayload.Create(
 278            CanonicalArtifactTypes.AuditResidueLifecycleEvent,
 279            lifecycleEvent.EventId,
 280            AsiBackboneSchemaVersions.StableArtifactsV1,
 281            effectiveOptions.CanonicalizationVersion,
 282            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    {
 790        ArgumentNullException.ThrowIfNull(envelope);
 791        CanonicalPayloadOptions effectiveOptions = options ?? CanonicalPayloadOptions.Default;
 92
 793        return CanonicalPayload.Create(
 794            CanonicalArtifactTypes.GovernanceEmissionEnvelope,
 795            envelope.EnvelopeId,
 796            envelope.SchemaVersion,
 797            effectiveOptions.CanonicalizationVersion,
 798            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    {
 7106        ArgumentNullException.ThrowIfNull(entry);
 7107        CanonicalPayloadOptions effectiveOptions = options ?? CanonicalPayloadOptions.Default;
 108
 7109        SortedDictionary<string, object?> content = new(StringComparer.Ordinal)
 7110        {
 7111            ["createdUtc"] = FormatUtc(entry.CreatedUtc),
 7112            ["deadLetterReason"] = entry.DeadLetterReason,
 7113            ["envelope"] = BuildGovernanceEmissionEnvelopeContent(entry.Envelope, effectiveOptions),
 7114            ["lastError"] = BuildGovernanceEmissionErrorContent(entry.LastError),
 7115            ["maxRetryCount"] = entry.MaxRetryCount,
 7116            ["metadata"] = FilterMetadata(entry.Metadata, effectiveOptions),
 7117            ["nextRetryUtc"] = FormatUtc(entry.NextRetryUtc),
 7118            ["outboxEntryId"] = entry.OutboxEntryId,
 7119            ["providerName"] = entry.ProviderName,
 7120            ["providerRecordId"] = entry.ProviderRecordId,
 7121            ["retryCount"] = entry.RetryCount,
 7122            ["status"] = entry.Status.ToString(),
 7123            ["updatedUtc"] = FormatUtc(entry.UpdatedUtc)
 7124        };
 125
 7126        return CanonicalPayload.Create(
 7127            CanonicalArtifactTypes.GovernanceOutboxEntry,
 7128            entry.OutboxEntryId,
 7129            entry.Envelope.SchemaVersion,
 7130            effectiveOptions.CanonicalizationVersion,
 7131            content);
 132    }
 133
 134    private static SortedDictionary<string, object?> BuildAuditResidueContent(
 135        IAsiBackboneAuditResidue residue,
 136        CanonicalPayloadOptions options,
 137        string auditResidueId)
 138    {
 13139        return new SortedDictionary<string, object?>(StringComparer.Ordinal)
 13140        {
 13141            ["actorDisplayName"] = residue.ActorDisplayName,
 13142            ["actorId"] = residue.ActorId,
 13143            ["actorType"] = residue.ActorType.ToString(),
 13144            ["auditResidueId"] = auditResidueId,
 13145            ["constraintCount"] = residue.ConstraintCount,
 13146            ["constraintSetHash"] = residue.ConstraintSetHash,
 13147            ["correlationId"] = residue.CorrelationId,
 13148            ["decisionLatencyMs"] = residue.DecisionLatencyMs,
 13149            ["decisionStage"] = residue.DecisionStage,
 13150            ["emitterProvider"] = residue.EmitterProvider,
 13151            ["emitterStatus"] = residue.EmitterStatus,
 13152            ["eventId"] = residue.EventId,
 13153            ["gatewayExecutionId"] = residue.GatewayExecutionId,
 13154            ["metadata"] = FilterMetadata(residue.Metadata, options),
 13155            ["occurredUtc"] = FormatUtc(residue.OccurredUtc),
 13156            ["operationName"] = residue.OperationName,
 13157            ["organizationHash"] = residue.OrganizationHash,
 13158            ["outboxSequence"] = residue.OutboxSequence,
 13159            ["outcome"] = residue.Outcome,
 13160            ["parentSpanId"] = residue.ParentSpanId,
 13161            ["policyHash"] = residue.PolicyHash,
 13162            ["policyScope"] = residue.PolicyScope,
 13163            ["policyVersion"] = residue.PolicyVersion,
 13164            ["reasonCodes"] = NormalizeStringSet(residue.ReasonCodes),
 13165            ["riskScore"] = residue.RiskScore,
 13166            ["schemaVersion"] = residue.SchemaVersion,
 13167            ["spanId"] = residue.SpanId,
 13168            ["tenantHash"] = residue.TenantHash,
 13169            ["traceId"] = residue.TraceId
 13170        };
 171    }
 172
 173    private static SortedDictionary<string, object?> BuildGovernanceEmissionEnvelopeContent(GovernanceEmissionEnvelope e
 174    {
 14175        return new SortedDictionary<string, object?>(StringComparer.Ordinal)
 14176        {
 14177            ["actorId"] = envelope.ActorId,
 14178            ["auditResidueId"] = envelope.AuditResidueId,
 14179            ["correlationId"] = envelope.CorrelationId,
 14180            ["createdUtc"] = FormatUtc(envelope.CreatedUtc),
 14181            ["decisionStage"] = envelope.DecisionStage,
 14182            ["emitterProvider"] = envelope.EmitterProvider,
 14183            ["emitterStatus"] = envelope.EmitterStatus,
 14184            ["envelopeId"] = envelope.EnvelopeId,
 14185            ["eventId"] = envelope.EventId,
 14186            ["eventType"] = envelope.EventType.ToString(),
 14187            ["gatewayExecutionId"] = envelope.GatewayExecutionId,
 14188            ["lifecycleStage"] = envelope.LifecycleStage?.ToString(),
 14189            ["lifecycleStageSequence"] = envelope.LifecycleStageSequence,
 14190            ["metadata"] = FilterMetadata(envelope.Metadata, options),
 14191            ["occurredUtc"] = FormatUtc(envelope.OccurredUtc),
 14192            ["operationName"] = envelope.OperationName,
 14193            ["outboxSequence"] = envelope.OutboxSequence,
 14194            ["outcome"] = envelope.Outcome,
 14195            ["parentSpanId"] = envelope.ParentSpanId,
 14196            ["payload"] = BuildGovernanceEmissionPayloadContent(envelope.Payload, options),
 14197            ["policyHash"] = envelope.PolicyHash,
 14198            ["policyVersion"] = envelope.PolicyVersion,
 14199            ["schemaVersion"] = envelope.SchemaVersion,
 14200            ["spanId"] = envelope.SpanId,
 14201            ["traceId"] = envelope.TraceId
 14202        };
 203    }
 204
 205    private static SortedDictionary<string, object?>? BuildGovernanceEmissionPayloadContent(GovernanceEmissionPayload? p
 206    {
 14207        return payload is null
 14208            ? null
 14209            : new SortedDictionary<string, object?>(StringComparer.Ordinal)
 14210            {
 14211                ["contentHash"] = payload.ContentHash,
 14212                ["contentType"] = payload.ContentType,
 14213                ["metadata"] = FilterMetadata(payload.Metadata, options),
 14214                ["payloadType"] = payload.PayloadType,
 14215                ["schemaVersion"] = payload.SchemaVersion,
 14216                ["sizeBytes"] = payload.SizeBytes
 14217            };
 218    }
 219
 220    private static SortedDictionary<string, object?>? BuildGovernanceEmissionErrorContent(GovernanceEmissionError? error
 221    {
 7222        return error is null
 7223            ? null
 7224            : new SortedDictionary<string, object?>(StringComparer.Ordinal)
 7225            {
 7226                ["code"] = error.Code,
 7227                ["isRetryable"] = error.IsRetryable,
 7228                ["message"] = error.Message,
 7229                ["providerErrorCode"] = error.ProviderErrorCode,
 7230                ["providerName"] = error.ProviderName
 7231            };
 232    }
 233
 234    private static SortedDictionary<string, object?> FilterMetadata(IReadOnlyDictionary<string, string>? metadata, Canon
 235    {
 46236        SortedDictionary<string, object?> filteredMetadata = new(StringComparer.Ordinal);
 237
 46238        if (metadata is null || metadata.Count == 0)
 239        {
 24240            return filteredMetadata;
 241        }
 242
 124243        foreach (KeyValuePair<string, string> item in metadata)
 244        {
 40245            if (!options.AllowsMetadataKey(item.Key))
 246            {
 247                continue;
 248            }
 249
 23250            filteredMetadata[item.Key.Trim()] = item.Value?.Trim() ?? string.Empty;
 251        }
 252
 22253        return filteredMetadata;
 254    }
 255
 256    private static string[] NormalizeStringSet(IEnumerable<string> values)
 257    {
 13258        return [.. values
 22259            .Where(value => !string.IsNullOrWhiteSpace(value))
 21260            .Select(value => value.Trim())
 13261            .Distinct(StringComparer.Ordinal)
 23262            .OrderBy(value => value, StringComparer.Ordinal)];
 263    }
 264
 265    private static string? FormatUtc(DateTimeOffset? timestamp)
 266    {
 7267        return timestamp.HasValue ? FormatUtc(timestamp.Value) : null;
 268    }
 269
 270    private static string FormatUtc(DateTimeOffset timestamp)
 271    {
 70272        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    {
 2277        return string.IsNullOrWhiteSpace(residue.AuditResidueId)
 2278            ? residue.EventId
 2279            : 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)