aere-cloud Go 1.5.0: the Go client (standard library only), same surface as the JS client

Import path git.aere.network/aere-network/aere-cloud-sdk/go. JSON routes return json.RawMessage; the audit export and the check of a
kept export return typed results with the sha256 computed locally. Redirects are not followed. go test ./... runs offline against a
local httptest server; control-negativ.mjs breaks six guards in a copy of the package and requires each one's test to fail.
This commit is contained in:
Liviu 2026-09-25 10:45:59 +03:00
parent 84cc9493f2
commit ed88a38a4e
5 changed files with 658 additions and 0 deletions

43
go/README.md Normal file
View File

@ -0,0 +1,43 @@
# aere-cloud (Go)
The official Go client for the [Aere Cloud API](https://aere.network/cloud.html) on Aere Network (chain 2800).
**Zero dependencies** beyond the Go standard library. Same routes and semantics as the JS, Python and Java
clients 1.5.0.
JSON routes return the server's JSON as `json.RawMessage`, so you decode it into 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
```go
import aerecloud "git.aere.network/aere-network/aere-cloud-sdk/go"
c, err := aerecloud.New(os.Getenv("AERE_KEY"))
head, err := c.DataHead(ctx) // {"block":...,"chainId":2800,...}
n, err := c.BlockNumber(ctx)
// quantum-readiness scan of a domain's TLS edge: free, no key is sent unless you attest the report
report, err := c.Readiness(ctx, "example.com", false, false)
// the audit log of a day as a file; Verified is true only if the sha256 of the bytes you received equals the digest
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)
// verify a webhook delivery (constant-time HMAC-SHA256 over the exact bytes received)
ok := aerecloud.VerifyWebhook(rawBody, r.Header.Get("x-aere-signature"), secret)
```
Non-2xx answers come back as `*aerecloud.Error` with `Status`, `Body` and `Path`. 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
```
go test ./... # offline, against a local httptest server
node control-negativ.mjs # breaks six guards in a copy of the package; each one's test must fail
```
Measured 2026-09-25: 7 tests green; the negative control 6 of 6 red, the untouched package green.

353
go/aerecloud.go Normal file
View File

@ -0,0 +1,353 @@
// Package aerecloud is the official Go client for the Aere Cloud API (Aere Network, chain 2800).
//
// Same routes and semantics as @aere/cloud (JS), aere-cloud (Python) and aere-cloud (Java) 1.5.0. Zero dependencies
// beyond the Go standard library. JSON routes return the server's JSON as json.RawMessage, so the integrator decodes it
// into 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 returned as *Error with the HTTP status, the server's body and the path.
package aerecloud
// Nota interna: comentariile de implementare sunt in romana fara diacritice, ca in celelalte SDK-uri ale casei;
// documentatia publica a API-ului (comentariile exportate) e in engleza.
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
// Version of this client, sent in the User-Agent (the edge refuses generic library agents on every route).
const Version = "1.5.0"
// DefaultBaseURL is the production gateway.
const DefaultBaseURL = "https://cloud.aere.network/v1"
// Error is a non-2xx answer from Aere Cloud (Status 0 means the request never got an answer).
type Error struct {
Status int
Body []byte
Path string
}
func (e *Error) Error() string {
b := string(e.Body)
if b == "" {
b = "request failed"
}
return fmt.Sprintf("Aere Cloud %d on %s: %s", e.Status, e.Path, b)
}
// Client talks to one Aere Cloud gateway with one API key (ak2800.<address>.<secret>).
type Client struct {
apiKey string
baseURL string
http *http.Client
}
// New returns a client for the production gateway. The API key is required.
func New(apiKey string) (*Client, error) { return NewWithBase(apiKey, DefaultBaseURL, nil) }
// NewWithBase returns a client for another base URL (a test server) and an optional *http.Client.
func NewWithBase(apiKey, baseURL string, hc *http.Client) (*Client, error) {
if apiKey == "" {
return nil, errors.New("aerecloud: apiKey is required (ak2800.<address>.<secret>)")
}
if hc == nil {
// fara redirectari urmate: un 3xx e un raspuns, nu un drum spre alta gazda care ar primi cheia
hc = &http.Client{Timeout: 30 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
}
return &Client{apiKey: apiKey, baseURL: strings.TrimRight(baseURL, "/"), http: hc}, nil
}
type raspuns struct {
status int
body []byte
headers http.Header
}
func (c *Client) do(ctx context.Context, method, path string, body any, auth bool) (*raspuns, error) {
var rd io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return nil, err
}
rd = bytes.NewReader(b)
}
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, rd)
if err != nil {
return nil, err
}
req.Header.Set("user-agent", "aere-cloud-go/"+Version)
if body != nil {
req.Header.Set("content-type", "application/json")
}
if auth {
req.Header.Set("x-api-key", c.apiKey)
}
r, err := c.http.Do(req)
if err != nil {
// mesajul de retea nu poarta cheia (e in antet, nu in URL)
return nil, &Error{Status: 0, Body: []byte(err.Error()), Path: path}
}
defer r.Body.Close()
b, err := io.ReadAll(r.Body)
if err != nil {
return nil, &Error{Status: 0, Body: []byte(err.Error()), Path: path}
}
return &raspuns{status: r.StatusCode, body: b, headers: r.Header}, nil
}
func (c *Client) jsonReq(ctx context.Context, method, path string, body any, auth bool) (json.RawMessage, error) {
r, err := c.do(ctx, method, path, body, auth)
if err != nil {
return nil, err
}
if r.status < 200 || r.status >= 300 {
return nil, &Error{Status: r.status, Body: r.body, Path: path}
}
return json.RawMessage(r.body), nil
}
func esc(s string) string { return url.PathEscape(s) }
// ---- health, account, JSON-RPC ----
func (c *Client) Health(ctx context.Context) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/health", nil, false)
}
func (c *Client) Account(ctx context.Context) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/account", nil, true)
}
// RPC sends one JSON-RPC call and returns the whole JSON-RPC envelope.
func (c *Client) RPC(ctx context.Context, method string, params []any) (json.RawMessage, error) {
if params == nil {
params = []any{}
}
return c.jsonReq(ctx, "POST", "/rpc", map[string]any{"jsonrpc": "2.0", "id": 1, "method": method, "params": params}, true)
}
// BlockNumber is eth_blockNumber as an integer.
func (c *Client) BlockNumber(ctx context.Context) (uint64, error) {
b, err := c.RPC(ctx, "eth_blockNumber", nil)
if err != nil {
return 0, err
}
var env struct {
Result string `json:"result"`
}
if err := json.Unmarshal(b, &env); err != nil || !strings.HasPrefix(env.Result, "0x") {
return 0, &Error{Status: 200, Body: b, Path: "/rpc"}
}
return strconv.ParseUint(env.Result[2:], 16, 64)
}
// ---- post-quantum verification, chain data, notarization, proofs ----
// PQVerify: payload like {"scheme":"ml-dsa-44","publicKey":"0x..","signature":"0x..","message":"0x.."}.
func (c *Client) PQVerify(ctx context.Context, payload any) (json.RawMessage, error) {
return c.jsonReq(ctx, "POST", "/pq/verify", payload, true)
}
func (c *Client) DataHead(ctx context.Context) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/data/head", nil, true)
}
func (c *Client) Validators(ctx context.Context) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/data/validators", nil, true)
}
func (c *Client) Anchors(ctx context.Context, limit int) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/data/anchors?limit="+strconv.Itoa(limit), nil, true)
}
func (c *Client) Anchor(ctx context.Context, height uint64) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/data/anchors/"+strconv.FormatUint(height, 10), nil, true)
}
func (c *Client) Transfers(ctx context.Context, address string, limit int) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/data/transfers?address="+url.QueryEscape(address)+"&limit="+strconv.Itoa(limit), nil, true)
}
func (c *Client) Notarize(ctx context.Context, hash string) (json.RawMessage, error) {
return c.jsonReq(ctx, "POST", "/notarize", map[string]string{"hash": hash}, true)
}
func (c *Client) ProofOf(ctx context.Context, hash string) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/notarize/"+esc(hash), nil, true)
}
// Proof: the proof with its post-quantum finality (first appearance, covering anchor, verdict, verification steps).
func (c *Client) Proof(ctx context.Context, hash string) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/proof/"+esc(hash), nil, true)
}
// ---- quantum readiness ----
// Readiness scans a domain's public TLS edge. Free without a key; attest=true notarizes the report digest and sends the key.
func (c *Client) Readiness(ctx context.Context, domain string, fresh, attest bool) (json.RawMessage, error) {
b := map[string]any{"domain": domain}
if fresh {
b["fresh"] = true
}
if attest {
b["attest"] = true
}
return c.jsonReq(ctx, "POST", "/pq/readiness", b, attest)
}
func (c *Client) ReadinessReport(ctx context.Context, domain string) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/pq/readiness/"+esc(domain), nil, false)
}
// PerimeterOptions: Hosts may be nil; Discover nil keeps the server default; Attest notarizes only a COMPLETE report.
type PerimeterOptions struct {
Hosts []string
Discover *bool
Attest bool
}
// ReadinessPerimeter: the whole public perimeter of a domain in one keyed report; call again while summary.complete is false.
func (c *Client) ReadinessPerimeter(ctx context.Context, domain string, o PerimeterOptions) (json.RawMessage, error) {
b := map[string]any{"domain": domain}
if o.Hosts != nil {
b["hosts"] = o.Hosts
}
if o.Discover != nil && !*o.Discover {
b["discover"] = false
}
if o.Attest {
b["attest"] = true
}
return c.jsonReq(ctx, "POST", "/pq/readiness/perimeter", b, true)
}
// PQFinality: AIP-21 per-block post-quantum finality. client: "besu", "nethermind" or "both" (with `agreement`). No key.
func (c *Client) PQFinality(ctx context.Context, block, network, client string) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/pq/finality/"+esc(block)+"?network="+url.QueryEscape(network)+"&client="+url.QueryEscape(client), nil, false)
}
// ---- audit log ----
// AuditLog: the paged JSON view of one UTC day (day "" = today), with the digest of the day file.
func (c *Client) AuditLog(ctx context.Context, day string, offset, limit int) (json.RawMessage, error) {
q := "?offset=" + strconv.Itoa(offset) + "&limit=" + strconv.Itoa(limit)
if day != "" {
q += "&day=" + url.QueryEscape(day)
}
return c.jsonReq(ctx, "GET", "/account/audit"+q, nil, true)
}
// AuditExport is a day as a file. For jsonl, Verified reports whether the sha256 of the bytes RECEIVED equals the stated
// digest (computed here, not taken on trust); for cef it is nil, because the digest is the one of the source file.
type AuditExport struct {
Day, Format, Digest string
Bytes []byte
Entries int
Verified *bool
}
func sha256Hex(b []byte) string { h := sha256.Sum256(b); return "0x" + hex.EncodeToString(h[:]) }
func (c *Client) AuditExport(ctx context.Context, day, format string) (*AuditExport, error) {
if format == "" {
format = "jsonl"
}
p := "/account/audit?format=" + url.QueryEscape(format)
if day != "" {
p += "&day=" + url.QueryEscape(day)
}
r, err := c.do(ctx, "GET", p, nil, true)
if err != nil {
return nil, err
}
if r.status < 200 || r.status >= 300 {
return nil, &Error{Status: r.status, Body: r.body, Path: "/account/audit"}
}
e := &AuditExport{Day: r.headers.Get("x-aere-day"), Format: format, Digest: r.headers.Get("x-aere-digest"), Bytes: r.body}
e.Entries, _ = strconv.Atoi(r.headers.Get("x-aere-entries"))
if format == "jsonl" {
ok := e.Digest != "" && e.Digest == sha256Hex(r.body)
e.Verified = &ok
}
return e, nil
}
// AuditRetention reads the retention of the audit log with the API key (read-only). Retention is SET with a wallet
// signature in the console, never with the key: a stolen key must not be able to shorten the trail of its own use.
func (c *Client) AuditRetention(ctx context.Context) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/account/audit/retention", nil, true)
}
// KeptCheck: does an export you KEPT still match the day we hold ("held"), or the tombstone a deleted day left ("expired")?
type KeptCheck struct {
Day, State, Digest, Local, DeletedAt string
Matches bool
}
// AuditVerifyKept 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 returned as an error, never as "does not match".
func (c *Client) AuditVerifyKept(ctx context.Context, day string, kept []byte) (*KeptCheck, error) {
local := sha256Hex(kept)
r, err := c.do(ctx, "GET", "/account/audit?format=jsonl&day="+url.QueryEscape(day), nil, true)
if err != nil {
return nil, err
}
if r.status == 410 {
var t struct {
Expired *struct {
Digest string `json:"digest"`
DeletedAt string `json:"deletedAt"`
} `json:"expired"`
}
if json.Unmarshal(r.body, &t) == nil && t.Expired != nil && t.Expired.Digest != "" {
return &KeptCheck{Day: day, State: "expired", Digest: t.Expired.Digest, Local: local, Matches: local == t.Expired.Digest, DeletedAt: t.Expired.DeletedAt}, nil
}
}
if r.status < 200 || r.status >= 300 {
return nil, &Error{Status: r.status, Body: r.body, Path: "/account/audit"}
}
d := sha256Hex(r.body)
return &KeptCheck{Day: day, State: "held", Digest: d, Local: local, Matches: local == d}, nil
}
// ---- webhooks and gas sponsorship ----
func (c *Client) ListWebhooks(ctx context.Context) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/webhooks", nil, true)
}
func (c *Client) CreateWebhook(ctx context.Context, spec any) (json.RawMessage, error) {
return c.jsonReq(ctx, "POST", "/webhooks", spec, true)
}
func (c *Client) DeleteWebhook(ctx context.Context, id string) (json.RawMessage, error) {
return c.jsonReq(ctx, "DELETE", "/webhooks/"+esc(id), nil, true)
}
func (c *Client) SponsorHealth(ctx context.Context) (json.RawMessage, error) {
return c.jsonReq(ctx, "GET", "/sponsor/health", nil, true)
}
func (c *Client) SponsorCreateAccount(ctx context.Context, body any) (json.RawMessage, error) {
return c.jsonReq(ctx, "POST", "/sponsor/createAccount", body, true)
}
// SponsorCreatePQAccount: a smart account with a POST-QUANTUM owner (Falcon-512, 0x09-prefixed, 897 bytes), gas sponsored.
func (c *Client) SponsorCreatePQAccount(ctx context.Context, falconPubKeyHex string, salt uint64) (json.RawMessage, error) {
return c.jsonReq(ctx, "POST", "/sponsor/createPqAccount", map[string]any{"falconPubKey": falconPubKeyHex, "salt": salt}, true)
}
func (c *Client) SponsorExecute(ctx context.Context, body any) (json.RawMessage, error) {
return c.jsonReq(ctx, "POST", "/sponsor/execute", body, true)
}
// VerifyWebhook checks a webhook delivery's HMAC-SHA256 signature in constant time. rawBody must be the EXACT bytes
// received, not re-serialized JSON. Never act on a webhook without this.
func VerifyWebhook(rawBody []byte, signatureHeader, secret string) bool {
m := hmac.New(sha256.New, []byte(secret))
m.Write(rawBody)
want := []byte(hex.EncodeToString(m.Sum(nil)))
return subtle.ConstantTimeCompare(want, []byte(signatureHeader)) == 1
}

