// .NET 8+ example: use alone as Program.cs. No third-party dependencies. // Read: dotnet run -- GET 'integrations/v1/catalogue?sku=EXAMPLE-10ML' // Write: dotnet run -- POST integrations/v1/catalogue product.json // Optional fourth argument is a PERSISTED idempotency key for endpoints that // explicitly support one (inventory transfers / order allocations). Ordinary // CRUD endpoints do not gain idempotency merely by sending the header. // Store API credentials privately. Never place a machine key in browser code. using System.Net; using System.Net.Http.Headers; using System.Text.Json; if (args.Length < 2 || args.Length > 4) throw new ArgumentException("Usage: METHOD integrations/v1/path [body.json] [persisted-idempotency-key]"); var method = new HttpMethod(args[0].ToUpperInvariant()); if (!new[] { "GET", "POST", "PATCH", "PUT", "DELETE" }.Contains(method.Method)) throw new ArgumentException("Use a documented GET, POST, PATCH, PUT or DELETE operation."); var origin = new Uri((Environment.GetEnvironmentVariable("VAPETAX_API_URL") ?? throw new InvalidOperationException("Configure VAPETAX_API_URL.")).TrimEnd('/') + "/"); if ((!origin.IsLoopback && origin.Scheme != Uri.UriSchemeHttps) || (origin.Scheme != Uri.UriSchemeHttp && origin.Scheme != Uri.UriSchemeHttps) || origin.AbsolutePath != "/" || origin.Query.Length > 0 || origin.Fragment.Length > 0 || origin.UserInfo.Length > 0) throw new ArgumentException("Use an HTTPS API origin, or isolated loopback HTTP."); var requestPath = args[1]; if (!requestPath.StartsWith("integrations/v1/", StringComparison.Ordinal) || requestPath.Contains("..") || requestPath.Contains('\\') || requestPath.Contains('#')) throw new ArgumentException("Use a relative, documented integrations/v1/ path."); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { BaseAddress = origin, Timeout = TimeSpan.FromSeconds(30) }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("VAPETAX_API_KEY") ?? throw new InvalidOperationException("Configure VAPETAX_API_KEY securely.")); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); using (var capResponse = await client.GetAsync("integrations/v1/capabilities")) { capResponse.EnsureSuccessStatusCode(); using var caps = JsonDocument.Parse(await capResponse.Content.ReadAsStringAsync()); if (method != HttpMethod.Get && !caps.RootElement.GetProperty("operationalWritesEnabled").GetBoolean()) throw new InvalidOperationException("Operational writes require an enabled isolated pilot. Read the endpoint's scope and gates first."); } byte[]? payload = args.Length >= 3 ? await File.ReadAllBytesAsync(args[2]) : null; if (payload?.Length > 1_048_576) throw new ArgumentException("JSON body exceeds 1 MiB."); if (payload != null) { using var validated = JsonDocument.Parse(payload); } string? key = args.Length == 4 ? args[3] : null; if (key != null && (key.Length < 1 || key.Length > 128 || key.Any(c => c < '!' || c > '~'))) throw new ArgumentException("Persisted keys use 1–128 printable non-space ASCII characters."); // Safe reads may retry transient failures. Writes are attempted ONCE: a timeout // may have committed. Recover by resource/business identity and the documented // idempotency contract. Never blindly repeat a non-idempotent CRUD operation. var attempts = method == HttpMethod.Get ? 4 : 1; for (var attempt = 0; attempt < attempts; attempt++) { using var request = new HttpRequestMessage(method, requestPath); if (payload != null) { request.Content = new ByteArrayContent(payload); request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); } if (key != null) request.Headers.Add("Idempotency-Key", key); try { using var response = await client.SendAsync(request); var result = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) { Console.WriteLine(result); return; } var transient = response.StatusCode == HttpStatusCode.TooManyRequests || (int)response.StatusCode >= 500; if (!transient || attempt + 1 == attempts) throw new InvalidOperationException($"VapeTax returned HTTP {(int)response.StatusCode}: {result}. Reconcile uncertain writes before repeating them."); var delay = response.Headers.RetryAfter?.Delta ?? (response.Headers.RetryAfter?.Date - DateTimeOffset.UtcNow) ?? TimeSpan.FromSeconds(Math.Pow(2, attempt + 1)); await Task.Delay(TimeSpan.FromSeconds(Math.Clamp(delay.TotalSeconds, 1, 60))); } catch (HttpRequestException) when (method == HttpMethod.Get && attempt + 1 < attempts) { await Task.Delay(TimeSpan.FromSeconds(2)); } catch (TaskCanceledException) when (method == HttpMethod.Get && attempt + 1 < attempts) { await Task.Delay(TimeSpan.FromSeconds(2)); } } throw new IOException("Outcome unresolved. Preserve the original request and investigate; do not create a replacement business identity.");