diff --git a/go/README.md b/go/README.md
new file mode 100644
index 0000000..8ec34cf
--- /dev/null
+++ b/go/README.md
@@ -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.
diff --git a/go/aerecloud.go b/go/aerecloud.go
new file mode 100644
index 0000000..3818f79
--- /dev/null
+++ b/go/aerecloud.go
@@ -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.
.).
+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..)")
+ }
+ 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
+}
diff --git a/go/aerecloud_test.go b/go/aerecloud_test.go
new file mode 100644
index 0000000..3dfb4f7
--- /dev/null
+++ b/go/aerecloud_test.go
@@ -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")
+ }
+}
diff --git a/go/control-negativ.mjs b/go/control-negativ.mjs
new file mode 100644
index 0000000..fc216b6
--- /dev/null
+++ b/go/control-negativ.mjs
@@ -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;
diff --git a/go/go.mod b/go/go.mod
new file mode 100644
index 0000000..c7a2172
--- /dev/null
+++ b/go/go.mod
@@ -0,0 +1,3 @@
+module git.aere.network/aere-network/aere-cloud-sdk/go
+
+go 1.21