// SPDX-License-Identifier: Apache-2.0 // Copyright 2026 Anil Jangra // A minimal RiskRouter client. .NET 8, base class library only. // Server to server only: your backend holds the key, never a browser. using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; public sealed class RiskRouterException(int status, JsonNode? body) : Exception($"{status} {body?["error"]?["code"]}: {body?["error"]?["message"]} ({body?["error"]?["docs"]})") { public int Status { get; } = status; public JsonNode? Body { get; } = body; } public sealed class RiskRouter { private readonly HttpClient _http; private readonly string? _apiKey; public RiskRouter(string? apiKey = null, string? baseUrl = null) { _apiKey = apiKey ?? Environment.GetEnvironmentVariable("RISKROUTER_API_KEY"); var root = (baseUrl ?? Environment.GetEnvironmentVariable("RISKROUTER_BASE") ?? "https://api.riskrouter.eu").TrimEnd('/'); _http = new HttpClient { BaseAddress = new Uri(root + "/"), Timeout = TimeSpan.FromSeconds(10) }; } private async Task Call(HttpMethod method, string path, object? body = null, bool withKey = true, string? idempotencyKey = null) { using var request = new HttpRequestMessage(method, path.TrimStart('/')); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); if (withKey && !string.IsNullOrEmpty(_apiKey)) request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey); if (idempotencyKey != null) request.Headers.Add("Idempotency-Key", idempotencyKey); if (body != null) request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); using var response = await _http.SendAsync(request); var node = JsonNode.Parse(await response.Content.ReadAsStringAsync()); if (!response.IsSuccessStatusCode) throw new RiskRouterException((int)response.StatusCode, node); return node!; } /// Priced by the server, never by you. No key is sent: nothing is recorded. public Task Price(string vertical, Dictionary components) => Call(HttpMethod.Post, "/api/v1/quote", new { active_vertical = vertical, selected_components = components }, withKey: false); /// Priced and recorded. Retrying with the same idempotency key never records twice. /// Check persistence.persisted: a 200 prices the quote even when the ledger refused the write. public Task RecordQuote(string vertical, Dictionary components, string idempotencyKey) => Call(HttpMethod.Post, "/api/v1/quote", new { active_vertical = vertical, selected_components = components }, idempotencyKey: idempotencyKey); /// Record that a regulated decision existed, without sending it. Keep the salt with your record. public async Task<(JsonNode Result, byte[] Salt)> RecordEvidence(string record, string kind) { var salt = RandomNumberGenerator.GetBytes(16); var digest = Convert.ToHexString(SHA256.HashData(salt.Concat(Encoding.UTF8.GetBytes(record)).ToArray())).ToLowerInvariant(); return (await Call(HttpMethod.Post, "/api/v2/evidence", new { record_digest = digest, kind }), salt); } public Task EvidenceProof(long leafIndex) => Call(HttpMethod.Get, $"/api/v2/evidence/proof?leaf_index={leafIndex}"); /// Your entries, the chain they sit in, and the signed head: evidence that outlives us. public Task Export() => Call(HttpMethod.Get, "/api/v1/ledger/export"); }