206
go/aerecloud_test.go Normal file
View File

@ -0,0 +1,206 @@
package aerecloud
// Probele clientului Go, pe un server httptest LOCAL: ce trimite clientul (metoda, cale, cheia, corpul, user-agent-ul) si ce
// face cu fiecare raspuns. Fiecare proba are perechea ei negativa; controlul negativ pe paznici e control-negativ.mjs.
// go test ./...
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
)
type vazut struct{ Method, Path, Key, Body, UA string }
type raspunsProba struct {
status int
body []byte
headers map[string]string
}
type server struct {
mu sync.Mutex
vazute []vazut
raspunsuri map[string]raspunsProba
srv *httptest.Server
}
func porneste(t *testing.T) (*server, *Client) {
s := &server{raspunsuri: map[string]raspunsProba{}}
s.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
p := r.URL.EscapedPath()
if r.URL.RawQuery != "" {
p += "?" + r.URL.RawQuery
}
s.mu.Lock()
s.vazute = append(s.vazute, vazut{r.Method, p, r.Header.Get("x-api-key"), string(b), r.Header.Get("user-agent")})
rp, ok := s.raspunsuri[r.URL.EscapedPath()]
s.mu.Unlock()
if !ok {
rp = raspunsProba{200, []byte("{}"), nil}
}
for k, v := range rp.headers {
w.Header().Set(k, v)
}
w.WriteHeader(rp.status)
w.Write(rp.body)
}))
t.Cleanup(s.srv.Close)
c, err := NewWithBase("ak2800.0xAB.SINTETIC", s.srv.URL+"/v1", nil)
if err != nil {
t.Fatal(err)
}
return s, c
}
func (s *server) ultima() vazut { s.mu.Lock(); defer s.mu.Unlock(); return s.vazute[len(s.vazute)-1] }
func (s *server) pune(p string, st int, body []byte, h map[string]string) {
s.mu.Lock()
s.raspunsuri[p] = raspunsProba{st, body, h}
s.mu.Unlock()
}
var ctx = context.Background()
func TestCheiaEObligatorie(t *testing.T) {
if _, err := New(""); err == nil {
t.Fatal("o cheie goala nu a fost refuzata")
}
}
func TestReadinessCheiaNumaiLaAttest(t *testing.T) {
s, c := porneste(t)
if _, err := c.Readiness(ctx, "example.com", false, false); err != nil {
t.Fatal(err)
}
v := s.ultima()
if v.Method != "POST" || v.Path != "/v1/pq/readiness" || v.Key != "" || v.Body != `{"domain":"example.com"}` {
t.Fatalf("fara attest: %+v", v)
}
if v.UA != "aere-cloud-go/1.5.0" {
t.Fatalf("user-agent: %q", v.UA)
}
c.Readiness(ctx, "example.com", true, true)
v = s.ultima()
if v.Key != "ak2800.0xAB.SINTETIC" || v.Body != `{"attest":true,"domain":"example.com","fresh":true}` {
t.Fatalf("cu attest: %+v", v)
}
}
func TestPerimetruFinalitateDovada(t *testing.T) {
s, c := porneste(t)
f := false
c.ReadinessPerimeter(ctx, "example.com", PerimeterOptions{Hosts: []string{"api", `www"x`}, Discover: &f, Attest: true})
v := s.ultima()
var b map[string]any
if err := json.Unmarshal([]byte(v.Body), &b); err != nil || b["discover"] != false || b["attest"] != true || len(b["hosts"].([]any)) != 2 || v.Key == "" {
t.Fatalf("perimetru: %+v", v)
}
c.PQFinality(ctx, "latest", "testnet", "both")
if v = s.ultima(); v.Path != "/v1/pq/finality/latest?network=testnet&client=both" || v.Key != "" {
t.Fatalf("finalitate: %+v", v)
}
c.Proof(ctx, "0xab")
if v = s.ultima(); v.Path != "/v1/proof/0xab" || v.Key == "" {
t.Fatalf("dovada: %+v", v)
}
}
var zi = []byte("{\"t\":\"2026-05-01T10:00:00.000Z\",\"m\":\"GET\",\"p\":\"/v1/account\",\"s\":200}\n")
func TestExportulZilei(t *testing.T) {
s, c := porneste(t)
d := sha256Hex(zi)
s.pune("/v1/account/audit", 200, zi, map[string]string{"x-aere-digest": d, "x-aere-day": "2026-05-01", "x-aere-entries": "1"})
e, err := c.AuditExport(ctx, "2026-05-01", "jsonl")
if err != nil || e.Verified == nil || !*e.Verified || e.Entries != 1 || e.Day != "2026-05-01" || string(e.Bytes) != string(zi) {
t.Fatalf("jsonl: %+v %v", e, err)
}
if v := s.ultima(); v.Path != "/v1/account/audit?format=jsonl&day=2026-05-01" {
t.Fatalf("cale: %s", v.Path)
}
s.pune("/v1/account/audit", 200, []byte("alti octeti"), map[string]string{"x-aere-digest": d})
if e, _ = c.AuditExport(ctx, "2026-05-01", "jsonl"); e.Verified == nil || *e.Verified {
t.Fatal("octeti straini nu au voie sa iasa verificati")
}
if e, _ = c.AuditExport(ctx, "2026-05-01", "cef"); e.Verified != nil {
t.Fatal("cef nu pretinde verificare")
}
s.pune("/v1/account/audit", 400, []byte(`{"error":"bad_format"}`), nil)
var ae *Error
if _, err = c.AuditExport(ctx, "", "xml"); !errors.As(err, &ae) || ae.Status != 400 {
t.Fatalf("400 neridicat: %v", err)
}
}
func TestExportulPastrat(t *testing.T) {
s, c := porneste(t)
d := sha256Hex(zi)
s.pune("/v1/account/audit/retention", 200, []byte(`{"retentionDays":90}`), nil)
if r, err := c.AuditRetention(ctx); err != nil || !strings.Contains(string(r), "90") || s.ultima().Method != "GET" || s.ultima().Key == "" {
t.Fatalf("retentie: %s %v", r, err)
}
piatra := `{"error":"audit_day_expired","expired":{"day":"2026-05-01","digest":"` + d + `","entries":1,"deletedAt":"2026-09-25T12:00:01.000Z","retentionDays":90}}`
s.pune("/v1/account/audit", 410, []byte(piatra), nil)
k, err := c.AuditVerifyKept(ctx, "2026-05-01", zi)
if err != nil || k.State != "expired" || !k.Matches || k.Digest != d || k.DeletedAt != "2026-09-25T12:00:01.000Z" {
t.Fatalf("piatra: %+v %v", k, err)
}
stricat := append([]byte{}, zi...)
stricat[5] ^= 1
if k, _ = c.AuditVerifyKept(ctx, "2026-05-01", stricat); k.Matches {
t.Fatal("un export modificat nu are voie sa se potriveasca cu piatra")
}
s.pune("/v1/account/audit", 200, zi, map[string]string{"x-aere-digest": d})
if k, _ = c.AuditVerifyKept(ctx, "2026-05-01", zi); k.State != "held" || !k.Matches {
t.Fatalf("ziua tinuta: %+v", k)
}
if k, _ = c.AuditVerifyKept(ctx, "2026-05-01", stricat); k.Matches {
t.Fatal("ziua tinuta: un export modificat nu se potriveste")
}
s.pune("/v1/account/audit", 429, []byte(`{"error":"rate_limit"}`), nil)
var ae *Error
if _, err = c.AuditVerifyKept(ctx, "2026-05-01", zi); !errors.As(err, &ae) || ae.Status != 429 {
t.Fatalf("un 429 intors ca nepotrivire: %v", err)
}
}
func TestRedirectareaNuEUrmata(t *testing.T) {
s, c := porneste(t)
// o redirectare spre alta cale a aceluiasi server: daca ar fi urmata, cheia ar ajunge acolo
s.pune("/v1/account", 302, nil, map[string]string{"location": s.srv.URL + "/in-alta-parte"})
var ae *Error
if _, err := c.Account(ctx); !errors.As(err, &ae) || ae.Status != 302 {
t.Fatalf("302 trebuia intors ca raspuns, nu urmat: %v", err)
}
for _, v := range s.vazute {
if strings.HasPrefix(v.Path, "/in-alta-parte") {
t.Fatal("redirectarea a fost urmata")
}
}
}
func TestVerifyWebhook(t *testing.T) {
secret := "AERE-SINTETIC-secret"
corp := []byte(`{"event":"pq-anchor","data":{"height":42}}`)
m := hmac.New(sha256.New, []byte(secret))
m.Write(corp)
bun := hex.EncodeToString(m.Sum(nil))
if !VerifyWebhook(corp, bun, secret) {
t.Fatal("semnatura buna refuzata")
}
stricata := bun[:len(bun)-1] + map[bool]string{true: "b", false: "a"}[strings.HasSuffix(bun, "a")]
if VerifyWebhook(corp, stricata, secret) || VerifyWebhook(corp, bun+"x", secret) || VerifyWebhook(append(corp, ' '), bun, secret) {
t.Fatal("o semnatura gresita sau un corp atins a trecut")
}
}

