commit fa8478c816e77ccab86a453d765d4b36d39e5ae0 Author: Aere Network Date: Sun Aug 23 17:21:45 2026 +0300 @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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..552f221 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +*.log diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f8ffba4 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c0f3c24 --- /dev/null +++ b/README.md @@ -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..`. + +## 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. diff --git a/index.mjs b/index.mjs new file mode 100644 index 0000000..c0f4f4e --- /dev/null +++ b/index.mjs @@ -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.. + * @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.
.)'); + 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; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f52a7ba --- /dev/null +++ b/package.json @@ -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": {} +} diff --git a/test/sdk.test.mjs b/test/sdk.test.mjs new file mode 100644 index 0000000..a782480 --- /dev/null +++ b/test/sdk.test.mjs @@ -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');