// @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; }