1.6.0: the Trust API (verify: one AERE Proof Protocol envelope -> VALID / INVALID / PARTIAL, judged by the published reference verifier) in all clients; a .NET client (Aere.Cloud, .NET 8+); Java 1.6.1 on the Java 8 API (Android transport, redirects not followed). Go's Verify takes the envelope's bytes (json.RawMessage): a Go map would sort the keys and change the statement's hash.

This commit is contained in:
Aere Network 2026-09-29 15:45:17 +03:00
parent 983f93aa66
commit a27c3e79ae
23 changed files with 852 additions and 53 deletions

View File

@ -16,8 +16,9 @@ pip install "git+https://git.aere.network/aere-network/aere-cloud-sdk.git#subdir
go get git.aere.network/aere-network/aere-cloud-sdk/go@main # Go
```
Java: build `java/` from source with the JDK alone (see `java/README.md`). All four clients have the same routes and
semantics, and none of them follows redirects, so your API key is never sent to a host you did not name.
Java: build `java/` from source with the JDK alone (see `java/README.md`); since 1.6.1 the same class is built against the Java 8 API, for Android too (not yet run on a device). .NET: build `csharp/Aere.Cloud` from source with the
.NET SDK (see `csharp/README.md`). All five clients have the same routes and semantics, and none of them follows redirects, so your
API key is never sent to a host you did not name.
Get a key: the free trial is self-serve at [aere.network/cloud.html#subscribe](https://aere.network/cloud.html#subscribe).
A key looks like `ak2800.<your-0x-address>.<secret>`.
@ -65,6 +66,10 @@ const fin = await aere.pqFinality('latest', 'testnet', 'both'); // AIP-21: { fin
// internal interface; the gateway builds M' = 0x00 || len(ctx) || ctx || message for you)
const v = await aere.pqVerify({ scheme: 'ml-dsa-44', interface: 'external', publicKey, signature, message });
// Trust API (1.6.0): any AERE Proof Protocol (AIP-23) envelope -> VALID / INVALID / PARTIAL, judged by the published reference
// verifier, with the levels it checked and the command that reproduces the verdict. Served by cloud.aere.network since 2026-09-29.
const r = await aere.verify(envelope, { chain: 2800 }); // { verdict, levels: [{ level, state, detail }], verifier, reproduce }
// transfer history for an address (token from genesis, native from launch)
const tx = await aere.transfers('0xYourAddress', 50);
@ -74,7 +79,7 @@ const acct = await aere.sponsorCreatePqAccount('0x09' + falconPkHex, 0);
```
A Python mirror of this client lives in `python/` (installed from this repository, see Install): same
routes, same semantics, snake_case names. `go/` and `java/` carry the Go and Java clients.
routes, same semantics, snake_case names. `go/`, `java/` and `csharp/` carry the Go, Java and .NET clients.
Every method throws `AereCloudError` on a non-2xx response, carrying `.status` and the server's
JSON `.body` so you can act on the exact error.

