feat(rate-limiting): Implement core rate limiting functionality with configuration, decision-making, metrics, middleware, and service registration
- Add RateLimitConfig for configuration management with YAML binding support. - Introduce RateLimitDecision to encapsulate the result of rate limit checks. - Implement RateLimitMetrics for OpenTelemetry metrics tracking. - Create RateLimitMiddleware for enforcing rate limits on incoming requests. - Develop RateLimitService to orchestrate instance and environment rate limit checks. - Add RateLimitServiceCollectionExtensions for dependency injection registration.
This commit is contained in:
@@ -504,6 +504,161 @@ internal static class CanonicalJson
|
||||
}
|
||||
```
|
||||
|
||||
### 11.1 Full Canonical JSON with Sorted Keys
|
||||
|
||||
> **Added**: 2025-12-17 from "Building a Deeper Moat Beyond Reachability" advisory
|
||||
|
||||
```csharp
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
public static class CanonJson
|
||||
{
|
||||
public static byte[] Canonicalize<T>(T obj)
|
||||
{
|
||||
var json = JsonSerializer.SerializeToUtf8Bytes(obj, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = false,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
using var ms = new MemoryStream();
|
||||
using var writer = new Utf8JsonWriter(ms, new JsonWriterOptions { Indented = false });
|
||||
|
||||
WriteElementSorted(doc.RootElement, writer);
|
||||
writer.Flush();
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
private static void WriteElementSorted(JsonElement el, Utf8JsonWriter w)
|
||||
{
|
||||
switch (el.ValueKind)
|
||||
{
|
||||
case JsonValueKind.Object:
|
||||
w.WriteStartObject();
|
||||
foreach (var prop in el.EnumerateObject().OrderBy(p => p.Name, StringComparer.Ordinal))
|
||||
{
|
||||
w.WritePropertyName(prop.Name);
|
||||
WriteElementSorted(prop.Value, w);
|
||||
}
|
||||
w.WriteEndObject();
|
||||
break;
|
||||
|
||||
case JsonValueKind.Array:
|
||||
w.WriteStartArray();
|
||||
foreach (var item in el.EnumerateArray())
|
||||
WriteElementSorted(item, w);
|
||||
w.WriteEndArray();
|
||||
break;
|
||||
|
||||
default:
|
||||
el.WriteTo(w);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static string Sha256Hex(ReadOnlySpan<byte> bytes)
|
||||
=> Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
|
||||
}
|
||||
```
|
||||
|
||||
## 11.2 SCORE PROOF LEDGER
|
||||
|
||||
> **Added**: 2025-12-17 from "Building a Deeper Moat Beyond Reachability" advisory
|
||||
|
||||
The Score Proof Ledger provides an append-only trail of scoring decisions with per-node hashing.
|
||||
|
||||
### Proof Node Types
|
||||
|
||||
```csharp
|
||||
public enum ProofNodeKind { Input, Transform, Delta, Score }
|
||||
|
||||
public sealed record ProofNode(
|
||||
string Id,
|
||||
ProofNodeKind Kind,
|
||||
string RuleId,
|
||||
string[] ParentIds,
|
||||
string[] EvidenceRefs, // digests / refs inside bundle
|
||||
double Delta, // 0 for non-Delta nodes
|
||||
double Total, // running total at this node
|
||||
string Actor, // module name
|
||||
DateTimeOffset TsUtc,
|
||||
byte[] Seed,
|
||||
string NodeHash // sha256 over canonical node (excluding NodeHash)
|
||||
);
|
||||
```
|
||||
|
||||
### Proof Hashing
|
||||
|
||||
```csharp
|
||||
public static class ProofHashing
|
||||
{
|
||||
public static ProofNode WithHash(ProofNode n)
|
||||
{
|
||||
var canonical = CanonJson.Canonicalize(new
|
||||
{
|
||||
n.Id, n.Kind, n.RuleId, n.ParentIds, n.EvidenceRefs, n.Delta, n.Total,
|
||||
n.Actor, n.TsUtc, Seed = Convert.ToBase64String(n.Seed)
|
||||
});
|
||||
|
||||
return n with { NodeHash = "sha256:" + CanonJson.Sha256Hex(canonical) };
|
||||
}
|
||||
|
||||
public static string ComputeRootHash(IEnumerable<ProofNode> nodesInOrder)
|
||||
{
|
||||
// Deterministic: root hash over canonical JSON array of node hashes in order.
|
||||
var arr = nodesInOrder.Select(n => n.NodeHash).ToArray();
|
||||
var bytes = CanonJson.Canonicalize(arr);
|
||||
return "sha256:" + CanonJson.Sha256Hex(bytes);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Minimal Ledger
|
||||
|
||||
```csharp
|
||||
public sealed class ProofLedger
|
||||
{
|
||||
private readonly List<ProofNode> _nodes = new();
|
||||
public IReadOnlyList<ProofNode> Nodes => _nodes;
|
||||
|
||||
public void Append(ProofNode node)
|
||||
{
|
||||
_nodes.Add(ProofHashing.WithHash(node));
|
||||
}
|
||||
|
||||
public string RootHash() => ProofHashing.ComputeRootHash(_nodes);
|
||||
}
|
||||
```
|
||||
|
||||
### Score Replay Invariant
|
||||
|
||||
The score replay must produce identical ledger root hashes given:
|
||||
- Same manifest (artifact, snapshots, policy)
|
||||
- Same seed
|
||||
- Same timestamp (or frozen clock)
|
||||
|
||||
```csharp
|
||||
public class DeterminismTests
|
||||
{
|
||||
[Fact]
|
||||
public void Score_Replay_IsBitIdentical()
|
||||
{
|
||||
var seed = Enumerable.Repeat((byte)7, 32).ToArray();
|
||||
var inputs = new ScoreInputs(9.0, 0.50, false, ReachabilityClass.Unknown, new("enforced","ro"));
|
||||
|
||||
var (s1, l1) = RiskScoring.Score(inputs, "scanA", seed, DateTimeOffset.Parse("2025-01-01T00:00:00Z"));
|
||||
var (s2, l2) = RiskScoring.Score(inputs, "scanA", seed, DateTimeOffset.Parse("2025-01-01T00:00:00Z"));
|
||||
|
||||
Assert.Equal(s1, s2, 10);
|
||||
Assert.Equal(l1.RootHash(), l2.RootHash());
|
||||
Assert.True(l1.Nodes.Zip(l2.Nodes).All(z => z.First.NodeHash == z.Second.NodeHash));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 12. REPLAY RUNNER
|
||||
|
||||
```csharp
|
||||
|
||||
Reference in New Issue
Block a user