53
go/control-negativ.mjs Normal file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env node
// Controlul negativ al probelor clientului Go: fiecare paznic se strica intr-o COPIE a pachetului (sursa din depozit nu se
// atinge), `go test` ruleaza pe copie si proba tinta TREBUIE sa pice; o plantare care nu se aplica sau nu compileaza e STRICAT,
// nu rosu. La urma, pachetul neatins trebuie sa treaca intreg.
// node aerenew/sdk/go/control-negativ.mjs
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const AICI = path.dirname(fileURLToPath(import.meta.url));
const FISIERE = ['go.mod', 'aerecloud.go', 'aerecloud_test.go'];
const sursa = fs.readFileSync(path.join(AICI, 'aerecloud.go'), 'utf8');
const PLANTARI = [
['exportul jsonl declarat verificat fara hash', 'ok := e.Digest != "" && e.Digest == sha256Hex(r.body)', 'ok := true', 'TestExportulZilei'],
['piatra zilei sterse acceptata fara comparatie', 'Matches: local == t.Expired.Digest,', 'Matches: true,', 'TestExportulPastrat'],
['ziua tinuta acceptata fara comparatie', 'Matches: local == d}', 'Matches: true}', 'TestExportulPastrat'],
['cheia trimisa si fara attest', 'return c.jsonReq(ctx, "POST", "/pq/readiness", b, attest)', 'return c.jsonReq(ctx, "POST", "/pq/readiness", b, true)', 'TestReadinessCheiaNumaiLaAttest'],
['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'],
];
function ruleaza(src) {
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-go-cn-'));
try {
for (const f of FISIERE) fs.copyFileSync(path.join(AICI, f), path.join(d, f));
fs.writeFileSync(path.join(d, 'aerecloud.go'), src);
const r = spawnSync('go', ['test', '-count=1', '-v', './...'], { cwd: d, encoding: 'utf8' });
const out = (r.stdout || '') + (r.stderr || '');
if (/\[build failed\]|cannot use|undefined:|syntax error/.test(out)) return { stricat: 'nu compileaza: ' + out.split('\n').find((l) => /error|undefined|cannot/.test(l)) };
const picate = [...out.matchAll(/--- FAIL: (\w+)/g)].map((m) => m[1]);
const trecute = [...out.matchAll(/--- PASS: (\w+)/g)].map((m) => m[1]);
if (!picate.length && !trecute.length) return { stricat: 'probele nu au rulat' };
return { picate, trecute };
} finally { fs.rmSync(d, { recursive: true, force: true }); }
}
let rele = 0;
for (const [nume, vechi, nou, tinta] of PLANTARI) {
if (sursa.split(vechi).length !== 2) { console.log(` STRICAT ${nume}: ancora apare de ${sursa.split(vechi).length - 1} ori`); rele++; continue; }
const r = ruleaza(sursa.replace(vechi, nou));
if (r.stricat) { console.log(` STRICAT ${nume}: ${r.stricat}`); rele++; continue; }
if (r.picate.includes(tinta)) console.log(` ROSU cum trebuia ${nume} (pica ${r.picate.join(', ')})`);
else { console.log(` CONTROL CAZUT ${nume}: ${tinta} a ramas verde`); rele++; }
}
const m = ruleaza(sursa);
if (m.stricat || m.picate.length) { console.log(' MARTORUL nu e verde: ' + JSON.stringify(m)); rele++; }
else console.log(` martor: sursa neatinsa, ${m.trecute.length} probe verzi`);
console.log(rele ? `CONTROL NEGATIV: ${rele} probleme` : `CONTROL NEGATIV TRECUT (${PLANTARI.length} din ${PLANTARI.length} rosii, martorul verde)`);
process.exitCode = rele ? 1 : 0;

3
go/go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.aere.network/aere-network/aere-cloud-sdk/go
go 1.21