@aere/cloud v1.0.0: clientul oficial al Aere Cloud, zero dependinte (RPC, PQ verify, date, transferuri, notarizare, webhooks, gaz), 7 teste inclusiv verifyWebhook cu control negativ si proba vie

This commit is contained in:
Aere Network 2026-08-23 17:21:45 +03:00
commit fa8478c816
6 changed files with 320 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/
*.log

15
LICENSE Normal file
View File

@ -0,0 +1,15 @@
MIT License
Copyright (c) 2026 Aere Network
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.

77
README.md Normal file
View File

@ -0,0 +1,77 @@
# @aere/cloud
The official 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; runs in Node 18+ and modern
browsers.
## Install
```
npm install @aere/cloud
```
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>`.
## Use
```js
import { AereCloud } from '@aere/cloud';
const aere = new AereCloud({ apiKey: process.env.AERE_KEY });
// chain data
const head = await aere.dataHead();
const anchors = await aere.anchors(5); // post-quantum anchor certificates
const set = await aere.validators(); // live QBFT validator set
// JSON-RPC
const block = await aere.blockNumber();
// post-quantum verification, computed by the chain's own precompiles
const { valid } = await aere.pqVerify({
scheme: 'ml-dsa-44', publicKey: '0x…', signature: '0x…', message: '0x…',
});
// notarize a document hash — Aere pays the gas
const receipt = await aere.notarize('0x' + sha256Hex);
const proof = await aere.proofOf('0x' + sha256Hex); // { notarized, firstSeenAt, … }
// transfer history for an address (token from genesis, native from launch)
const tx = await aere.transfers('0xYourAddress', 50);
```
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.
## Webhooks
Create a webhook and verify every delivery — never trust a webhook payload without checking its
signature:
```js
import { AereCloud, verifyWebhook } from '@aere/cloud';
const aere = new AereCloud({ apiKey: process.env.AERE_KEY });
const wh = await aere.createWebhook({ type: 'pq-anchors', url: 'https://you.example/hook' });
// store wh.secret now — it is shown once
// in your webhook handler (raw body, not re-serialized):
app.post('/hook', async (req, res) => {
const ok = await verifyWebhook(req.rawBody, req.headers['x-aere-signature'], WEBHOOK_SECRET);
if (!ok) return res.status(401).end();
handle(JSON.parse(req.rawBody));
res.status(200).end();
});
```
Webhook types: `pq-anchors` (every 32nd-block certificate), `address-activity` (transactions
touching a watched address, pass `watch`), `subscription-events` (your billing events).
## Reference
Full API reference and OpenAPI spec: [aere.network/cloud-docs.html](https://aere.network/cloud-docs.html)
· live status you compute in your own browser: [aere.network/cloud-status.html](https://aere.network/cloud-status.html)
MIT licensed.

109
index.mjs Normal file
View File

@ -0,0 +1,109 @@
// @aere/cloud — clientul oficial al Aere Cloud. Zero dependinte: doar fetch si crypto,
// disponibile in Node 18+ si in browser. Impacheteaza fiecare ruta a gateway-ului intr-o
// metoda, cu erorile ridicate ca AereCloudError (cod HTTP + corpul serverului), ca integratorul
// sa nu mai construiasca cereri de mana. Aceeasi filozofie ca API-ul: nimic ascuns, totul
// verificabil — de aceea verifyWebhook e in SDK, ca primitorul de webhook-uri sa nu-si scrie
// singur verificarea HMAC (locul unde integrarile gresesc cel mai des si primesc date false).
export class AereCloudError extends Error {
constructor(status, body, path) {
super(`Aere Cloud ${status} on ${path}: ${body && body.error ? body.error : 'request failed'}`);
this.name = 'AereCloudError';
this.status = status;
this.body = body;
this.path = path;
}
}
export class AereCloud {
/**
* @param {object} opts
* @param {string} opts.apiKey cheia in forma ak2800.<adresa>.<secret>
* @param {string} [opts.baseUrl='https://cloud.aere.network/v1']
* @param {function} [opts.fetch] injectabil pentru teste/Node vechi
*/
constructor(opts = {}) {
if (!opts.apiKey) throw new Error('AereCloud: apiKey is required (ak2800.<address>.<secret>)');
this.apiKey = opts.apiKey;
this.baseUrl = (opts.baseUrl || 'https://cloud.aere.network/v1').replace(/\/$/, '');
this._fetch = opts.fetch || globalThis.fetch;
if (!this._fetch) throw new Error('AereCloud: no fetch available; pass opts.fetch on old Node');
}
async _req(method, path, { body, auth = true, raw = false } = {}) {
const headers = { 'content-type': 'application/json' };
if (auth) headers['x-api-key'] = this.apiKey;
const r = await this._fetch(this.baseUrl + path, {
method, headers, body: body === undefined ? undefined : JSON.stringify(body),
});
if (raw) return r;
let data = null;
try { data = await r.json(); } catch { /* corp non-JSON */ }
if (!r.ok) throw new AereCloudError(r.status, data, path);
return data;
}
// ---- health & account ----
health() { return this._req('GET', '/health', { auth: false }); }
account() { return this._req('GET', '/account'); }
// ---- JSON-RPC ----
/** Un apel JSON-RPC brut; @returns rezultatul sau arunca la eroarea RPC. */
async rpc(method, params = []) {
const d = await this._req('POST', '/rpc', { body: { jsonrpc: '2.0', id: 1, method, params } });
if (d && d.error) throw new AereCloudError(200, d, '/rpc');
return d && d.result;
}
async blockNumber() { return parseInt(await this.rpc('eth_blockNumber'), 16); }
// ---- post-quantum verification ----
/**
* @param {object} p { scheme, publicKey, signature?, message?, signedMessage? } (0x-hex)
* @returns {Promise<{valid:boolean, scheme, precompile, block, chainId}>}
*/
pqVerify(p) { return this._req('POST', '/pq/verify', { body: p }); }
// ---- chain data ----
dataHead() { return this._req('GET', '/data/head'); }
validators() { return this._req('GET', '/data/validators'); }
anchors(limit = 10) { return this._req('GET', `/data/anchors?limit=${encodeURIComponent(limit)}`); }
anchor(height) { return this._req('GET', `/data/anchors/${encodeURIComponent(height)}`); }
transfers(address, limit = 50) {
return this._req('GET', `/data/transfers?address=${encodeURIComponent(address)}&limit=${encodeURIComponent(limit)}`);
}
// ---- notarization ----
/** Notarizeaza un digest de 32 de octeti (0x + 64 hex); noi platim gazul. */
notarize(hash) { return this._req('POST', '/notarize', { body: { hash } }); }
proofOf(hash) { return this._req('GET', `/notarize/${encodeURIComponent(hash)}`); }
// ---- webhooks ----
listWebhooks() { return this._req('GET', '/webhooks'); }
createWebhook(spec) { return this._req('POST', '/webhooks', { body: spec }); }
deleteWebhook(id) { return this._req('DELETE', `/webhooks/${encodeURIComponent(id)}`); }
// ---- gas sponsorship ----
sponsorHealth() { return this._req('GET', '/sponsor/health'); }
sponsorCreateAccount(body) { return this._req('POST', '/sponsor/createAccount', { body }); }
sponsorExecute(body) { return this._req('POST', '/sponsor/execute', { body }); }
}
/**
* Verifica semnatura unei livrari de webhook. A NU crede un webhook fara asta.
* @param {string} rawBody corpul brut al cererii, EXACT ca octeti (nu re-serializat)
* @param {string} signatureHeader antetul x-aere-signature
* @param {string} secret secretul webhook-ului, primit o data la creare
* @returns {boolean} true doar daca semnatura se potriveste
*/
export async function verifyWebhook(rawBody, signatureHeader, secret) {
const enc = new TextEncoder();
const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
const mac = await crypto.subtle.sign('HMAC', key, typeof rawBody === 'string' ? enc.encode(rawBody) : rawBody);
const asteptat = Array.from(new Uint8Array(mac), (b) => b.toString(16).padStart(2, '0')).join('');
const primit = String(signatureHeader || '');
// comparatie in timp constant, pe lungimi egale
if (asteptat.length !== primit.length) return false;
let dif = 0;
for (let i = 0; i < asteptat.length; i++) dif |= asteptat.charCodeAt(i) ^ primit.charCodeAt(i);
return dif === 0;
}

16
package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "@aere/cloud",
"version": "1.0.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",
"exports": { ".": "./index.mjs" },
"files": ["index.mjs", "README.md"],
"scripts": { "test": "node test/sdk.test.mjs" },
"keywords": ["aere", "blockchain", "evm", "post-quantum", "falcon", "rpc", "web3", "chain-2800"],
"license": "MIT",
"homepage": "https://aere.network/cloud.html",
"repository": { "type": "git", "url": "https://git.aere.network/aere-network/aere-cloud-sdk" },
"engines": { "node": ">=18" },
"dependencies": {}
}

101
test/sdk.test.mjs Normal file
View File

@ -0,0 +1,101 @@
// Testele SDK-ului: rutele contra unui server FALS (deterministe, offline) + verifyWebhook cu
// perechea negativa + o proba VIE optionala contra productiei daca AERE_CHEIE e in mediu.
import assert from 'node:assert';
import crypto from 'node:crypto';
import { AereCloud, AereCloudError, verifyWebhook } from '../index.mjs';
let treceri = 0;
async function test(nume, fn) {
try { await fn(); treceri++; console.log(' OK ' + nume); }
catch (e) { console.log(' ESEC ' + nume + ' — ' + e.message); process.exitCode = 1; }
}
// ---- server fals: raspunde dupa cale, ca sa testam constructia cererii si maparea erorilor ----
function fetchFals(rute) {
return async (url, opts = {}) => {
const u = new URL(url);
const cale = u.pathname.replace(/^\/v1/, ''); // baseUrl de test include /v1
const cheie = opts.method + ' ' + cale + u.search;
const r = rute[cheie] || rute[opts.method + ' ' + cale];
if (!r) return { ok: false, status: 404, json: async () => ({ error: 'not_found' }) };
r._vazut = { headers: opts.headers, body: opts.body ? JSON.parse(opts.body) : undefined };
return { ok: r.status < 400, status: r.status, json: async () => r.corp };
};
}
// AERE-SINTETIC: cheile de mai jos (ak2800.0xAAA.secret, secretul-meu) sunt FICTIVE, pentru
// testul offline; nu sunt chei ale retelei. Marcajul spune portii de secrete exact asta.
const BAZA_TEST = 'https://x.test/v1';
await test('constructorul cere cheia', () => {
assert.throws(() => new AereCloud({}), /apiKey is required/);
});
await test('cheia merge in antetul x-api-key, health e fara cheie', async () => {
const rute = { 'GET /health': { status: 200, corp: { ok: true, chainId: 2800, block: 5 } },
'GET /account': { status: 200, corp: { address: '0xabc', planId: 5 } } };
const c = new AereCloud({ apiKey: 'ak2800.0xAAA.AERE-SINTETIC-secret', baseUrl: BAZA_TEST, fetch: fetchFals(rute) });
const h = await c.health();
assert.equal(h.block, 5);
assert.equal(rute['GET /health']._vazut.headers['x-api-key'], undefined, 'health nu trimite cheie');
await c.account();
assert.equal(rute['GET /account']._vazut.headers['x-api-key'], 'ak2800.0xAAA.AERE-SINTETIC-secret');
});
await test('rpc impacheteaza corect si ridica eroarea RPC', async () => {
const rute = {
'POST /rpc': { status: 200, corp: { jsonrpc: '2.0', id: 1, result: '0x10' } },
};
const c = new AereCloud({ apiKey: 'ak2800.0xAAA.AERE-SINTETIC-s', baseUrl: BAZA_TEST, fetch: fetchFals(rute) });
assert.equal(await c.blockNumber(), 16);
assert.deepEqual(rute['POST /rpc']._vazut.body, { jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] });
// eroare RPC in plic -> AereCloudError
const rute2 = { 'POST /rpc': { status: 200, corp: { error: { code: -32601, message: 'nope' } } } };
const c2 = new AereCloud({ apiKey: 'ak2800.0xAAA.AERE-SINTETIC-s', baseUrl: BAZA_TEST, fetch: fetchFals(rute2) });
await assert.rejects(() => c2.rpc('qbft_proposeValidatorVote'), (e) => e instanceof AereCloudError && e.body.error.code === -32601);
});
await test('un status HTTP de eroare devine AereCloudError cu corpul serverului', async () => {
const rute = { 'POST /pq/verify': { status: 400, corp: { error: 'bad_publicKey', hint: '0x hex' } } };
const c = new AereCloud({ apiKey: 'ak2800.0xAAA.AERE-SINTETIC-s', baseUrl: BAZA_TEST, fetch: fetchFals(rute) });
await assert.rejects(() => c.pqVerify({ scheme: 'ml-dsa-44', publicKey: 'nu-e-hex' }),
(e) => e instanceof AereCloudError && e.status === 400 && e.body.error === 'bad_publicKey');
});
await test('rutele de date construiesc corect query-ul', async () => {
const rute = {
'GET /data/anchors?limit=3': { status: 200, corp: { anchors: [] } },
'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.transfers('0xBeef', 25);
// daca reperele nu s-ar potrivi, fetchFals ar da 404 si _req ar arunca
});
await test('verifyWebhook: semnatura buna trece, una gresita pica, o litera in plus pica', async () => {
const secret = 'AERE-SINTETIC-secretul-meu';
const corp = JSON.stringify({ event: 'pq-anchor', data: { height: 42 } });
const bun = crypto.createHmac('sha256', secret).update(corp).digest('hex'); // ca serverul
assert.equal(await verifyWebhook(corp, bun, secret), true);
assert.equal(await verifyWebhook(corp, bun.slice(0, -1) + (bun.slice(-1) === 'a' ? 'b' : 'a'), secret), false);
assert.equal(await verifyWebhook(corp, bun + 'x', secret), false);
assert.equal(await verifyWebhook(corp + ' ', bun, secret), false, 'corp modificat = alta semnatura');
});
// ---- proba VIE optionala ----
if (process.env.AERE_CHEIE) {
await test('LIVE: health + account + anchors prin productie', async () => {
const c = new AereCloud({ apiKey: process.env.AERE_CHEIE });
const h = await c.health();
assert.equal(h.chainId, 2800);
assert.ok(h.block > 0);
const a = await c.anchors(1);
assert.ok(a.anchors[0].falconSeals >= 6, 'ancora vie sub cvorum');
const acc = await c.account();
assert.ok(acc.usageLast31Days);
});
} else {
console.log(' (sar proba LIVE: AERE_CHEIE nu e in mediu)');
}
console.log('\n' + treceri + ' teste trecute');