AuditChainHasher
AgentPrism.Core.dllComputes the hash chain link for an audit trail entry. The single place the canonical form is defined; both the write path and the verify path call it, so the two can never drift apart.
public static class AuditChainHasherInheritance
Section titled “Inheritance”Inherited Members
Section titled “Inherited Members”object.GetType(), object.MemberwiseClone(), object.ToString(), object.Equals(object?), object.Equals(object?, object?), object.ReferenceEquals(object?, object?), object.GetHashCode()
Remarks
Section titled “Remarks”The canonical form is a fixed-shape, hand-written JSON object — fields are written in a FIXED order and the writer is not System.Text.Json’s default serializer, so there is no property-ordering ambiguity to worry about. The field order is part of the contract: changing it changes every hash computed afterward and makes an already-written chain unverifiable.
createdAt is rounded down to microsecond precision before it is
hashed. PostgreSQL’s timestamptz stores only microsecond precision (it
drops the last digit of a.NET 100ns tick); without this rounding, a value
hashed with full tick precision at write time would never match the value
read back from PostgreSQL at verify time, and every entry would misreport as
AuditChainStatus.Broken. SQL Server and SQLite both preserve
full tick precision, so rounding is harmless there — it is applied
unconditionally so the same canonical form works on all three providers.
Methods
Section titled “Methods”ComputeHash(string?, string, string?, string, string, string?, string?, DateTimeOffset)
Section titled “ ComputeHash(string?, string, string?, string, string, string?, string?, DateTimeOffset)”Computes the hash of an audit entry.
public static string ComputeHash(string? previousHash, string tenantId, string? actor, string action, string entity, string? before, string? after, DateTimeOffset createdAt)Parameters
Section titled “Parameters”previousHash string?
The hash of the previous entry of the same tenant; null for the first entry.
tenantId string
The tenant id.
actor string?
The actor.
action string
The action name.
entity string
The affected entity.
before string?
The state before the change, as JSON text.
after string?
The state after the change, as JSON text.
createdAt DateTimeOffset
The time the record was written.
Returns
Section titled “Returns”The hash, as a lowercase 64-character hex string (SHA-256).
TruncateToMicroseconds(DateTimeOffset)
Section titled “ TruncateToMicroseconds(DateTimeOffset)”Rounds a timestamp DOWN to microsecond precision — the precision PostgreSQL’s
timestamptz actually stores.
public static DateTimeOffset TruncateToMicroseconds(DateTimeOffset value)Parameters
Section titled “Parameters”value DateTimeOffset
The timestamp.
Returns
Section titled “Returns”The rounded timestamp, in UTC.
Remarks
Section titled “Remarks”The SQL write path (SqlAuditLog.WriteAsync) truncates
AuditEntry.CreatedAt to THIS value BEFORE it is hashed AND
before it is written to the created_at column. Truncating only for
the hash (and leaving the column at full precision) is not enough: whether
Npgsql’s client-side conversion truncates or ROUNDS a sub-microsecond
remainder is an implementation detail this code must not depend on —
measured, it does not always agree with a simple truncation, and every
entry misreported as AuditChainStatus.Broken as a result. By
pre-truncating the value that is ACTUALLY stored, there is no remainder
left for the database to round any way at all.