diff --git a/README.md b/README.md index 6a23acf..5707cba 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ const attested = await aere.readiness('example.com', { attest: true }); // + att const perimeter = await aere.readinessPerimeter('example.com', { hosts: ['api', 'mail'] }); // perimeter.summary: { measured, withoutPqKeyExchange: [...], soonestCertificateExpiry, worstScore, verdict, complete } // while summary.complete is false, call again: finished scans are cached; { attest: true } notarizes a complete report +// the audit log of a day as a FILE (1.4.0): the exact bytes whose sha256 is the digest you notarize; format: 'cef' for a SIEM +const file = await aere.auditExport({ day: '2026-09-17' }); // file.verified === true: sha256(file.bytes) is the stated digest const fin = await aere.pqFinality('latest', 'testnet', 'both'); // AIP-21: { finality, agreement, postQuantumConfirmedByBoth, besu: {…}, nethermind: {…} } // pqVerify: pass interface:'external' to verify a standard FIPS 204/205 signature (the precompile implements the diff --git a/index.mjs b/index.mjs index 128bd64..f7b2f3e 100644 --- a/index.mjs +++ b/index.mjs @@ -107,6 +107,19 @@ export class AereCloud { 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 }; + } // ---- webhooks ---- listWebhooks() { return this._req('GET', '/webhooks'); } diff --git a/package.json b/package.json index 7b14832..a3bccbb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aere/cloud", - "version": "1.3.0", + "version": "1.4.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", diff --git a/test/sdk.test.mjs b/test/sdk.test.mjs index 13a78a9..0d43d84 100644 --- a/test/sdk.test.mjs +++ b/test/sdk.test.mjs @@ -113,6 +113,24 @@ await test('2026-09-18: readinessPerimeter(): ruta cu cheie, corpul poarta doar await assert.rejects(() => c409.readinessPerimeter('example.com', { attest: true }), (e) => e.status === 409); }); +await test('2026-09-18: auditExport(): octetii zilei + digestul din antet; verified=true cand octetii dau digestul, FALSE cand nu il dau; cef nu pretinde verificare; un 400 ajunge la apelant', async () => { + const { createHash } = await import('node:crypto'); + const corp = Buffer.from('{"t":"2026-09-17T10:00:00.000Z","m":"GET","p":"/v1/account","s":200}\n'); + const d = '0x' + createHash('sha256').update(corp).digest('hex'); + const raspuns = (octeti, digest, status = 200) => async (url, init) => new Response(octeti, { status, headers: { 'x-aere-digest': digest, 'x-aere-day': '2026-09-17', 'x-aere-entries': '1' } }); + const vazute = []; + const c = new AereCloud({ apiKey: 'ak2800.0xAAA.AERE-SINTETIC-s', baseUrl: BAZA_TEST, fetch: async (u, i) => { vazute.push({ u, cheie: !!i.headers['x-api-key'] }); return raspuns(corp, d)(u, i); } }); + const e = await c.auditExport({ day: '2026-09-17' }); + assert.equal(e.verified, true); assert.equal(e.digest, d); assert.equal(e.entries, 1); assert.equal(e.text, corp.toString('utf8')); assert.equal(vazute[0].cheie, true); + assert.ok(vazute[0].u.endsWith('/account/audit?format=jsonl&day=2026-09-17'), vazute[0].u); + const stricat = new AereCloud({ apiKey: 'ak2800.0xAAA.AERE-SINTETIC-s', baseUrl: BAZA_TEST, fetch: raspuns(Buffer.from('alti octeti'), d) }); + assert.equal((await stricat.auditExport({ day: '2026-09-17' })).verified, false, 'octeti care nu dau digestul declarat nu au voie sa iasa verified'); + const cef = new AereCloud({ apiKey: 'ak2800.0xAAA.AERE-SINTETIC-s', baseUrl: BAZA_TEST, fetch: raspuns(Buffer.from('CEF:0|x\n'), d) }); + assert.equal((await cef.auditExport({ day: '2026-09-17', format: 'cef' })).verified, null, 'pentru cef digestul e al fisierului-sursa, nu al textului primit'); + const rau = new AereCloud({ apiKey: 'ak2800.0xAAA.AERE-SINTETIC-s', baseUrl: BAZA_TEST, fetch: async () => new Response(JSON.stringify({ error: 'bad_format' }), { status: 400 }) }); + await assert.rejects(() => rau.auditExport({ format: 'xml' }), (x) => x.status === 400); +}); + 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 } });