2
csharp/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
bin/
obj/

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Aere.Cloud\Aere.Cloud.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,182 @@
// Probele clientului .NET (Aere.Cloud 1.6.0), fara framework de probe: fiecare proba tipareste "--- PASS: Nume" sau "--- FAIL: Nume: motiv",
// iar codul de iesire e numarul de probe picate (0 = toate verzi). Aceleasi garzi ca la clientul Go: cheia obligatorie, cheia trimisa NUMAI
// unde trebuie, redirectarile NEurmate (dovedit pe un server local real, fiindca numai handlerul implicit al SDK-ului o poate arata),
// exportul jurnalului verificat pe octetii primiti, verificarea unei copii pastrate (tinuta / stearsa / eroare), webhook-ul in timp constant.
// Controlul negativ (control-negativ.sh) strica fiecare paznic intr-o COPIE a sursei si cere ca proba lui sa pice.
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Aere.Cloud;
const string Cheie = "ak2800.0x1111111111111111111111111111111111111111.secretproba";
int picate = 0, trecute = 0;
async Task Proba(string nume, Func<Task> f)
{
try { await f(); Console.WriteLine("--- PASS: " + nume); trecute++; }
catch (Exception e) { Console.WriteLine("--- FAIL: " + nume + ": " + e.Message.Split('\n')[0]); picate++; }
}
void Cer(bool c, string ce) { if (!c) throw new Exception("cerut: " + ce); }
string Sha(byte[] b) => "0x" + Convert.ToHexString(SHA256.HashData(b)).ToLowerInvariant();
(AereCloudClient, Ciot) Client(Func<HttpRequestMessage, HttpResponseMessage>? r = null)
{
var c = new Ciot { Raspunde = r ?? (_ => Json(200, "{}")) };
return (new AereCloudClient(Cheie, "https://ciot.invalid/v1", c), c);
}
HttpResponseMessage Json(int s, string corp) => new((HttpStatusCode)s) { Content = new StringContent(corp, Encoding.UTF8, "application/json") };
await Proba("CheiaEObligatorie", () =>
{
bool a = false, b = false;
try { _ = new AereCloudClient(""); } catch (ArgumentException) { a = true; }
try { _ = new AereCloudClient(null!); } catch (ArgumentException) { b = true; }
Cer(a && b, "o cheie goala sau lipsa e refuzata la constructie");
return Task.CompletedTask;
});
await Proba("ReadinessCheiaNumaiLaAttest", async () =>
{
var (c, ciot) = Client();
await c.ReadinessAsync("exemplu.test");
Cer(!ciot.Ultima.Antete.ContainsKey("x-api-key"), "fara attest cheia NU pleaca");
await c.ReadinessAsync("exemplu.test", attest: true);
Cer(ciot.Ultima.Antete.GetValueOrDefault("x-api-key") == Cheie, "cu attest cheia pleaca");
Cer(ciot.Ultima.Corp.Contains("\"attest\":true"), "corpul poarta attest");
await c.ReadinessReportAsync("exemplu.test"); Cer(!ciot.Ultima.Antete.ContainsKey("x-api-key"), "raportul public fara cheie");
await c.PqFinalityAsync(); Cer(!ciot.Ultima.Antete.ContainsKey("x-api-key"), "finalitatea AIP-21 fara cheie");
await c.HealthAsync(); Cer(!ciot.Ultima.Antete.ContainsKey("x-api-key"), "health fara cheie");
await c.AccountAsync(); Cer(ciot.Ultima.Antete.GetValueOrDefault("x-api-key") == Cheie, "account cu cheie");
Cer(ciot.Ultima.Antete.GetValueOrDefault("user-agent", "").StartsWith("aere-cloud-csharp/1.6.0"), "user-agent-ul clientului");
});
await Proba("PerimetruFinalitateDovada", async () =>
{
var (c, ciot) = Client();
await c.ReadinessPerimeterAsync("exemplu.test", new PerimeterOptions(new[] { "www", "api" }, false, true));
using (var d = JsonDocument.Parse(ciot.Ultima.Corp))
{
var r = d.RootElement;
Cer(r.GetProperty("hosts").GetArrayLength() == 2 && !r.GetProperty("discover").GetBoolean() && r.GetProperty("attest").GetBoolean(), "corpul perimetrului");
}
Cer(ciot.Ultima.Antete.ContainsKey("x-api-key"), "perimetrul cere cheia");
await c.FinalityAsync(); Cer(ciot.Ultima.Url == "https://ciot.invalid/v1/data/finality" && ciot.Ultima.Metoda == "GET", "GET /data/finality");
await c.ProofAsync("0xab/c"); Cer(ciot.Ultima.Url.EndsWith("/proof/0xab%2Fc"), "hash-ul scapat in cale: " + ciot.Ultima.Url);
});
await Proba("TrustApiVerify", async () =>
{
var (c, ciot) = Client(_ => Json(200, "{\"verdict\":\"VALID\"}"));
var r = await c.VerifyAsync(new Dictionary<string, object> { ["kind"] = "data" }, 28001);
Cer(ciot.Ultima.Metoda == "POST" && ciot.Ultima.Url.EndsWith("/verify?chain=28001"), "POST /verify?chain=28001");
Cer(ciot.Ultima.Corp == "{\"kind\":\"data\"}", "plicul trimis ca atare: " + ciot.Ultima.Corp);
Cer(r.GetProperty("verdict").GetString() == "VALID", "verdictul intors");
await c.VerifyAsync(new Dictionary<string, object> { ["kind"] = "data" });
Cer(ciot.Ultima.Url.EndsWith("/verify"), "fara lant: /verify");
});
await Proba("ExportulZilei", async () =>
{
var corp = Encoding.UTF8.GetBytes("{\"a\":1}\n{\"b\":2}\n");
string? digest = Sha(corp); string format = "jsonl";
var (c, _) = Client(_ =>
{
var m = new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(corp) };
if (digest is not null) m.Headers.Add("x-aere-digest", digest);
m.Headers.Add("x-aere-day", "2026-09-27"); m.Headers.Add("x-aere-entries", "2");
return m;
});
var e = await c.AuditExportAsync("2026-09-27", format);
Cer(e.Verified == true && e.Entries == 2 && e.Day == "2026-09-27", "octetii se potrivesc cu amprenta declarata");
digest = "0x" + new string('0', 64);
e = await c.AuditExportAsync("2026-09-27", format); Cer(e.Verified == false, "o amprenta care nu e a octetilor: NEverificat");
digest = null;
e = await c.AuditExportAsync("2026-09-27", format); Cer(e.Verified == false, "fara amprenta: NEverificat");
e = await c.AuditExportAsync("2026-09-27", "cef"); Cer(e.Verified is null, "cef: nimic de verificat aici");
});
await Proba("ExportulPastrat", async () =>
{
var tinut = Encoding.UTF8.GetBytes("{\"x\":1}\n");
int stare = 200; string piatra = "";
var (c, _) = Client(_ => stare switch
{
200 => new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(tinut) },
410 => Json(410, piatra),
_ => Json(stare, "{\"error\":\"too many requests\"}"),
});
var k = await c.AuditVerifyKeptAsync("2026-09-01", tinut); Cer(k.State == "held" && k.Matches, "copia pastrata = ziua tinuta");
k = await c.AuditVerifyKeptAsync("2026-09-01", Encoding.UTF8.GetBytes("altceva")); Cer(k.State == "held" && !k.Matches, "o copie alterata NU se potriveste");
stare = 410; piatra = "{\"expired\":{\"digest\":\"" + Sha(tinut) + "\",\"deletedAt\":\"2026-09-20T00:00:00Z\"}}";
k = await c.AuditVerifyKeptAsync("2026-09-01", tinut); Cer(k.State == "expired" && k.Matches && k.DeletedAt == "2026-09-20T00:00:00Z", "ziua stearsa: piatra de mormant");
k = await c.AuditVerifyKeptAsync("2026-09-01", Encoding.UTF8.GetBytes("altceva")); Cer(k.State == "expired" && !k.Matches, "piatra de mormant nu se potriveste cu o copie alterata");
stare = 429; bool eroare = false;
try { await c.AuditVerifyKeptAsync("2026-09-01", tinut); } catch (AereCloudException x) { eroare = x.Status == 429; }
Cer(eroare, "un 429 e o EROARE, niciodata 'nu se potriveste'");
});
await Proba("RedirectareaNuEUrmata", async () =>
{
int Port() { var l = new TcpListener(IPAddress.Loopback, 0); l.Start(); var p = ((IPEndPoint)l.LocalEndpoint).Port; l.Stop(); return p; }
int pa = Port(), pb = Port(); int loviturib = 0; string? cheiaLaB = null;
using var a = new HttpListener(); a.Prefixes.Add($"http://127.0.0.1:{pa}/"); a.Start();
using var b = new HttpListener(); b.Prefixes.Add($"http://127.0.0.1:{pb}/"); b.Start();
var ta = Task.Run(async () => { while (a.IsListening) { HttpListenerContext x; try { x = await a.GetContextAsync(); } catch { break; } x.Response.StatusCode = 302; x.Response.RedirectLocation = $"http://127.0.0.1:{pb}/fura"; x.Response.Close(); } });
var tb = Task.Run(async () => { while (b.IsListening) { HttpListenerContext x; try { x = await b.GetContextAsync(); } catch { break; } loviturib++; cheiaLaB = x.Request.Headers["x-api-key"]; x.Response.StatusCode = 200; x.Response.Close(); } });
using var c = new AereCloudClient(Cheie, $"http://127.0.0.1:{pa}/v1"); // handlerul IMPLICIT al SDK-ului
int status = -1;
try { await c.AccountAsync(); } catch (AereCloudException x) { status = x.Status; }
await Task.Delay(200);
a.Stop(); b.Stop();
Cer(status == 302, "un 3xx e un raspuns (AereCloudException 302), nu un drum; primit " + status);
Cer(loviturib == 0 && cheiaLaB is null, "gazda din Location NU e contactata (lovituri " + loviturib + ")");
});
await Proba("VerifyWebhook", () =>
{
var corp = Encoding.UTF8.GetBytes("{\"event\":\"anchor\",\"height\":1}");
var sig = Convert.ToHexString(HMACSHA256.HashData(Encoding.UTF8.GetBytes("secret"), corp)).ToLowerInvariant();
Cer(AereCloudClient.VerifyWebhook(corp, sig, "secret"), "semnatura buna trece");
var rea = sig[..^1] + (sig[^1] == '0' ? '1' : '0');
Cer(!AereCloudClient.VerifyWebhook(corp, rea, "secret"), "o cifra schimbata e refuzata");
Cer(!AereCloudClient.VerifyWebhook(corp, sig[..10], "secret"), "o semnatura scurta e refuzata");
Cer(!AereCloudClient.VerifyWebhook(corp, null, "secret"), "fara antet: refuzat");
Cer(!AereCloudClient.VerifyWebhook(corp, sig, "alt-secret"), "alt secret: refuzat");
return Task.CompletedTask;
});
await Proba("EroriSiBlockNumber", async () =>
{
var (c, _) = Client(r => r.RequestUri!.AbsolutePath.EndsWith("/rpc") ? Json(200, "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x10\"}") : Json(500, "boom"));
Cer(await c.BlockNumberAsync() == 16, "eth_blockNumber 0x10 = 16");
AereCloudException? e = null;
try { await c.DataHeadAsync(); } catch (AereCloudException x) { e = x; }
Cer(e is { Status: 500, Body: "boom", Path: "/data/head" }, "un 500 poarta statusul, corpul si calea");
var (c2, _) = Client(_ => throw new HttpRequestException("retea cazuta"));
AereCloudException? e2 = null;
try { await c2.HealthAsync(); } catch (AereCloudException x) { e2 = x; }
Cer(e2 is { Status: 0 } && !e2.Message.Contains(Cheie), "fara raspuns: status 0, iar mesajul nu poarta cheia");
var (c3, _) = Client(_ => Json(200, "{\"jsonrpc\":\"2.0\",\"id\":1,\"error\":{\"code\":-1}}"));
bool refuz = false; try { await c3.BlockNumberAsync(); } catch (AereCloudException x) { refuz = x.Status == 200; }
Cer(refuz, "un rezultat fara forma 0x e o eroare, nu un zero");
});
Console.WriteLine($"probe: {trecute} trecute, {picate} picate");
return picate;
sealed class Ciot : HttpMessageHandler
{
public sealed record Cerere(string Metoda, string Url, Dictionary<string, string> Antete, string Corp);
public List<Cerere> Cereri { get; } = new();
public Cerere Ultima => Cereri[^1];
public Func<HttpRequestMessage, HttpResponseMessage> Raspunde { get; set; } = _ => new HttpResponseMessage(HttpStatusCode.OK);
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage r, CancellationToken ct)
{
var corp = r.Content is null ? "" : await r.Content.ReadAsStringAsync(ct);
var h = r.Headers.ToDictionary(x => x.Key.ToLowerInvariant(), x => string.Join(" ", x.Value));
Cereri.Add(new Cerere(r.Method.Method, r.RequestUri!.ToString(), h, corp));
return Raspunde(r);
}
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RootNamespace>Aere.Cloud</RootNamespace>
<AssemblyName>Aere.Cloud</AssemblyName>
<PackageId>Aere.Cloud</PackageId>
<Version>1.6.0</Version>
<Authors>Aere Network</Authors>
<Description>Official .NET client for the Aere Cloud API (Aere Network, chain 2800): RPC, post-quantum verification, the Trust API (AERE Proof Protocol envelopes), chain data, notarization and proofs, quantum readiness, audit log, webhooks and gas sponsorship.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://aere.network</PackageProjectUrl>
<Deterministic>true</Deterministic>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,257 @@
// Aere.Cloud - the official .NET client for the Aere Cloud API (Aere Network, chain 2800).
//
// Same routes and semantics as @aere/cloud (JS), aere-cloud (Python), aere-cloud (Java) and the Go client, 1.6.0. No dependency beyond
// the .NET base library. JSON routes return the server's JSON as a JsonElement, so the integrator binds it to whatever types they
// already use; the two routes that return FILES (the audit export and the check of a kept export) return typed results with the
// digest computed locally from the bytes received. Non-2xx answers are thrown as AereCloudException (HTTP status, server body, path).
//
// Nota interna: comentariile de implementare sunt in romana fara diacritice, ca in celelalte SDK-uri ale casei; documentatia publica
// (comentariile XML ale membrilor publici) e in engleza.
using System.Net;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace Aere.Cloud;
/// <summary>A non-2xx answer from Aere Cloud. <see cref="Status"/> 0 means the request never got an answer.</summary>
public sealed class AereCloudException : Exception
{
/// <summary>HTTP status (0 when there was no answer).</summary>
public int Status { get; }
/// <summary>The server's body, as text.</summary>
public string Body { get; }
/// <summary>The API path that failed.</summary>
public string Path { get; }
/// <summary>Creates the exception.</summary>
public AereCloudException(int status, string body, string path)
: base($"Aere Cloud {status} on {path}: {(string.IsNullOrEmpty(body) ? "request failed" : body)}")
{
Status = status; Body = body; Path = path;
}
}
/// <summary>Options for <see cref="AereCloudClient.ReadinessPerimeterAsync"/>.</summary>
public sealed record PerimeterOptions(IReadOnlyList<string>? Hosts = null, bool? Discover = null, bool Attest = false);
/// <summary>An audit day as a file. For jsonl, <see cref="Verified"/> says whether the sha256 of the bytes RECEIVED equals the stated
/// digest (computed here, not taken on trust); for cef it is null, because the digest is the one of the source file.</summary>
public sealed record AuditExport(string? Day, string Format, string? Digest, byte[] Bytes, int Entries, bool? Verified)
{
/// <summary>The bytes as UTF-8 text.</summary>
public string Text => Encoding.UTF8.GetString(Bytes);
}
/// <summary>Does an export you KEPT still match the day we hold ("held"), or the tombstone a deleted day left ("expired")?</summary>
public sealed record KeptCheck(string Day, string State, string Digest, string Local, bool Matches, string? DeletedAt);
/// <summary>One Aere Cloud gateway with one API key (ak2800.&lt;address&gt;.&lt;secret&gt;).</summary>
public sealed class AereCloudClient : IDisposable
{
/// <summary>Version of this client, sent in the User-Agent (the edge refuses generic library agents on every route).</summary>
public const string Version = "1.6.0";
/// <summary>The production gateway.</summary>
public const string DefaultBaseUrl = "https://cloud.aere.network/v1";
private readonly string _apiKey;
private readonly string _baseUrl;
private readonly HttpClient _http;
/// <summary>A client for <paramref name="baseUrl"/> (default: production). The API key is required. A test may pass its own
/// <paramref name="handler"/>; the default one does NOT follow redirects, so the key never travels to a host named in a Location.</summary>
public AereCloudClient(string apiKey, string? baseUrl = null, HttpMessageHandler? handler = null)
{
if (string.IsNullOrEmpty(apiKey)) throw new ArgumentException("Aere.Cloud: apiKey is required (ak2800.<address>.<secret>)", nameof(apiKey));
_apiKey = apiKey;
_baseUrl = (baseUrl ?? DefaultBaseUrl).TrimEnd('/');
// fara redirectari urmate: un 3xx e un raspuns (AereCloudException cu statusul lui), nu un drum spre alta gazda care ar primi cheia
_http = new HttpClient(handler ?? new HttpClientHandler { AllowAutoRedirect = false }) { Timeout = TimeSpan.FromSeconds(30) };
}
/// <inheritdoc/>
public void Dispose() => _http.Dispose();
private sealed record Raspuns(int Status, byte[] Body, Func<string, string?> Antet);
private async Task<Raspuns> DoAsync(HttpMethod method, string path, object? body, bool auth, CancellationToken ct)
{
using var req = new HttpRequestMessage(method, _baseUrl + path);
req.Headers.UserAgent.ParseAdd("aere-cloud-csharp/" + Version);
if (auth) req.Headers.Add("x-api-key", _apiKey);
if (body is not null)
{
req.Content = new ByteArrayContent(JsonSerializer.SerializeToUtf8Bytes(body));
req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
}
HttpResponseMessage r;
try { r = await _http.SendAsync(req, ct).ConfigureAwait(false); }
catch (HttpRequestException e) { throw new AereCloudException(0, e.Message, path); } // mesajul de retea nu poarta cheia (e in antet)
using (r)
{
var b = await r.Content.ReadAsByteArrayAsync(ct).ConfigureAwait(false);
var h = r.Headers; var hc = r.Content.Headers;
string? Antet(string n) => h.TryGetValues(n, out var v) ? v.FirstOrDefault() : hc.TryGetValues(n, out var w) ? w.FirstOrDefault() : null;
return new Raspuns((int)r.StatusCode, b, Antet);
}
}
private async Task<JsonElement> JsonAsync(HttpMethod method, string path, object? body, bool auth, CancellationToken ct)
{
var r = await DoAsync(method, path, body, auth, ct).ConfigureAwait(false);
if (r.Status < 200 || r.Status >= 300) throw new AereCloudException(r.Status, Encoding.UTF8.GetString(r.Body), path);
using var d = JsonDocument.Parse(r.Body.Length == 0 ? "null"u8.ToArray() : r.Body);
return d.RootElement.Clone();
}
private static string Esc(string s) => Uri.EscapeDataString(s);
private static string Sha256Hex(byte[] b) => "0x" + Convert.ToHexString(SHA256.HashData(b)).ToLowerInvariant();
// ---- health, account, JSON-RPC ----
/// <summary>GET /health (no key).</summary>
public Task<JsonElement> HealthAsync(CancellationToken ct = default) => JsonAsync(HttpMethod.Get, "/health", null, false, ct);
/// <summary>GET /account.</summary>
public Task<JsonElement> AccountAsync(CancellationToken ct = default) => JsonAsync(HttpMethod.Get, "/account", null, true, ct);
/// <summary>One JSON-RPC call; returns the whole JSON-RPC envelope.</summary>
public Task<JsonElement> RpcAsync(string method, IReadOnlyList<object?>? parameters = null, CancellationToken ct = default) =>
JsonAsync(HttpMethod.Post, "/rpc", new Dictionary<string, object?> { ["jsonrpc"] = "2.0", ["id"] = 1, ["method"] = method, ["params"] = parameters ?? Array.Empty<object?>() }, true, ct);
/// <summary>eth_blockNumber as an integer.</summary>
public async Task<ulong> BlockNumberAsync(CancellationToken ct = default)
{
var e = await RpcAsync("eth_blockNumber", null, ct).ConfigureAwait(false);
if (e.ValueKind != JsonValueKind.Object || !e.TryGetProperty("result", out var r) || r.GetString() is not { } s || !s.StartsWith("0x", StringComparison.Ordinal))
throw new AereCloudException(200, e.GetRawText(), "/rpc");
return Convert.ToUInt64(s[2..], 16);
}
// ---- post-quantum verification, the Trust API, chain data, notarization, proofs ----
/// <summary>POST /pq/verify, payload like {"scheme":"ml-dsa-44","publicKey":"0x..","signature":"0x..","message":"0x.."}.</summary>
public Task<JsonElement> PqVerifyAsync(object payload, CancellationToken ct = default) => JsonAsync(HttpMethod.Post, "/pq/verify", payload, true, ct);
/// <summary>The Trust API (1.6.0): one AERE Proof Protocol (AIP-23) envelope of any kind -> VALID / INVALID / PARTIAL with the levels
/// checked and the command that reproduces the verdict. <paramref name="chain"/>: 2800 or 28001; 0 = the chain the envelope declares.</summary>
public Task<JsonElement> VerifyAsync(object envelope, ulong chain = 0, CancellationToken ct = default) =>
JsonAsync(HttpMethod.Post, "/verify" + (chain > 0 ? "?chain=" + chain : ""), envelope, true, ct);
/// <summary>GET /data/head.</summary>
public Task<JsonElement> DataHeadAsync(CancellationToken ct = default) => JsonAsync(HttpMethod.Get, "/data/head", null, true, ct);
/// <summary>GET /data/validators.</summary>
public Task<JsonElement> ValidatorsAsync(CancellationToken ct = default) => JsonAsync(HttpMethod.Get, "/data/validators", null, true, ct);
/// <summary>GET /data/anchors?limit=.</summary>
public Task<JsonElement> AnchorsAsync(int limit = 10, CancellationToken ct = default) => JsonAsync(HttpMethod.Get, "/data/anchors?limit=" + limit, null, true, ct);
/// <summary>GET /data/anchors/{height}.</summary>
public Task<JsonElement> AnchorAsync(ulong height, CancellationToken ct = default) => JsonAsync(HttpMethod.Get, "/data/anchors/" + height, null, true, ct);
/// <summary>The post-quantum finality point (finalized/safe = the parent of the last certified anchor).</summary>
public Task<JsonElement> FinalityAsync(CancellationToken ct = default) => JsonAsync(HttpMethod.Get, "/data/finality", null, true, ct);
/// <summary>GET /data/transfers?address=&amp;limit=.</summary>
public Task<JsonElement> TransfersAsync(string address, int limit = 50, CancellationToken ct = default) =>
JsonAsync(HttpMethod.Get, "/data/transfers?address=" + Esc(address) + "&limit=" + limit, null, true, ct);
/// <summary>Notarizes a 32-byte digest (0x + 64 hex); the gas is ours.</summary>
public Task<JsonElement> NotarizeAsync(string hash, CancellationToken ct = default) => JsonAsync(HttpMethod.Post, "/notarize", new Dictionary<string, string> { ["hash"] = hash }, true, ct);
/// <summary>GET /notarize/{hash}.</summary>
public Task<JsonElement> ProofOfAsync(string hash, CancellationToken ct = default) => JsonAsync(HttpMethod.Get, "/notarize/" + Esc(hash), null, true, ct);
/// <summary>The proof with its post-quantum finality (first appearance, covering anchor, verdict, verification steps).</summary>
public Task<JsonElement> ProofAsync(string hash, CancellationToken ct = default) => JsonAsync(HttpMethod.Get, "/proof/" + Esc(hash), null, true, ct);
// ---- quantum readiness ----
/// <summary>Scans a domain's public TLS edge. Free WITHOUT a key; <paramref name="attest"/> notarizes the report digest and only then sends the key.</summary>
public Task<JsonElement> ReadinessAsync(string domain, bool fresh = false, bool attest = false, CancellationToken ct = default)
{
var b = new Dictionary<string, object> { ["domain"] = domain };
if (fresh) b["fresh"] = true;
if (attest) b["attest"] = true;
return JsonAsync(HttpMethod.Post, "/pq/readiness", b, attest, ct);
}
/// <summary>GET /pq/readiness/{domain} (no key).</summary>
public Task<JsonElement> ReadinessReportAsync(string domain, CancellationToken ct = default) => JsonAsync(HttpMethod.Get, "/pq/readiness/" + Esc(domain), null, false, ct);
/// <summary>The whole public perimeter of a domain in one keyed report; call again while summary.complete is false.</summary>
public Task<JsonElement> ReadinessPerimeterAsync(string domain, PerimeterOptions? o = null, CancellationToken ct = default)
{
o ??= new PerimeterOptions();
var b = new Dictionary<string, object> { ["domain"] = domain };
if (o.Hosts is not null) b["hosts"] = o.Hosts;
if (o.Discover == false) b["discover"] = false;
if (o.Attest) b["attest"] = true;
return JsonAsync(HttpMethod.Post, "/pq/readiness/perimeter", b, true, ct);
}
/// <summary>AIP-21 per-block post-quantum finality. client: "besu", "nethermind" or "both" (with `agreement`). No key.</summary>
public Task<JsonElement> PqFinalityAsync(string block = "latest", string network = "testnet", string client = "besu", CancellationToken ct = default) =>
JsonAsync(HttpMethod.Get, "/pq/finality/" + Esc(block) + "?network=" + Esc(network) + "&client=" + Esc(client), null, false, ct);
// ---- audit log ----
/// <summary>The paged JSON view of one UTC day (null = today), with the digest of the day file.</summary>
public Task<JsonElement> AuditLogAsync(string? day = null, int offset = 0, int limit = 1000, CancellationToken ct = default) =>
JsonAsync(HttpMethod.Get, "/account/audit?offset=" + offset + "&limit=" + limit + (day is null ? "" : "&day=" + Esc(day)), null, true, ct);
/// <summary>A day as a file: "jsonl" (the exact bytes of the day file, checked here against the stated digest) or "cef".</summary>
public async Task<AuditExport> AuditExportAsync(string? day = null, string format = "jsonl", CancellationToken ct = default)
{
var r = await DoAsync(HttpMethod.Get, "/account/audit?format=" + Esc(format) + (day is null ? "" : "&day=" + Esc(day)), null, true, ct).ConfigureAwait(false);
if (r.Status < 200 || r.Status >= 300) throw new AereCloudException(r.Status, Encoding.UTF8.GetString(r.Body), "/account/audit");
var digest = r.Antet("x-aere-digest");
bool? verified = format == "jsonl" ? !string.IsNullOrEmpty(digest) && digest == Sha256Hex(r.Body) : null;
return new AuditExport(r.Antet("x-aere-day"), format, digest, r.Body, int.TryParse(r.Antet("x-aere-entries"), out var n) ? n : 0, verified);
}
/// <summary>The retention of the audit log, READ-ONLY with the key: retention is SET with a wallet signature in the console, never
/// with the key, so a stolen key cannot shorten the trail of its own use.</summary>
public Task<JsonElement> AuditRetentionAsync(CancellationToken ct = default) => JsonAsync(HttpMethod.Get, "/account/audit/retention", null, true, ct);
/// <summary>Hashes YOUR bytes locally and compares them with the day we hold, or with the tombstone of a deleted day (410). Any other
/// failure is thrown, never reported as "does not match".</summary>
public async Task<KeptCheck> AuditVerifyKeptAsync(string day, byte[] kept, CancellationToken ct = default)
{
var local = Sha256Hex(kept);
var r = await DoAsync(HttpMethod.Get, "/account/audit?format=jsonl&day=" + Esc(day), null, true, ct).ConfigureAwait(false);
if (r.Status == 410)
{
try
{
using var d = JsonDocument.Parse(r.Body);
if (d.RootElement.TryGetProperty("expired", out var t) && t.ValueKind == JsonValueKind.Object
&& t.TryGetProperty("digest", out var dg) && dg.GetString() is { Length: > 0 } digest)
{
var del = t.TryGetProperty("deletedAt", out var da) ? da.GetString() : null;
return new KeptCheck(day, "expired", digest, local, local == digest, del);
}
}
catch (JsonException) { /* un 410 fara piatra de mormant cade mai jos, ca eroare */ }
}
if (r.Status < 200 || r.Status >= 300) throw new AereCloudException(r.Status, Encoding.UTF8.GetString(r.Body), "/account/audit");
var held = Sha256Hex(r.Body);
return new KeptCheck(day, "held", held, local, local == held, null);
}
// ---- webhooks and gas sponsorship ----
/// <summary>GET /webhooks.</summary>
public Task<JsonElement> ListWebhooksAsync(CancellationToken ct = default) => JsonAsync(HttpMethod.Get, "/webhooks", null, true, ct);
/// <summary>POST /webhooks.</summary>
public Task<JsonElement> CreateWebhookAsync(object spec, CancellationToken ct = default) => JsonAsync(HttpMethod.Post, "/webhooks", spec, true, ct);
/// <summary>DELETE /webhooks/{id}.</summary>
public Task<JsonElement> DeleteWebhookAsync(string id, CancellationToken ct = default) => JsonAsync(HttpMethod.Delete, "/webhooks/" + Esc(id), null, true, ct);
/// <summary>GET /sponsor/health.</summary>
public Task<JsonElement> SponsorHealthAsync(CancellationToken ct = default) => JsonAsync(HttpMethod.Get, "/sponsor/health", null, true, ct);
/// <summary>POST /sponsor/createAccount.</summary>
public Task<JsonElement> SponsorCreateAccountAsync(object body, CancellationToken ct = default) => JsonAsync(HttpMethod.Post, "/sponsor/createAccount", body, true, ct);
/// <summary>A smart account with a POST-QUANTUM owner (Falcon-512, 0x09-prefixed, 897 bytes), gas sponsored; idempotent on (key, salt).</summary>
public Task<JsonElement> SponsorCreatePqAccountAsync(string falconPubKeyHex, ulong salt = 0, CancellationToken ct = default) =>
JsonAsync(HttpMethod.Post, "/sponsor/createPqAccount", new Dictionary<string, object> { ["falconPubKey"] = falconPubKeyHex, ["salt"] = salt }, true, ct);
/// <summary>POST /sponsor/execute.</summary>
public Task<JsonElement> SponsorExecuteAsync(object body, CancellationToken ct = default) => JsonAsync(HttpMethod.Post, "/sponsor/execute", body, true, ct);
/// <summary>Checks a webhook delivery's HMAC-SHA256 signature in constant time. <paramref name="rawBody"/> must be the EXACT bytes
/// received, not re-serialized JSON. Never act on a webhook without this.</summary>
public static bool VerifyWebhook(byte[] rawBody, string? signatureHeader, string secret)
{
var want = Encoding.ASCII.GetBytes(Convert.ToHexString(HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), rawBody)).ToLowerInvariant());
var got = Encoding.ASCII.GetBytes(signatureHeader ?? "");
// FixedTimeEquals intoarce fals pe lungimi diferite fara sa scurteze comparatia pe continut
return CryptographicOperations.FixedTimeEquals(want, got);
}
}

