aere-proof-of-software/sdk/index.mjs
Aere Network 065f84e34a Aere Proof of Software 1.4.0: an attestation of what was built, from what, by whom and when, verifiable without trusting Aere Network
Every artifact's SHA-256 and size, the SBOM's digest, the git commit and tree of the source, a hybrid signature (classical + ML-DSA,
both required) and, when notarized, a first-seen time on Aere Network covered by the validators' post-quantum certificate. verify
recomputes everything from the files; --rebuild-from repeats an npm pack build from a clone the verifier chose; the SBOM is
re-derived from the committed package-lock.json or, new in 1.4.0, from the committed go.mod and go.sum; --signer requires the
signing keys you expect (1.4.0); a developer credential binds the keys to a person, judged against a trust root you choose. The
builder's declared date can only accuse, never acquit (1.4.0). Includes the GitHub Action and the hybrid signature library it uses.

Laid out as in the development repository (tools/proof-of-software/, sdk-pq-sign/, sdk/) so that nothing is rewritten for
publication. Tests and negative controls measured on 2026-09-29 are listed in README.md.
2026-09-29 22:46:57 +03:00

195 lines
12 KiB
JavaScript

// @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;
// 1.5.1: fara redirectari urmate. fetch le urmeaza implicit si duce x-api-key la orice gazda numita in Location;
// un 3xx e un raspuns (AereCloudError cu statusul lui), nu un drum.
const r = await this._fetch(this.baseUrl + path, {
method, headers, body: body === undefined ? undefined : JSON.stringify(body), redirect: 'manual',
});
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 }); }
/**
* the Trust API (1.6.0): one AERE Proof Protocol (AIP-23) envelope of any kind -> VALID / INVALID / PARTIAL with the levels checked and the command that reproduces the verdict; the judgement is the published reference verifier. Needs a gateway that serves POST /v1/verify.
* @param {object} envelope the envelope as produced (statement, statementHash, signatures, notarization...)
* @param {{chain?: number}} [opts] 2800 or 28001; omitted = the chain the envelope declares
* @returns {Promise<{verdict:'VALID'|'INVALID'|'PARTIAL', levels:object[], verifier:object, reproduce:string}>}
*/
verify(envelope, { chain } = {}) {
return this._req('POST', '/verify' + (chain ? `?chain=${encodeURIComponent(chain)}` : ''), { body: envelope });
}
// ---- 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)}`); }
// 2026-09-26: the post-quantum finality point (finalized/safe = the parent of the last certified anchor), the tag nodes do not serve yet
finality() { return this._req('GET', '/data/finality'); }
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)}`); }
// 2026-09-17: the proof with its post-quantum finality (first appearance, covering anchor, verdict, verification steps)
proof(hash) { return this._req('GET', `/proof/${encodeURIComponent(hash)}`); }
// quantum readiness scan of a domain's public TLS edge: free without a key; { attest: true } notarizes the report digest
// on chain 2800 and needs the key (the response then carries `attestation`)
readiness(domain, opts = {}) {
const body = { domain, ...(opts.fresh ? { fresh: true } : {}), ...(opts.attest ? { attest: true } : {}) };
return this._req('POST', '/pq/readiness', { body, auth: !!opts.attest });
}
readinessReport(domain) { return this._req('GET', `/pq/readiness/${encodeURIComponent(domain)}`, { auth: false }); }
// the whole public perimeter of a domain in one report (keyed): the hosts you name (labels or names under the domain) plus common
// prefixes found through DNS, each measured with the same handshakes. The request has a deadline: while summary.complete is false,
// call again (finished scans are cached). { attest: true } notarizes the digest of a COMPLETE report on chain 2800.
readinessPerimeter(domain, opts = {}) {
const body = { domain, ...(opts.hosts ? { hosts: opts.hosts } : {}), ...(opts.discover === false ? { discover: false } : {}), ...(opts.attest ? { attest: true } : {}) };
return this._req('POST', '/pq/readiness/perimeter', { body });
}
// AIP-21 (2026-09-18): the per-block post-quantum finality record (seals heard by a validator, re-verified, quorum
// verdict). Free, no key. Testnet 28001 first; `network` is reserved for chain 2800 once the method reaches its validators.
// `client`: 'besu' (default), 'nethermind', or 'both' (two independent implementations, with `agreement` computed by the gateway).
pqFinality(block = 'latest', network = 'testnet', client = 'besu') {
return this._req('GET', `/pq/finality/${encodeURIComponent(block)}?network=${encodeURIComponent(network)}&client=${encodeURIComponent(client)}`, { auth: false });
}
// audit log of every keyed request of this account for one UTC day (append-only, paged), with the day file's sha256
auditLog({ day, offset = 0, limit = 1000 } = {}) {
const q = new URLSearchParams({ offset: String(offset), limit: String(limit) });
if (day) q.set('day', day);
return this._req('GET', `/account/audit?${q}`);
}
// the same day as a file (1.4.0): format 'jsonl' = the EXACT bytes of the day file, whose sha256 is the digest you notarize
// (`verified` says the bytes you received hash to the digest the gateway states); 'cef' = one ArcSight CEF line per request
// for a SIEM, carrying the digest of the source file. Returns { day, format, bytes, text, entries, digest, verified }.
async auditExport({ day, format = 'jsonl' } = {}) {
const q = new URLSearchParams({ format }); if (day) q.set('day', day);
const r = await this._req('GET', `/account/audit?${q}`, { raw: true });
if (!r.ok) { let d = null; try { d = await r.json(); } catch { /* non-JSON */ } throw new AereCloudError(r.status, d, '/account/audit'); }
const bytes = new Uint8Array(await r.arrayBuffer());
const digest = r.headers.get('x-aere-digest');
const local = '0x' + [...new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', bytes))].map((b) => b.toString(16).padStart(2, '0')).join('');
return { day: r.headers.get('x-aere-day'), format, bytes, text: new TextDecoder().decode(bytes), entries: Number(r.headers.get('x-aere-entries') || 0), digest,
verified: format === 'jsonl' ? local === digest : null };
}
// retention of the audit log (1.5.0), READ-ONLY with the API key: what is in force, what is pending and until when, what we
// hold, and the ledger of every change and of every day deleted (each with its digest). 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.
auditRetention() { return this._req('GET', '/account/audit/retention'); }
// does an export you KEPT still match the day on our side, even after we deleted it under your retention? Hashes YOUR bytes
// locally and compares with the digest of the day we hold ('held') or with the tombstone the deleted day left ('expired').
// Returns { day, state, digest, local, matches, deletedAt }.
async auditVerifyKept(day, bytes) {
const u8 = typeof bytes === 'string' ? new TextEncoder().encode(bytes) : new Uint8Array(bytes);
const local = '0x' + [...new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', u8))].map((b) => b.toString(16).padStart(2, '0')).join('');
const r = await this._req('GET', `/account/audit?${new URLSearchParams({ format: 'jsonl', day })}`, { raw: true });
let d = null;
if (!r.ok) { try { d = await r.json(); } catch { /* non-JSON */ } }
if (r.status === 410 && d && d.expired) return { day, state: 'expired', digest: d.expired.digest, local, matches: local === d.expired.digest, deletedAt: d.expired.deletedAt };
if (!r.ok) throw new AereCloudError(r.status, d, '/account/audit');
await r.arrayBuffer();
const digest = r.headers.get('x-aere-digest');
return { day, state: 'held', digest, local, matches: local === digest, deletedAt: null };
}
// ---- 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 }); }
/**
* Cont inteligent cu proprietar POST-CUANTIC (Falcon-512), gazul platit de releu.
* @param {string} falconPubKey 0x-hex, 897 octeti, antetul 0x09
* @param {number} [salt=0] idempotent pe (cheie, salt): a doua chemare intoarce contul, fara tx
*/
sponsorCreatePqAccount(falconPubKey, salt = 0) {
return this._req('POST', '/sponsor/createPqAccount', { body: { falconPubKey, salt } });
}
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;
}