// Create an ASP.NET Core empty project and add Microsoft.Data.Sqlite. // Configure WEBHOOK_SECRET and WEBHOOK_ENVIRONMENT; use a persistent SQLite database volume. using Microsoft.Data.Sqlite; using System.Security.Cryptography; using System.Text; using System.Text.Json; var builder = WebApplication.CreateBuilder(args); var secret = Environment.GetEnvironmentVariable("WEBHOOK_SECRET") ?? throw new InvalidOperationException("Set WEBHOOK_SECRET."); var environment = Environment.GetEnvironmentVariable("WEBHOOK_ENVIRONMENT"); if (!secret.StartsWith("whsec_") || environment is not ("production" or "simulation")) throw new InvalidOperationException("Configure the secret and environment for this endpoint."); byte[] key = Convert.FromBase64String(secret[6..]); string connectionString = new SqliteConnectionStringBuilder { DataSource = Environment.GetEnvironmentVariable("WEBHOOK_DATABASE") ?? "webhook-inbox.sqlite" }.ToString(); await using (var db = new SqliteConnection(connectionString)) { await db.OpenAsync(); var command = db.CreateCommand(); command.CommandText = """ PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; CREATE TABLE IF NOT EXISTS receipts(event_id TEXT PRIMARY KEY, payload TEXT NOT NULL, received_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS invoice_status(environment TEXT, egs_unit_id TEXT, request_id TEXT, sequence INTEGER, payload TEXT, PRIMARY KEY(environment, egs_unit_id, request_id)); """; await command.ExecuteNonQueryAsync(); } var app = builder.Build(); app.MapPost("/webhooks/avtax", async (HttpRequest request) => { string id = request.Headers["webhook-id"].ToString(); string stamp = request.Headers["webhook-timestamp"].ToString(); long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (!Guid.TryParse(id, out _) || !long.TryParse(stamp, out long timestamp) || timestamp < now - 300 || timestamp > now + 300) return Results.Unauthorized(); using var body = new MemoryStream(); var buffer = new byte[8192]; int count; while ((count = await request.Body.ReadAsync(buffer)) > 0) { if (body.Length + count > 1024 * 1024) return Results.StatusCode(413); await body.WriteAsync(buffer.AsMemory(0, count)); } byte[] raw = body.ToArray(); byte[] prefix = Encoding.UTF8.GetBytes($"{id}.{stamp}."); byte[] signed = new byte[prefix.Length + raw.Length]; prefix.CopyTo(signed, 0); raw.CopyTo(signed, prefix.Length); byte[] expected = HMACSHA256.HashData(key, signed); bool verified = false; foreach (string part in request.Headers["webhook-signature"].ToString().Split(' ', StringSplitOptions.RemoveEmptyEntries)) { if (!part.StartsWith("v1,")) continue; try { verified |= CryptographicOperations.FixedTimeEquals(Convert.FromBase64String(part[3..]), expected); } catch (FormatException) { } } if (!verified) return Results.Unauthorized(); using var parsed = JsonDocument.Parse(raw); var payload = parsed.RootElement; if (payload.GetProperty("eventId").GetString() != id || payload.GetProperty("environment").GetString() != environment || payload.GetProperty("schemaVersion").GetString() != "1.0") return Results.BadRequest(); string json = Encoding.UTF8.GetString(raw); await using var connection = new SqliteConnection(connectionString); await connection.OpenAsync(); await using var transaction = (SqliteTransaction)await connection.BeginTransactionAsync(); var insert = connection.CreateCommand(); insert.Transaction = transaction; insert.CommandText = "INSERT OR IGNORE INTO receipts VALUES ($id, $payload, $received)"; insert.Parameters.AddWithValue("$id", id); insert.Parameters.AddWithValue("$payload", json); insert.Parameters.AddWithValue("$received", DateTimeOffset.UtcNow.ToString("O")); int added = await insert.ExecuteNonQueryAsync(); if (added > 0 && payload.GetProperty("eventType").GetString() is "invoice.reporting.delayed" or "invoice.reporting.completed") { var project = connection.CreateCommand(); project.Transaction = transaction; project.CommandText = """ INSERT INTO invoice_status VALUES ($environment, $egs, $request, $sequence, $payload) ON CONFLICT(environment, egs_unit_id, request_id) DO UPDATE SET sequence=excluded.sequence, payload=excluded.payload WHERE excluded.sequence > invoice_status.sequence """; project.Parameters.AddWithValue("$environment", environment); project.Parameters.AddWithValue("$egs", payload.GetProperty("egsUnitId").GetString()); project.Parameters.AddWithValue("$request", payload.GetProperty("data").GetProperty("clientRequestId").GetString()); project.Parameters.AddWithValue("$sequence", payload.GetProperty("sequence").GetInt32()); project.Parameters.AddWithValue("$payload", json); await project.ExecuteNonQueryAsync(); } await transaction.CommitAsync(); return Results.NoContent(); }); app.Run();