49
csharp/README.md Normal file
View File

@ -0,0 +1,49 @@
# Aere.Cloud (.NET)
The official .NET client for the [Aere Cloud API](https://aere.network/cloud.html) on Aere Network (chain 2800).
**No dependencies** beyond the .NET base library (`System.Net.Http`, `System.Text.Json`, `System.Security.Cryptography`).
Targets .NET 8 (LTS) and runs on later versions. Same routes and semantics as the JS, Python, Go and Java clients 1.6.0,
including `VerifyAsync`, the Trust API.
JSON routes return the server's JSON as a `JsonElement`, so you bind it to the types you already use. The two routes
that return files (the audit export and the check of a kept export) return typed results, with the SHA-256 computed
locally from the bytes received.
## Use
```csharp
using Aere.Cloud;
using var c = new AereCloudClient(Environment.GetEnvironmentVariable("AERE_KEY")!);
var head = await c.DataHeadAsync(); // {"block":...,"chainId":2800,...}
ulong n = await c.BlockNumberAsync();
// the Trust API: any AERE Proof Protocol (AIP-23) envelope -> VALID / INVALID / PARTIAL, with the levels checked
var verdict = await c.VerifyAsync(JsonDocument.Parse(File.ReadAllText("envelope.json")).RootElement); // chain: 2800 or 28001, default = the one the envelope declares
// pass the parsed JsonElement (key order kept): the verdict hashes the statement in its own key order
// quantum-readiness scan of a domain's TLS edge: free, no key is sent unless you attest the report
var report = await c.ReadinessAsync("example.com");
// the audit log of a day as a file; Verified is true only if the sha256 of the bytes you received equals the digest
AuditExport day = await c.AuditExportAsync("2026-09-24");
// a kept export still matches the day we hold, or the tombstone a deleted day left behind
KeptCheck k = await c.AuditVerifyKeptAsync("2026-05-01", keptBytes);
// verify a webhook delivery (constant-time HMAC-SHA256 over the exact bytes received)
bool ok = AereCloudClient.VerifyWebhook(rawBody, request.Headers["x-aere-signature"], secret);
```
Non-2xx answers are thrown as `AereCloudException` with `Status`, `Body` and `Path` (`Status` 0 = no answer; the
message never carries your key). The client does not follow redirects: a 3xx is returned as an answer, so your API key
is never sent to a host you did not name.
## Tests
```
dotnet run --project Aere.Cloud.Tests -c Release # offline: a recording HTTP handler, plus a real local server for the redirect test
bash control-negativ.sh # breaks ten guards in a copy of the source; each one's test must fail
```
Measured 2026-09-28 (WSL, .NET SDK 10.0.302): 9 tests green; the negative control 10 of 10 red, the untouched source green.

55
csharp/control-negativ.sh Normal file
View File

@ -0,0 +1,55 @@
#!/bin/bash
# control-negativ.sh - IN WSL: controlul negativ al probelor clientului .NET (Aere.Cloud). Fiecare paznic se strica intr-o COPIE a sursei
# (depozitul nu se atinge), probele ruleaza pe copie si proba tinta TREBUIE sa pice. O plantare a carei ancora nu apare exact o data, sau
# care nu compileaza (TreatWarningsAsErrors), e STRICAT, nu rosu. La urma, sursa neatinsa trebuie sa treaca intreaga.
# bash csharp/control-negativ.sh (Linux sau WSL, cu un SDK .NET 8+ in PATH; `dotnet` gasit altundeva: DOTNET_ROOT + PATH)
# 2026-09-29: caile sunt derivate (dosarul scriptului, un dosar temporar), nu scrise fix, ca scriptul sa mearga si din depozitul public.
set -u
export DOTNET_CLI_TELEMETRY_OPTOUT=1 DOTNET_NOLOGO=1
command -v dotnet >/dev/null || { echo "STRICAT: dotnet nu e in PATH"; exit 2; }
S=$(cd "$(dirname "$0")" && pwd)
W=$(mktemp -d "${TMPDIR:-/tmp}/sdk-csharp-control-XXXX")
cp -r "$S/Aere.Cloud" "$S/Aere.Cloud.Tests" "$W/"; rm -rf "$W"/*/bin "$W"/*/obj
SRC="$W/Aere.Cloud/AereCloudClient.cs"; tr -d '\r' < "$S/Aere.Cloud/AereCloudClient.cs" > "$W/original.cs"
cat > "$W/plantari.py" <<'PY'
import sys
P = [
("exportul jsonl declarat verificat fara hash", 'format == "jsonl" ? !string.IsNullOrEmpty(digest) && digest == Sha256Hex(r.Body) : null', 'format == "jsonl" ? true : null', "ExportulZilei"),
("ziua tinuta acceptata fara comparatie", 'new KeptCheck(day, "held", held, local, local == held, null)', 'new KeptCheck(day, "held", held, local, true, null)', "ExportulPastrat"),
("piatra zilei sterse acceptata fara comparatie", 'new KeptCheck(day, "expired", digest, local, local == digest, del)', 'new KeptCheck(day, "expired", digest, local, true, del)', "ExportulPastrat"),
# inaintea throw-ului pentru non-2xx (prima forma a plantarii statea dupa el, deci nu se executa niciodata si "a ramas verde")
("un 429 intors ca nepotrivire", ' if (r.Status < 200 || r.Status >= 300) throw new AereCloudException(r.Status, Encoding.UTF8.GetString(r.Body), "/account/audit");\n var held', ' if (r.Status == 429) return new KeptCheck(day, "held", "", local, false, null);\n if (r.Status < 200 || r.Status >= 300) throw new AereCloudException(r.Status, Encoding.UTF8.GetString(r.Body), "/account/audit");\n var held', "ExportulPastrat"),
("cheia trimisa si fara attest", 'return JsonAsync(HttpMethod.Post, "/pq/readiness", b, attest, ct);', 'return JsonAsync(HttpMethod.Post, "/pq/readiness", b, true, ct);', "ReadinessCheiaNumaiLaAttest"),
("redirectarile urmate (cheia ar pleca spre alta gazda)", 'new HttpClientHandler { AllowAutoRedirect = false }', 'new HttpClientHandler { AllowAutoRedirect = true }', "RedirectareaNuEUrmata"),
("webhook acceptat fara semnatura", 'return CryptographicOperations.FixedTimeEquals(want, got);', 'return got.Length >= 0 || CryptographicOperations.FixedTimeEquals(want, got);', "VerifyWebhook"),
("cheia goala acceptata", 'if (string.IsNullOrEmpty(apiKey)) throw', 'if (apiKey == "niciodata-" + "asa") throw', "CheiaEObligatorie"),
("lantul cerut scapat din Trust API", '(chain > 0 ? "?chain=" + chain : "")', '(chain > 0 ? "" : "")', "TrustApiVerify"),
("un rezultat RPC fara forma citit ca zero", ' throw new AereCloudException(200, e.GetRawText(), "/rpc");', ' return 0;', "EroriSiBlockNumber"),
]
if sys.argv[1] == "n": print(len(P)); sys.exit(0)
i = int(sys.argv[1]); nume, vechi, nou, tinta = P[i]
t = open(sys.argv[2]).read()
if t.count(vechi) != 1: print("ANCORA %d" % t.count(vechi)); sys.exit(3)
open(sys.argv[3], "w").write(t.replace(vechi, nou)); print(nume + "|" + tinta)
PY
ruleaza() { # -> iesirea probelor in $W/iesire.txt; cod: 0 rulat, 2 nu compileaza
( cd "$W" && dotnet build Aere.Cloud.Tests -c Release -p:NuGetAudit=false -nodeReuse:false -p:UseSharedCompilation=false > build.log 2>&1 < /dev/null ) || return 2
# exact UN dll de probe in iesirea acestei constructii (un `| head -1` peste mai multe ar alege la intamplare)
local dll; dll=$(ls "$W"/Aere.Cloud.Tests/bin/Release/net*/Aere.Cloud.Tests.dll 2>/dev/null); [ "$(printf '%s\n' "$dll" | grep -c .)" = 1 ] || return 2
dotnet "$dll" > "$W/iesire.txt" 2>&1 < /dev/null; return 0
}
N=$(python3 "$W/plantari.py" n); RAU=0
for i in $(seq 0 $((N - 1))); do
L=$(python3 "$W/plantari.py" "$i" "$W/original.cs" "$SRC"); c=$?
[ "$c" = 0 ] || { echo " STRICAT plantarea $i: $L"; RAU=$((RAU+1)); continue; }
nume=${L%|*}; tinta=${L#*|}
if ! ruleaza; then echo " STRICAT $nume: nu compileaza ($(grep -m1 -oE 'error [A-Z]+[0-9]+[^[]*' "$W/build.log"))"; RAU=$((RAU+1)); continue; fi
if grep -q "^--- FAIL: $tinta" "$W/iesire.txt"; then echo " ROSU cum trebuia $nume (pica $(grep -c '^--- FAIL' "$W/iesire.txt"): $(grep '^--- FAIL' "$W/iesire.txt" | cut -d: -f2 | tr '\n' ' '))"
else echo " CONTROL CAZUT $nume: $tinta a ramas verde"; RAU=$((RAU+1)); fi
done
cp "$W/original.cs" "$SRC"
if ruleaza && ! grep -q '^--- FAIL' "$W/iesire.txt" && grep -q '^--- PASS' "$W/iesire.txt"; then echo " martor: sursa neatinsa, $(grep -c '^--- PASS' "$W/iesire.txt") probe verzi"
else echo " MARTORUL nu e verde"; RAU=$((RAU+1)); fi
[ "$RAU" = 0 ] && echo "CONTROL NEGATIV TRECUT ($N din $N rosii, martorul verde)" || echo "CONTROL NEGATIV: $RAU probleme"
rm -rf "$W"
exit $([ "$RAU" = 0 ] && echo 0 || echo 1)

View File

@ -26,6 +26,10 @@ day, err := c.AuditExport(ctx, "2026-09-24", "jsonl")
// a kept export still matches the day we hold, or the tombstone a deleted day left behind
k, err := c.AuditVerifyKept(ctx, "2026-05-01", keptBytes)
// Trust API (1.6.0): any AERE Proof Protocol (AIP-23) envelope -> VALID / INVALID / PARTIAL; chain 0 = the one the envelope declares.
// Pass the envelope's bytes as received (os.ReadFile): a Go map would sort the keys and change the statement's hash.
verdict, err := c.Verify(ctx, json.RawMessage(envelopeBytes), 2800)
// verify a webhook delivery (constant-time HMAC-SHA256 over the exact bytes received)
ok := aerecloud.VerifyWebhook(rawBody, r.Header.Get("x-aere-signature"), secret)
```

View File

@ -30,7 +30,7 @@ import (
)
// Version of this client, sent in the User-Agent (the edge refuses generic library agents on every route).
const Version = "1.5.2"
const Version = "1.6.0"
// DefaultBaseURL is the production gateway.
const DefaultBaseURL = "https://cloud.aere.network/v1"
@ -162,6 +162,18 @@ func (c *Client) BlockNumber(ctx context.Context) (uint64, error) {
func (c *Client) PQVerify(ctx context.Context, payload any) (json.RawMessage, error) {
return c.jsonReq(ctx, "POST", "/pq/verify", payload, true)
}
// Verify is the Trust API (1.6.0): one AERE Proof Protocol (AIP-23) envelope of any kind -> VALID / INVALID / PARTIAL with the levels checked and the command that reproduces the verdict; the judgement is the published reference verifier. Needs a gateway that serves POST /v1/verify.
// chain: 2800 or 28001; 0 = the chain the envelope declares.
// envelope: the envelope's JSON bytes as you received them. The verdict hashes the statement in its own key order, so the bytes are
// sent as they are; a Go map would sort the keys and turn a valid envelope into an INVALID one (measured 2026-09-29, before 1.6.0 shipped).
func (c *Client) Verify(ctx context.Context, envelope json.RawMessage, chain uint64) (json.RawMessage, error) {
p := "/verify"
if chain > 0 {
p += "?chain=" + strconv.FormatUint(chain, 10)
}
return c.jsonReq(ctx, "POST", p, envelope, true)
}
func (c *Client) DataHead(ctx context.Context) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/data/head", nil, true)
}

View File

@ -114,6 +114,14 @@ func TestPerimetruFinalitateDovada(t *testing.T) {
if v = s.ultima(); v.Path != "/v1/proof/0xab" || v.Key == "" {
t.Fatalf("dovada: %+v", v)
}
c.Verify(ctx, json.RawMessage(`{"statementHash":"0x00"}`), 28001)
if v = s.ultima(); v.Method != "POST" || v.Path != "/v1/verify?chain=28001" || v.Key == "" || v.Body != `{"statementHash":"0x00"}` {
t.Fatalf("verify: %+v", v)
}
c.Verify(ctx, json.RawMessage(`{}`), 0)
if v = s.ultima(); v.Path != "/v1/verify" {
t.Fatalf("verify fara lant: %+v", v)
}
}
var zi = []byte("{\"t\":\"2026-05-01T10:00:00.000Z\",\"m\":\"GET\",\"p\":\"/v1/account\",\"s\":200}\n")
@ -190,6 +198,25 @@ func TestRedirectareaNuEUrmata(t *testing.T) {
}
}
// 2026-09-29: verdictul Trust API hashuieste statement-ul in ordinea LUI de chei; clientul trimite octetii plicului asa cum sunt.
// Perechea negativa: acelasi plic trecut printr-un map de Go iese cu cheile sortate, adica alt text canonic.
func TestVerifyPastreazaOrdinea(t *testing.T) {
s, c := porneste(t)
plic := `{"v":1,"statement":{"v":1,"kind":"aere-proof-of-data","subject":{"name":"x"},"createdAt":"2026-09-26T00:00:00Z"},"statementHash":"0x00"}`
if _, err := c.Verify(context.Background(), json.RawMessage(plic), 0); err != nil {
t.Fatal(err)
}
if v := s.ultima(); v.Body != plic {
t.Fatalf("ordinea cheilor schimbata: %s", v.Body)
}
var m map[string]any
_ = json.Unmarshal([]byte(plic), &m)
prin, _ := json.Marshal(m)
if string(prin) == plic {
t.Fatal("controlul probei: un map ar fi trebuit sa sorteze cheile")
}
}
func TestVerifyWebhook(t *testing.T) {
secret := "AERE-SINTETIC-secret"
corp := []byte(`{"event":"pq-anchor","data":{"height":42}}`)

View File

@ -21,6 +21,9 @@ const PLANTARI = [
['redirectarile urmate (cheia ar pleca spre alta gazda)', 'CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }', 'CheckRedirect: nil', 'TestRedirectareaNuEUrmata'],
['un 429 intors ca nepotrivire', 'return nil, &Error{Status: r.status, Body: r.body, Path: "/account/audit"}\n\t}\n\td := sha256Hex(r.body)',
'return &KeptCheck{Day: day, State: "held", Local: local}, nil\n\t}\n\td := sha256Hex(r.body)', 'TestExportulPastrat'],
// 2026-09-29: plicul trecut printr-un map de Go (cheile sortate = alt text canonic, un plic valid ar iesi INVALID la Trust API)
['Verify trece plicul printr-un map', 'return c.jsonReq(ctx, "POST", p, envelope, true)',
'var m map[string]any\n\t_ = json.Unmarshal(envelope, &m)\n\treturn c.jsonReq(ctx, "POST", p, m, true)', 'TestVerifyPastreazaOrdinea'],
];
function ruleaza(src) {

View File

@ -65,6 +65,16 @@ export class AereCloud {
*/
pqVerify(p) { return this._req('POST', '/pq/verify', { body: p }); }
/**
* the Trust API (1.6.0): one AERE Proof Protocol (AIP-23) envelope of any kind -> VALID / INVALID / PARTIAL with the levels checked and the command that reproduces the verdict; the judgement is the published reference verifier. Needs a gateway that serves POST /v1/verify.
* @param {object} envelope the envelope as produced (statement, statementHash, signatures, notarization...)
* @param {{chain?: number}} [opts] 2800 or 28001; omitted = the chain the envelope declares
* @returns {Promise<{verdict:'VALID'|'INVALID'|'PARTIAL', levels:object[], verifier:object, reproduce:string}>}
*/
verify(envelope, { chain } = {}) {
return this._req('POST', '/verify' + (chain ? `?chain=${encodeURIComponent(chain)}` : ''), { body: envelope });
}
// ---- chain data ----
dataHead() { return this._req('GET', '/data/head'); }
validators() { return this._req('GET', '/data/validators'); }

View File

@ -2,8 +2,10 @@
The official Java client for the [Aere Cloud API](https://aere.network/cloud.html) — one keyed
endpoint for RPC, post-quantum signature verification, chain data, notarization, webhooks and
gas sponsorship on Aere Network (chain 2800). **Zero dependencies** beyond the JDK (11+):
`java.net.http.HttpClient` and `javax.crypto` only.
gas sponsorship on Aere Network (chain 2800). **Zero dependencies** beyond the standard library:
`java.net.HttpURLConnection` and `javax.crypto` only. Since 1.6.1 the class uses only the Java 8 API (the tests enforce it with
`javac --release 8`), which is what Android provides; it has not yet been run on an Android device (see below). It does not follow redirects: a 3xx is raised as an
error, so your API key is never sent to a host you did not name.
Mirror of `@aere/cloud` (JS) and `aere-cloud` (Python): same routes, same semantics. Bodies are
raw JSON strings — the client does not force a JSON library on the integrator; parse responses
@ -23,6 +25,9 @@ long n = aere.blockNumber();
// a smart account with a POST-QUANTUM owner (Falcon-512), gas sponsored:
String acct = aere.sponsorCreatePqAccount("0x09" + falconPkHex, 0);
// Trust API (1.6.0): any AERE Proof Protocol (AIP-23) envelope (JSON text) -> VALID / INVALID / PARTIAL with the levels checked
String verdict = aere.verify(envelopeJson, 2800);
// verify a webhook delivery (constant-time HMAC) — never trust one without this:
boolean ok = AereCloud.verifyWebhook(rawBodyBytes, request.header("x-aere-signature"), secret);
```
@ -48,9 +53,16 @@ AereCloud.KeptCheck k = aere.auditVerifyKept("2026-05-01", keptBytes);
`auditVerifyKept` hashes YOUR bytes locally. An error (rate limit, network) is raised, never returned as "does not match".
## Android
Copy `src/main/java/network/aere/cloud/AereCloud.java` into your app module (package `network.aere.cloud`) and call it off the main
thread, as with any network call on Android. What is measured: the class compiles against the Java 8 API (`javac --release 8`) and its
tests pass on the JDK. What is not: we have not run it on an Android device or emulator.
## Tests
```
javac --release 8 -d out8 src/main/java/network/aere/cloud/AereCloud.java # the Android gate: Java 8 API only
javac -d out src/main/java/network/aere/cloud/AereCloud.java src/test/java/network/aere/cloud/TestAereCloud.java
java -cp out network.aere.cloud.TestAereCloud # unit, offline
AERE_KEY=ak2800... java -cp out network.aere.cloud.TestAereCloud --live # + live probes
@ -59,6 +71,10 @@ AERE_KEY=ak2800... java -cp out network.aere.cloud.TestAereCloud --live # + li
Measured 2026-08-27: 7/7 green, including live probes against production (health, validator
set, block number, and the free JSON refusal on the PQ sponsorship route).
Measured 2026-09-28 (1.6.1, JDK 21): 9/9 offline, including a 302 to a second local host that receives zero requests; the negative
control (`node control-negativ.mjs`) turns 8 of 8 planted faults red, among them a followed redirect and two post-Java-8 calls that
the JDK tests alone would not catch but `--release 8` refuses.
Measured 2026-09-25 (1.5.0): 7/7 offline, the new routes judged against a local JDK HTTP server (what the client sends,
what it does with each answer). `node control-negativ.mjs` breaks five guards in a copy of the class and requires each
one's test to fail: 5/5 red, the untouched class green.

View File

@ -1,5 +1,5 @@
#!/usr/bin/env node
// Controlul negativ al probelor clientului Java 1.5.0: fiecare paznic se strica intr-o COPIE a clasei (sursa din depozit nu se
// Controlul negativ al probelor clientului Java (1.5.0; 1.6.1 adauga redirectarea si poarta --release 8): fiecare paznic se strica intr-o COPIE a clasei (sursa din depozit nu se
// atinge), probele se ruleaza pe copie si proba tinta TREBUIE sa pice; o plantare care nu se aplica sau nu compileaza e un ESEC al
// controlului (STRICAT), nu un rosu. La urma, clasa neatinsa trebuie sa treaca intreaga.
// node aerenew/sdk/java/control-negativ.mjs
@ -25,7 +25,24 @@ const PLANTARI = [
'return req("POST", "/pq/readiness", json(m), true);', 'readiness fara attest'],
['un 429 intors ca nepotrivire', 'if (r.status < 200 || r.status >= 300) throw new AereCloudException(r.status, r.text(), "/account/audit");\n String dig = sha256Hex(r.octeti);',
'if (r.status < 200 || r.status >= 300) return new KeptCheck(day, "held", null, local, false, null);\n String dig = sha256Hex(r.octeti);', 'auditVerifyKept'],
// 1.6.1: HttpURLConnection urmeaza redirectarile implicit; fara oprire, cheia pleaca spre gazda din Location
['redirectarea urmata (cheia pleaca spre alta gazda)', 'c.setInstanceFollowRedirects(false);', 'c.setInstanceFollowRedirects(true);', '302'],
];
// 1.6.1: poarta Android = clasa compileaza cu `javac --release 8`. Un API de dupa Java 8 trece probele pe JDK 21 si trebuie prins
// numai de poarta; plantarea NU e o proba, e o compilare care TREBUIE sa cada.
const PLANTARI_JAVA8 = [
['InputStream.readAllBytes (Java 9)', 'while ((n = in.read(buf)) != -1) b.write(buf, 0, n);\n return b.toByteArray();', 'return in.readAllBytes();'],
['URLEncoder.encode cu Charset (Java 10)', 'try { return URLEncoder.encode(s, "UTF-8"); }', 'try { if (s == null) throw new java.io.UnsupportedEncodingException(); return URLEncoder.encode(s, StandardCharsets.UTF_8); }'],
];
function compileazaJava8(sursa) {
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-java8-'));
try {
fs.writeFileSync(path.join(d, 'AereCloud.java'), sursa);
execFileSync('javac', ['--release', '8', '-Xlint:-options', '-d', path.join(d, 'out'), path.join(d, 'AereCloud.java')], { stdio: 'pipe' });
return { ok: true };
} catch (e) { return { ok: false, err: String(e.stderr || e.message).split('\n').find((l) => /error/.test(l)) || 'eroare' }; }
finally { fs.rmSync(d, { recursive: true, force: true }); }
}
function ruleaza(sursa) {
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-java-cn-'));
@ -50,8 +67,16 @@ for (const [nume, vechi, nou, tinta] of PLANTARI) {
if (r.picate.some((l) => l.includes(tinta))) console.log(` ROSU cum trebuia ${nume} (${r.esecuri} esec, intre ele proba '${tinta}')`);
else { console.log(` CONTROL CAZUT ${nume}: proba '${tinta}' a ramas verde`); rele++; }
}
for (const [nume, vechi, nou] of PLANTARI_JAVA8) {
if (original.split(vechi).length !== 2) { console.log(` STRICAT ${nume}: ancora apare de ${original.split(vechi).length - 1} ori`); rele++; continue; }
const r = compileazaJava8(original.replace(vechi, nou));
if (!r.ok) console.log(` ROSU cum trebuia ${nume}: --release 8 refuza (${r.err.trim().slice(0, 90)})`);
else { console.log(` CONTROL CAZUT ${nume}: --release 8 a compilat`); rele++; }
}
const j8 = compileazaJava8(original);
if (!j8.ok) { console.log(' MARTORUL nu compileaza cu --release 8: ' + j8.err); rele++; } else console.log(' martor: sursa neatinsa compileaza cu --release 8 (API Java 8, deci si Android)');
const martor = ruleaza(original);
if (martor.stricat || martor.esecuri !== 0) { console.log(' MARTORUL nu e verde: ' + JSON.stringify(martor)); rele++; }
else console.log(` martor: sursa neatinsa ${martor.treceri}/${martor.treceri} verde`);
console.log(rele ? `CONTROL NEGATIV: ${rele} probleme` : `CONTROL NEGATIV TRECUT (${PLANTARI.length} din ${PLANTARI.length} rosii, martorul verde)`);
console.log(rele ? `CONTROL NEGATIV: ${rele} probleme` : `CONTROL NEGATIV TRECUT (${PLANTARI.length + PLANTARI_JAVA8.length} din ${PLANTARI.length + PLANTARI_JAVA8.length} rosii, martorul verde si pe --release 8)`);
process.exitCode = rele ? 1 : 0;

View File

@ -1,13 +1,15 @@
package network.aere.cloud;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.LinkedHashMap;
import javax.crypto.Mac;
@ -18,19 +20,24 @@ import javax.crypto.spec.SecretKeySpec;
*
* Oglinda fidela a @aere/cloud (sdk/index.mjs) si a aere-cloud (sdk/python): fiecare ruta a
* gateway-ului e o metoda; erorile non-2xx se ridica drept {@link AereCloudException} cu codul
* HTTP si corpul serverului. Zero dependinte in afara JDK (11+): HttpClient, Mac/HMAC.
* HTTP si corpul serverului. Zero dependinte in afara bibliotecii standard: HttpURLConnection, Mac/HMAC.
* Din 1.6.1 foloseste numai API-ul Java 8 (poarta: `javac --release 8`), cel pe care il are Android; nerulat inca pe un dispozitiv.
*
* Corpurile sunt JSON brut (String): clientul NU impune o biblioteca JSON integratorului.
* Metodele primesc/intorc String-uri JSON; integratorul le parseaza cu ce foloseste deja.
*/
public final class AereCloud {
// 1.5.0 (2026-09-25): paritate cu @aere/cloud si aere-cloud (Python) 1.5.0: proof, readiness*, pqFinality, audit*
public static final String VERSIUNE = "1.5.0";
// 1.6.0 (2026-09-28): verify() (Trust API, POST /v1/verify); VERSIUNE ramasese la 1.5.0 si in 1.5.1/1.5.2, adusa la zi cu celelalte trei
// 1.6.1 (2026-09-28): transportul pe HttpURLConnection in loc de java.net.http (care NU exista pe Android), fara redirectari
// urmate (ca inainte: HttpClient nu le urma implicit); URLEncoder cu numele setului de caractere (varianta cu Charset e
// Java 10+). Aceeasi suprafata publica.
public static final String VERSIUNE = "1.6.1";
private static final String BAZA_IMPLICITA = "https://cloud.aere.network/v1";
private static final int TIMP_MS = 30000;
private final String apiKey;
private final String baseUrl;
private final HttpClient http;
/** Raspuns non-2xx de la Aere Cloud: poarta status, corpul serverului si calea. */
public static final class AereCloudException extends RuntimeException {
@ -53,30 +60,57 @@ public final class AereCloud {
throw new IllegalArgumentException("AereCloud: apiKey is required (ak2800.<address>.<secret>)");
this.apiKey = apiKey;
this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
this.http = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build();
}
private String req(String metoda, String cale, String corpJson, boolean auth) {
Raspuns r = trimite(metoda, cale, corpJson == null ? null : corpJson.getBytes(StandardCharsets.UTF_8), auth, true);
if (r.status < 200 || r.status >= 300) throw new AereCloudException(r.status, r.text(), cale);
return r.text();
}
/**
* Singurul drum spre retea. Redirectarile NU se urmeaza (un 3xx e un raspuns, deci o eroare pentru apelant): cheia API nu
* pleaca niciodata spre o gazda pe care integratorul nu a numit-o. Un raspuns de eroare isi citeste corpul din fluxul de
* eroare; un raspuns fara corp da zero octeti, nu null.
*/
private Raspuns trimite(String metoda, String cale, byte[] corp, boolean auth, boolean json) {
HttpURLConnection c = null;
try {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(baseUrl + cale))
.timeout(Duration.ofSeconds(30))
// User-Agent explicit: marginea raspunde 403 la agentii generici de
// biblioteca pe orice ruta. Lectie masurata la SDK-ul Python, 2026-08-27.
.header("user-agent", "aere-cloud-java/" + VERSIUNE)
.header("content-type", "application/json");
if (auth) b.header("x-api-key", apiKey);
if (corpJson == null) b.method(metoda, HttpRequest.BodyPublishers.noBody());
else b.method(metoda, HttpRequest.BodyPublishers.ofString(corpJson, StandardCharsets.UTF_8));
HttpResponse<String> r = http.send(b.build(), HttpResponse.BodyHandlers.ofString());
if (r.statusCode() < 200 || r.statusCode() >= 300)
throw new AereCloudException(r.statusCode(), r.body(), cale);
return r.body();
} catch (IOException | InterruptedException e) {
if (e instanceof InterruptedException) Thread.currentThread().interrupt();
c = (HttpURLConnection) URI.create(baseUrl + cale).toURL().openConnection();
c.setInstanceFollowRedirects(false);
c.setConnectTimeout(TIMP_MS);
c.setReadTimeout(TIMP_MS);
c.setRequestMethod(metoda);
// User-Agent explicit: marginea raspunde 403 la agentii generici de
// biblioteca pe orice ruta. Lectie masurata la SDK-ul Python, 2026-08-27.
c.setRequestProperty("user-agent", "aere-cloud-java/" + VERSIUNE);
if (json) c.setRequestProperty("content-type", "application/json");
if (auth) c.setRequestProperty("x-api-key", apiKey);
if (corp != null) {
c.setDoOutput(true);
c.setFixedLengthStreamingMode(corp.length);
OutputStream o = c.getOutputStream();
try { o.write(corp); } finally { o.close(); }
}
int status = c.getResponseCode();
InputStream in = status >= 400 ? c.getErrorStream() : c.getInputStream();
return new Raspuns(status, citesteTot(in), c.getHeaderFields());
} catch (IOException e) {
throw new AereCloudException(0, e.getMessage(), cale);
}
}
private static byte[] citesteTot(InputStream in) throws IOException {
if (in == null) return new byte[0];
try {
ByteArrayOutputStream b = new ByteArrayOutputStream();
byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) != -1) b.write(buf, 0, n);
return b.toByteArray();
} finally { in.close(); }
}
private static String esc(String s) {
StringBuilder sb = new StringBuilder();
for (char c : s.toCharArray()) {
@ -126,6 +160,9 @@ public final class AereCloud {
// ---- post-quantum verification ----
/** payloadJson: {"scheme":"ml-dsa-44","publicKey":"0x..","signature":"0x..","message":"0x.."} */
public String pqVerify(String payloadJson) { return req("POST", "/pq/verify", payloadJson, true); }
/** the Trust API (1.6.0): one AERE Proof Protocol (AIP-23) envelope of any kind -> VALID / INVALID / PARTIAL with the levels checked and the command that reproduces the verdict; the judgement is the published reference verifier. Needs a gateway that serves POST /v1/verify. chain: 2800 or 28001; 0 = the chain the envelope declares. */
public String verify(String envelopeJson, long chain) { return req("POST", "/verify" + (chain > 0 ? "?chain=" + chain : ""), envelopeJson, true); }
public String verify(String envelopeJson) { return verify(envelopeJson, 0); }
// ---- chain data ----
public String dataHead() { return req("GET", "/data/head", null, true); }
@ -248,25 +285,18 @@ public final class AereCloud {
// ---- cererea bruta: status, antete si octeti, pentru rutele care intorc fisiere ----
static final class Raspuns {
final int status; final byte[] octeti; final java.net.http.HttpHeaders antete;
Raspuns(int s, byte[] o, java.net.http.HttpHeaders a) { status = s; octeti = o; antete = a; }
String antet(String n) { return antete.firstValue(n).orElse(null); }
final int status; final byte[] octeti; final Map<String, List<String>> antete;
Raspuns(int s, byte[] o, Map<String, List<String>> a) { status = s; octeti = o; antete = a; }
/** Primul antet cu numele dat, fara sa conteze literele mari (HttpURLConnection pastreaza forma trimisa de server). */
String antet(String n) {
if (antete == null) return null;
for (Map.Entry<String, List<String>> e : antete.entrySet())
if (e.getKey() != null && e.getKey().equalsIgnoreCase(n) && e.getValue() != null && !e.getValue().isEmpty()) return e.getValue().get(0);
return null;
}
String text() { return new String(octeti, StandardCharsets.UTF_8); }
}
private Raspuns reqBrut(String metoda, String cale, boolean auth) {
try {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(baseUrl + cale))
.timeout(Duration.ofSeconds(30))
.header("user-agent", "aere-cloud-java/" + VERSIUNE);
if (auth) b.header("x-api-key", apiKey);
b.method(metoda, HttpRequest.BodyPublishers.noBody());
HttpResponse<byte[]> r = http.send(b.build(), HttpResponse.BodyHandlers.ofByteArray());
return new Raspuns(r.statusCode(), r.body(), r.headers());
} catch (IOException | InterruptedException e) {
if (e instanceof InterruptedException) Thread.currentThread().interrupt();
throw new AereCloudException(0, e.getMessage(), cale);
}
}
private Raspuns reqBrut(String metoda, String cale, boolean auth) { return trimite(metoda, cale, null, auth, false); }
static String sha256Hex(byte[] b) {
try {
byte[] h = MessageDigest.getInstance("SHA-256").digest(b);
@ -283,7 +313,10 @@ public final class AereCloud {
int j = json.indexOf('"', i + t.length());
return j < 0 ? null : json.substring(i + t.length(), j);
}
private static String enc(String s) { return java.net.URLEncoder.encode(s, StandardCharsets.UTF_8); }
private static String enc(String s) {
try { return URLEncoder.encode(s, "UTF-8"); }
catch (java.io.UnsupportedEncodingException e) { throw new IllegalStateException(e); } // UTF-8 exista pe orice JVM
}
// ---- webhooks ----
public String listWebhooks() { return req("GET", "/webhooks", null, true); }

View File

@ -98,7 +98,7 @@ public final class TestAereCloud {
lc.readiness("example.com", false, false);
String[] v = ultima.get();
cere(v[0].equals("POST") && v[1].equals("/v1/pq/readiness") && v[2] == null && v[3].equals("{\"domain\":\"example.com\"}"), String.join(" | ", v));
cere(v[4].equals("aere-cloud-java/1.5.0"), v[4]);
cere(v[4].equals("aere-cloud-java/" + AereCloud.VERSIUNE), v[4]); // versiunea din cod, nu un literal (1.6.0 l-ar fi rupt)
lc.readiness("example.com", true, true);
v = ultima.get();
cere("ak2800.0xAB.SINTETIC".equals(v[2]) && v[3].equals("{\"domain\":\"example.com\",\"fresh\":true,\"attest\":true}"), String.join(" | ", v));
@ -113,6 +113,13 @@ public final class TestAereCloud {
lc.proof("0xab");
cere(ultima.get()[1].equals("/v1/proof/0xab") && ultima.get()[2] != null, ultima.get()[1]);
});
proba("1.6.0: verify (Trust API) trimite plicul neschimbat pe POST /v1/verify, cu cheie si ?chain; fara lant, fara parametru", () -> {
lc.verify("{\"statementHash\":\"0x00\"}", 28001);
String[] v = ultima.get();
cere(v[0].equals("POST") && v[1].equals("/v1/verify?chain=28001") && v[2] != null && v[3].equals("{\"statementHash\":\"0x00\"}"), String.join(" | ", v));
lc.verify("{}");
cere(ultima.get()[1].equals("/v1/verify"), ultima.get()[1]);
});
proba("1.5.0: auditExport: jsonl verified=true pe octetii care dau digestul, FALSE pe alti octeti, null la cef; un 400 se ridica", () -> {
raspunsuri.put("/v1/account/audit", new Object[]{200, zi, new String[]{"x-aere-digest", dZi, "x-aere-day", "2026-05-01", "x-aere-entries", "1"}});
AereCloud.AuditExport e = lc.auditExport("2026-05-01", "jsonl");
@ -140,6 +147,20 @@ public final class TestAereCloud {
raspunsuri.put("/v1/account/audit", new Object[]{429, "{\"error\":\"rate_limit\"}".getBytes(StandardCharsets.UTF_8), new String[0]});
try { lc.auditVerifyKept("2026-05-01", zi); throw new AssertionError("429 intors ca nepotrivire"); } catch (AereCloud.AereCloudException x) { cere(x.status == 429, "status " + x.status); }
});
// 1.6.1: transportul e HttpURLConnection, care URMEAZA redirectarile implicit; clientul trebuie sa le opreasca, altfel
// cheia API pleaca spre gazda din Location. A doua gazda locala numara cererile: trebuie sa primeasca ZERO.
final int[] strainVazute = {0};
com.sun.net.httpserver.HttpServer strain = com.sun.net.httpserver.HttpServer.create(new java.net.InetSocketAddress("127.0.0.1", 0), 0);
strain.createContext("/", ex -> { synchronized (strainVazute) { strainVazute[0]++; } ex.sendResponseHeaders(200, -1); ex.close(); });
strain.start();
try {
proba("1.6.1: un 302 spre alta gazda NU se urmeaza: clientul ridica 302, gazda straina primeste zero cereri (cheia nu pleaca)", () -> {
raspunsuri.put("/v1/data/head", new Object[]{302, new byte[0], new String[]{"location", "http://127.0.0.1:" + strain.getAddress().getPort() + "/fura"}});
try { lc.dataHead(); throw new AssertionError("302 neridicat"); }
catch (AereCloud.AereCloudException x) { cere(x.status == 302, "status " + x.status); }
synchronized (strainVazute) { cere(strainVazute[0] == 0, "gazda straina a primit " + strainVazute[0] + " cereri"); }
});
} finally { strain.stop(0); }
} finally {
srv.stop(0);
}
@ -159,6 +180,20 @@ public final class TestAereCloud {
cere(e.body != null && e.body.contains("897"), "alt refuz: " + e.body);
}
});
// 2026-09-29: Trust API prin productie - plic minimal valid -> VALID, acelasi cu statement schimbat -> INVALID
proba("LIVE: verify - plic valid VALID, plic atins INVALID", () -> {
String st = "{\"v\":1,\"kind\":\"aere-proof-of-data\",\"subject\":{\"name\":\"example.csv\"},\"sha256\":\"0x" + "11".repeat(32) + "\",\"createdAt\":\"2026-09-26T00:00:00.000Z\"}";
String h;
try {
byte[] d = java.security.MessageDigest.getInstance("SHA-256").digest(st.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder("0x"); for (byte b : d) sb.append(String.format("%02x", b)); h = sb.toString();
} catch (Exception e) { throw new RuntimeException(e); }
String plic = "{\"v\":1,\"kind\":\"aere-proof-of-data-attestation\",\"statement\":" + st + ",\"statementHash\":\"" + h + "\"}";
String bun = c.verify(plic);
cere(bun.contains("\"verdict\":\"VALID\""), bun);
String rau = c.verify(plic.replace("0x" + "11".repeat(32), "0x" + "22".repeat(32)));
cere(rau.contains("\"verdict\":\"INVALID\""), rau);
});
} else if (live) {
System.out.println(" (sar LIVE: AERE_KEY lipseste)");
}

View File

@ -1,6 +1,6 @@
{
"name": "@aere/cloud",
"version": "1.5.2",
"version": "1.6.0",
"description": "Official client for the Aere Cloud API: keyed RPC, post-quantum verification, chain data, notarization, webhooks and gas sponsorship on Aere Network (chain 2800).",
"type": "module",
"main": "index.mjs",

View File

@ -38,6 +38,9 @@ out = aere.pq_verify({
# notarize a document hash — Aere pays the gas
receipt = aere.notarize("0x" + sha256_hex)
# Trust API (1.6.0): any AERE Proof Protocol (AIP-23) envelope -> VALID / INVALID / PARTIAL, with the levels checked
r = aere.verify(envelope, chain=2800) # {"verdict", "levels", "verifier", "reproduce"}
proof = aere.proof_of("0x" + sha256_hex)
# a smart account with a POST-QUANTUM owner (Falcon-512), gas sponsored:

View File

@ -14,7 +14,7 @@ import urllib.request
import urllib.error
from typing import Any, Optional
__version__ = "1.5.2"
__version__ = "1.6.0"
__all__ = ["AereCloud", "AereCloudError", "verify_webhook"]
_BAZA = "https://cloud.aere.network/v1"
@ -106,6 +106,11 @@ class AereCloud:
"""{scheme, publicKey, signature?, message?, signedMessage?} (0x-hex) -> {valid, ...}"""
return self._req("POST", "/pq/verify", payload)
def verify(self, envelope: dict, chain: "int | None" = None) -> dict:
"""the Trust API (1.6.0): one AERE Proof Protocol (AIP-23) envelope of any kind -> VALID / INVALID / PARTIAL with the levels checked and the command that reproduces the verdict; the judgement is the published reference verifier. Needs a gateway that serves POST /v1/verify.
chain: 2800 or 28001; None = the chain the envelope declares."""
return self._req("POST", "/verify" + (f"?chain={int(chain)}" if chain else ""), envelope)
# ---- chain data ----
def data_head(self) -> dict:
return self._req("GET", "/data/head")

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aere-cloud"
version = "1.5.2"
version = "1.6.0"
description = "Official Python client for the Aere Cloud API (Aere Network, chain 2800): RPC, post-quantum verification, chain data, notarization, webhooks, gas sponsorship."
readme = "README.md"
requires-python = ">=3.9"

View File

@ -81,6 +81,16 @@ def t_query():
assert tr.cereri[2].full_url.endswith("/data/anchors/15000000")
assert tr.cereri[3].full_url.endswith("/data/finality")
def t_verify():
tr = Transport([(200, {"verdict": "VALID", "levels": []}), (200, {"verdict": "PARTIAL", "levels": []})])
c = AereCloud("k", opener=tr)
assert c.verify({"statement": {}, "statementHash": "0x00"}, chain=28001)["verdict"] == "VALID"
assert c.verify({"statement": {}})["verdict"] == "PARTIAL"
assert tr.cereri[0].full_url.endswith("/verify?chain=28001") and tr.cereri[0].get_method() == "POST"
assert tr.cereri[1].full_url.endswith("/verify")
assert tr.cereri[0].get_header("X-api-key") == "k", "verify cere cheia"
assert json.loads(tr.cereri[0].data.decode()) == {"statement": {}, "statementHash": "0x00"}
def t_pq_sponsor():
tr = Transport([(200, {"txHash": "0xab", "account": "0xcd", "alreadyDeployed": False})])
c = AereCloud("k", opener=tr)
@ -103,6 +113,7 @@ proba("cheia merge in antet, health e fara cheie", t_antet)
proba("rpc impacheteaza corect si ridica eroarea RPC", t_rpc)
proba("un status de eroare devine AereCloudError cu corpul serverului", t_eroare_http)
proba("rutele de date construiesc corect query-ul", t_query)
proba("verify (Trust API, 1.6.0): ruta, ?chain, cheia si plicul neschimbat", t_verify)
proba("sponsor_create_pq_account: corp si cale corecte", t_pq_sponsor)
proba("verify_webhook: buna trece; stricata, prelungita, corp atins pica", t_webhook)
@ -240,5 +251,17 @@ if "--live" in sys.argv:
proba("LIVE: ancora cu >=6 sigilii", t_live_ancora)
proba("LIVE: sponsor PQ refuza JSON pe cheie scurta", lambda: _live_pq(c))
# 2026-09-29: Trust API prin productie - plic minimal valid -> VALID, acelasi cu statement schimbat -> INVALID
def t_live_verify():
st = {"v": 1, "kind": "aere-proof-of-data", "subject": {"name": "example.csv"}, "sha256": "0x" + "11" * 32, "createdAt": "2026-09-26T00:00:00.000Z"}
text = json.dumps(st, separators=(",", ":"), ensure_ascii=False)
plic = {"v": 1, "kind": "aere-proof-of-data-attestation", "statement": st, "statementHash": "0x" + hashlib.sha256(text.encode("utf-8")).hexdigest()}
bun = c.verify(plic)
assert bun["verdict"] == "VALID", bun
rau = c.verify(dict(plic, statement=dict(st, sha256="0x" + "22" * 32)))
assert rau["verdict"] == "INVALID", rau
assert any(l["level"] == "integrity" and l["state"] == "FAILED" for l in rau["levels"])
proba("LIVE: verify - plic valid VALID, plic atins INVALID", t_live_verify)
print(f"\n{trecute} treceri, {picate} esecuri")
sys.exit(1 if picate else 0)

View File

@ -65,11 +65,15 @@ await test('rutele de date construiesc corect query-ul', async () => {
const rute = {
'GET /data/anchors?limit=3': { status: 200, corp: { anchors: [] } },
'GET /data/finality': { status: 200, corp: { finalized: 1, anchor: 2 } },
'POST /verify?chain=28001': { status: 200, corp: { verdict: 'VALID', levels: [] } },
'POST /verify': { status: 200, corp: { verdict: 'INVALID', levels: [] } },
'GET /data/transfers?address=0xBeef&limit=25': { status: 200, corp: { tokenTransfers: [], nativeTransfers: [] } },
};
const c = new AereCloud({ apiKey: 'ak2800.0xAAA.AERE-SINTETIC-s', baseUrl: BAZA_TEST, fetch: fetchFals(rute) });
await c.anchors(3);
await c.finality();
assert.equal((await c.verify({ statement: {}, statementHash: '0x00' }, { chain: 28001 })).verdict, 'VALID');
assert.equal((await c.verify({ statement: {}, statementHash: '0x00' })).verdict, 'INVALID');
await c.transfers('0xBeef', 25);
// daca reperele nu s-ar potrivi, fetchFals ar da 404 si _req ar arunca
});
@ -194,6 +198,17 @@ if (process.env.AERE_CHEIE) {
const acc = await c.account();
assert.ok(acc.usageLast31Days);
});
// 2026-09-29: Trust API prin productie - un plic minimal valid -> VALID, acelasi cu statement schimbat -> INVALID (integritatea)
await test('LIVE: verify() - plic valid VALID, plic atins INVALID', async () => {
const c = new AereCloud({ apiKey: process.env.AERE_CHEIE });
const statement = { v: 1, kind: 'aere-proof-of-data', subject: { name: 'example.csv' }, sha256: '0x' + '11'.repeat(32), createdAt: '2026-09-26T00:00:00.000Z' };
const plic = { v: 1, kind: 'aere-proof-of-data-attestation', statement, statementHash: '0x' + crypto.createHash('sha256').update(JSON.stringify(statement), 'utf8').digest('hex') };
const bun = await c.verify(plic);
assert.equal(bun.verdict, 'VALID', JSON.stringify(bun).slice(0, 200));
const rau = await c.verify({ ...plic, statement: { ...statement, sha256: '0x' + '22'.repeat(32) } });
assert.equal(rau.verdict, 'INVALID');
assert.ok(rau.levels.some((l) => l.level === 'integrity' && l.state === 'FAILED'));
});
} else {
console.log(' (sar proba LIVE: AERE_CHEIE nu e in mediu)');
}