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:
parent
983f93aa66
commit
a27c3e79ae
11
README.md
11
README.md
@ -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
2
csharp/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
bin/
|
||||
obj/
|
||||
17
csharp/Aere.Cloud.Tests/Aere.Cloud.Tests.csproj
Normal file
17
csharp/Aere.Cloud.Tests/Aere.Cloud.Tests.csproj
Normal 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>
|
||||
182
csharp/Aere.Cloud.Tests/Program.cs
Normal file
182
csharp/Aere.Cloud.Tests/Program.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
21
csharp/Aere.Cloud/Aere.Cloud.csproj
Normal file
21
csharp/Aere.Cloud/Aere.Cloud.csproj
Normal 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>
|
||||
257
csharp/Aere.Cloud/AereCloudClient.cs
Normal file
257
csharp/Aere.Cloud/AereCloudClient.cs
Normal 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.<address>.<secret>).</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=&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
49
csharp/README.md
Normal 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
55
csharp/control-negativ.sh
Normal 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)
|
||||
@ -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)
|
||||
```
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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}}`)
|
||||
|
||||
@ -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) {
|
||||
|
||||
10
index.mjs
10
index.mjs
@ -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'); }
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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); }
|
||||
|
||||
@ -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)");
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)');
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user