#!/usr/bin/env node // Aere Proof of Software ca GitHub Action (randul 19 din lista Aere Cloud: integrari native, GitHub e prima). // // Ordinea pasilor, si de ce e ordinea asta: // 1. mastile pentru intrarile secrete (::add-mask::) INAINTEA oricarei alte iesiri // 2. refuz daca source-path are schimbari necomise sau fisiere neurmarite: se atesta ARBORELE comis, nu copia de lucru // 3. artefactul = `npm pack` al arborelui comis (pos.mjs packNpmFromTree: octetii din magazia git, fara conversia de la // checkout; pe un runner Windows cu core.autocrlf=true copia de lucru are CRLF, arborele nu) // 4. atestarea (pos.mjs attest), cu semnatura hibrida daca exista signing-key // 5. verificarea prin RECONSTRUCTIE dintr-o clona curata a commitului (pos.mjs verify --rebuild-from); refuz daca nu // reproduce artefactul octet cu octet // 6. ABIA APOI, daca exista aere-api-key, notarizarea statementHash: o tranzactie pe 2800 nu se cheltuie pe o atestare // care nu se reproduce. Se judeca CODUL HTTP (200) si chitanta (txHash, block), nu doar ca fetch a mers; fara // redirectari urmate (un 30x ar duce antetul x-api-key la alta gazda) // 7. atestarea se scrie, se reciteste de pe disc si se verifica, apoi iesirile ($GITHUB_OUTPUT) si rezumatul // Logica atestarii NU e copiata aici: vine din ../pos.mjs, importat dinamic dupa ce dependintele lui (sdk-pq-sign -> // @noble/*) sunt la locul lor. Acest fisier foloseste numai module node: (nicio dependinta externa). // // Niciun secret nu se tipareste: tot ce iese (jurnal, erori, rezumat, iesiri) trece prin Secrete.curata() (valorile secrete // exacte, fara deosebire de litere mari/mici), iar mesajele BIBLIOTECILOR trec in plus prin taieHex() (orice sir hexa lung). // Mesajele de parsare ale cheii NU se transmit deloc: JSON.parse din V8 citeaza o bucata din textul pe care cade. // // Carlige de proba (AERE_POS_TEST_HOOK, AERE_POS_API_BASE): exista numai pentru test/action.test.mjs. Carligele sunt // refuzate pe un runner GitHub (GITHUB_ACTIONS=true); baza API poate arata numai spre o adresa loopback, deci cheia API // nu poate fi trimisa altundeva decat la cloud.aere.network sau pe masina locala. import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import crypto from 'node:crypto'; import { execFileSync, execSync } from 'node:child_process'; import { fileURLToPath, pathToFileURL } from 'node:url'; const AICI = path.dirname(fileURLToPath(import.meta.url)); const DIR_POS = path.resolve(AICI, '..'); // tools/proof-of-software const RADACINA = path.resolve(AICI, '..', '..', '..'); // radacina depozitului in care sta actiunea export const API_IMPLICIT = 'https://cloud.aere.network/v1'; // Aceleasi intrari si iesiri ca in action.yml; test/action.test.mjs cere ca cele doua liste sa fie identice. export const INTRARI = { 'source-path': { required: true }, build: { default: 'npm-pack' }, 'signing-key': { secret: true }, 'aere-api-key': { secret: true }, 'verify-rebuild': { default: 'true' }, 'out-dir': {}, }; export const IESIRI = ['attestation-path', 'artifact-path', 'artifact-sha256', 'tree', 'statement-hash', 'notarized-tx', 'proof-url']; const CARLIGE = new Set(['alter-after-attest', 'foreign-artifact', 'npm-offline']); class Refuz extends Error {} export function intrare(env, nume) { if (!(nume in INTRARI)) throw new Error(`intrare nedeclarata: ${nume}`); const v = env[`INPUT_${nume.replace(/ /g, '_').toUpperCase()}`]; return v === undefined || v === '' ? (INTRARI[nume].default ?? '') : v; } // ---------------------------------------------------------------- secretele si curatarea textului const escRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); export class Secrete { constructor() { this.valori = new Set(); this.re = []; } // intoarce valorile NOI (de mascat la runner); sub 8 caractere nu se inregistreaza nimic (ar masca text obisnuit) adauga(x) { if (typeof x !== 'string') return []; const s = x.trim(); const noi = []; for (const v of /^0x[0-9a-fA-F]+$/.test(s) ? [s, s.slice(2)] : [s]) { if (v.length >= 8 && !this.valori.has(v)) { this.valori.add(v); noi.push(v); } } if (noi.length) this.re = [...this.valori].sort((a, b) => b.length - a.length).map((v) => new RegExp(escRe(v), 'gi')); return noi; } curata(text) { let t = String(text); for (const r of this.re) t = t.replace(r, '***'); return t; } } // orice sir hexa de 32+ caractere dintr-un mesaj de BIBLIOTECA (chei secp256k1/ed25519, seminte, bucati de chei ML-DSA) export const taieHex = (t) => String(t).replace(/(?:0x)?[0-9a-fA-F]{32,}/g, '[long hex removed]'); // ORDINEA conteaza: intai valorile secrete exacte (sec.curata), abia apoi sirurile hexa. Invers, taieHex ar rupe o valoare // secreta a carei coada e hexa, si prefixul ei ar scapa de potrivirea exacta. function mesajBib(e, sec = null) { const baza = String((e && e.message) || e); const cauza = e && e.cause && e.cause.message ? ` (${e.cause.message})` : ''; const t = sec ? sec.curata(baza + cauza) : baza + cauza; return taieHex(t).replace(/\s+/g, ' ').slice(0, 300); } // ---------------------------------------------------------------- protocolul runner-ului const escData = (s) => String(s).replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A'); // nume=valoare pe un rand; o valoare pe mai multe randuri primeste un delimitator care nu apare in ea export function linieIesire(nume, valoare) { const v = String(valoare); if (!/[\r\n]/.test(v)) return `${nume}=${v}\n`; let d; do { d = `ghadelimiter_${crypto.randomUUID()}`; } while (v.includes(d)); return `${nume}<<${d}\n${v}\n${d}\n`; } function booleanGitHub(v, nume) { if (/^(true|True|TRUE)$/.test(v)) return true; if (/^(false|False|FALSE)$/.test(v)) return false; throw new Refuz(`input ${nume} must be true or false`); } // Cheia de semnare: fisierul scris de `pos.mjs keygen` (JSON), sau base64 al lui. Nu tipareste nimic; motivul unui refuz // nu contine niciodata continutul. export function citesteCheia(brut) { const incearca = (t) => { try { const o = JSON.parse(t); return o && typeof o === 'object' ? o : null; } catch { return null; } }; let text = brut; let obj = incearca(text); if (!obj && !text.startsWith('{')) { const dec = Buffer.from(text, 'base64').toString('utf8').trim(); if (dec.startsWith('{')) { text = dec; obj = incearca(dec); } } if (!obj) return { ok: false, text, motiv: 'signing-key is not valid JSON (nor base64 of JSON); its content is not shown' }; const f = (...k) => k.reduce((a, x) => (a && typeof a === 'object' ? a[x] : undefined), obj); const lipsa = [['alg'], ['classical', 'secretKey'], ['classical', 'publicKey'], ['pq', 'secretKey'], ['pq', 'publicKey']].some((k) => typeof f(...k) !== 'string'); if (lipsa) return { ok: false, text, obj, motiv: 'signing-key is JSON but not a key file written by `pos.mjs keygen` (alg, classical.secretKey/publicKey, pq.secretKey/publicKey); its content is not shown' }; return { ok: true, text, keys: obj }; } const frunze = (o, out = []) => { if (typeof o === 'string') out.push(o); else if (o && typeof o === 'object') for (const v of Object.values(o)) frunze(v, out); return out; }; function bazaApi(env) { const v = String(env.AERE_POS_API_BASE || '').trim(); if (!v) return API_IMPLICIT; let u; try { u = new URL(v); } catch { throw new Refuz('AERE_POS_API_BASE is not a URL'); } const loopback = ['127.0.0.1', 'localhost', '[::1]'].includes(u.hostname) && /^https?:$/.test(u.protocol); if (!loopback) throw new Refuz(`AERE_POS_API_BASE may point only to a loopback address (it exists for local tests); the API key is sent only to ${API_IMPLICIT}`); return v.replace(/\/+$/, ''); } function carligeDeProba(env) { const v = String(env.AERE_POS_TEST_HOOK || '').trim(); if (!v) return new Set(); if (env.GITHUB_ACTIONS === 'true') throw new Refuz('AERE_POS_TEST_HOOK is a switch for the local tests and is refused on a GitHub runner'); const s = new Set(v.split(',').map((x) => x.trim()).filter(Boolean)); for (const x of s) if (!CARLIGE.has(x)) throw new Refuz(`unknown AERE_POS_TEST_HOOK "${x.slice(0, 40)}"`); return s; } function git(cwd, args) { try { return { ok: true, out: execFileSync('git', ['--literal-pathspecs', ...args], { cwd, stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 1 << 28 }).toString().replace(/\r?\n$/, '') }; } catch (e) { return { ok: false, out: '', err: String((e.stderr && e.stderr.toString()) || e.message || '').trim().split(/\r?\n/)[0] || 'git failed' }; } } // Dependintele lui pos.mjs (sdk-pq-sign -> @noble/*) nu sunt in depozit (node_modules e ignorat). Pe un runner, checkout-ul // actiunii nu le are, deci se instaleaza EXACT versiunile din package-lock.json (npm ci verifica integritatea sha512), fara // scripturi de instalare. Local, unde exista deja, nu se atinge nimic. function dependinte(carlige, jurnal, bib) { const dir = path.join(RADACINA, 'sdk-pq-sign'); const cere = ['@noble/post-quantum', '@noble/curves', '@noble/hashes']; const lipsa = () => cere.filter((p) => !fs.existsSync(path.join(dir, 'node_modules', ...p.split('/'), 'package.json'))); const inainte = lipsa(); if (!inainte.length) return 'present'; if (!fs.existsSync(path.join(dir, 'package-lock.json'))) throw new Refuz('the action checkout has no sdk-pq-sign/package-lock.json, so its pinned dependencies cannot be installed'); jurnal(`installing the pinned dependencies of sdk-pq-sign from its package-lock.json (missing: ${inainte.join(', ')})`); const cmd = 'npm ci --ignore-scripts --no-audit --no-fund' + (carlige.has('npm-offline') ? ' --offline' : ''); try { execSync(cmd, { cwd: dir, stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 1 << 26 }); } catch (e) { const coada = String(e.stderr || '').split(/\r?\n/).filter((l) => /npm (error|ERR)/.test(l)).slice(0, 2).join(' | '); throw new Refuz(`could not install the pinned dependencies of sdk-pq-sign (npm ci from its package-lock.json): ${bib(coada || 'npm ci failed').slice(0, 300)}`); } const dupa = lipsa(); if (dupa.length) throw new Refuz(`npm ci ran but ${dupa.join(', ')} is still missing`); return 'installed'; } const strica = (p) => { const b = fs.readFileSync(p); const i = Math.min(64, b.length - 1); b[i] ^= 0x01; fs.writeFileSync(p, b); }; const sha256File = (p) => '0x' + crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex'); // ---------------------------------------------------------------- actiunea export async function ruleaza(env = process.env) { const sec = new Secrete(); const scrie = (s) => process.stdout.write(s); const jurnal = (s) => scrie(sec.curata(s).split(/\r?\n/).map((l) => ' ' + l).join('\n') + '\n'); const masca = (v) => { for (const n of sec.adauga(v)) scrie(`::add-mask::${escData(n)}\n`); }; const sumar = (md) => { if (env.GITHUB_STEP_SUMMARY) fs.appendFileSync(env.GITHUB_STEP_SUMMARY, sec.curata(md)); }; const stare = { notarizare: null, atestare: null }; const bib = (x) => taieHex(sec.curata(x)); // text venit de la o biblioteca sau o unealta (git, npm, serverul) try { // 1. mastile. Parsarea cheii nu tipareste nimic, deci poate sta inaintea mastilor; orice refuz vine DUPA ele. const apiBrut = String(intrare(env, 'aere-api-key')).trim(); if (apiBrut) { masca(apiBrut); for (const p of apiBrut.split(/[.\s]+/)) masca(p); } const cheieBrut = String(intrare(env, 'signing-key')).trim(); let keys = null; if (cheieBrut) { const r = citesteCheia(cheieBrut); masca(cheieBrut); masca(r.text); if (r.ok) { keys = r.keys; for (const s of [keys.classical.secretKey, keys.pq.secretKey, keys.pq.seed]) masca(s); } else { // nu stim ce e secret intr-o cheie pe care nu o intelegem: se mascheaza fiecare rand si fiecare valoare for (const l of `${cheieBrut}\n${r.text}`.split(/\r?\n/)) masca(l); for (const v of frunze(r.obj)) masca(v); throw new Refuz(r.motiv); } } if (apiBrut && /[\u0000-\u001f\u007f]/.test(apiBrut)) throw new Refuz('aere-api-key contains a line break or a control character; nothing was sent'); // 2. intrarile obisnuite const build = String(intrare(env, 'build')).trim(); if (build !== 'npm-pack') throw new Refuz(`build "${build.slice(0, 40)}" is not supported: this version repeats only npm-pack builds (npm pack of the committed tree)`); const verifRebuild = booleanGitHub(String(intrare(env, 'verify-rebuild')).trim(), 'verify-rebuild'); const sursa = String(intrare(env, 'source-path')).trim(); if (!sursa) throw new Refuz('source-path is required: the directory of the npm package inside the repository'); const ws = path.resolve(env.GITHUB_WORKSPACE || process.cwd()); let sursaAbs = path.resolve(ws, sursa); const relWs = path.relative(ws, sursaAbs); if (relWs.startsWith('..') || path.isAbsolute(relWs)) throw new Refuz(`source-path "${sursa}" is outside the workspace`); if (!fs.existsSync(sursaAbs) || !fs.statSync(sursaAbs).isDirectory()) throw new Refuz(`source-path "${sursa}" is not a directory in the workspace`); sursaAbs = fs.realpathSync.native(sursaAbs); const apiBase = bazaApi(env); const carlige = carligeDeProba(env); if (carlige.size) jurnal(`TEST HOOK ACTIVE: ${[...carlige].join(', ')} (local tests only; this run does not produce an attestation to use)`); const tmpBaza = env.RUNNER_TEMP || os.tmpdir(); const outDir = path.resolve(ws, String(intrare(env, 'out-dir')).trim() || path.join(tmpBaza, 'aere-proof-of-software')); const relOut = path.relative(sursaAbs, outDir); if (!relOut.startsWith('..') && !path.isAbsolute(relOut)) throw new Refuz('out-dir is inside source-path; the artifact would then dirty the tree it is attested from'); // 3. dependintele si modulele (pos.mjs + clientul Cloud), prin cale relativa const dep = dependinte(carlige, jurnal, bib); let pos, AereCloud; try { pos = await import(pathToFileURL(path.join(DIR_POS, 'pos.mjs')).href); ({ AereCloud } = await import(pathToFileURL(path.join(RADACINA, 'sdk', 'index.mjs')).href)); } catch (e) { throw new Refuz(`could not load pos.mjs and the Aere Cloud client next to this action: ${mesajBib(e, sec)}`); } jurnal(`Aere Proof of Software (${pos.TOOL}), dependencies ${dep}`); // 4. sursa: arborele comis al dosarului, si nimic necomis in el const top = git(sursaAbs, ['rev-parse', '--show-toplevel']); if (!top.ok) throw new Refuz(`source-path "${sursa}" is not inside a git checkout (${bib(top.err).slice(0, 160)})`); const radGit = fs.realpathSync.native(path.resolve(top.out)); const commit = git(radGit, ['rev-parse', '--verify', 'HEAD^{commit}']); if (!commit.ok || !commit.out) throw new Refuz('the checkout has no commit at HEAD'); const rel = path.relative(radGit, sursaAbs).split(path.sep).join('/'); if (!rel) throw new Refuz('source-path is the repository root: this version attests a package in a SUBDIRECTORY of the repository (the statement records the git tree of that directory)'); if (rel.startsWith('..')) throw new Refuz(`source-path "${sursa}" is outside the repository that contains it`); const tree = git(radGit, ['rev-parse', '--verify', '--quiet', `${commit.out}:${rel}`]); if (!tree.ok || !tree.out) throw new Refuz(`source-path "${rel}" is not tracked at commit ${commit.out.slice(0, 12)}`); if (git(radGit, ['cat-file', '-t', tree.out]).out !== 'tree') throw new Refuz(`source-path "${rel}" is not a directory at commit ${commit.out.slice(0, 12)}`); const st = git(radGit, ['status', '--porcelain=v1', '--untracked-files=all', '--', rel]); if (!st.ok) throw new Refuz(`git status failed on source-path "${rel}": ${bib(st.err).slice(0, 160)}`); if (st.out.trim()) { const l = st.out.split('\n').filter(Boolean); throw new Refuz(`source-path "${rel}" has uncommitted or untracked files, and the attestation is of the COMMITTED tree; commit or remove them first:\n${l.slice(0, 10).join('\n')}${l.length > 10 ? `\n... and ${l.length - 10} more` : ''}`); } const pj = git(radGit, ['cat-file', 'blob', `${commit.out}:${rel}/package.json`]); let pkg = null; try { pkg = JSON.parse(pj.out); } catch { pkg = null; } if (!pj.ok || !pkg || typeof pkg.name !== 'string' || typeof pkg.version !== 'string') throw new Refuz(`source-path "${rel}" has no committed package.json with a name and a version`); jurnal(`source: ${rel} at commit ${commit.out}, git tree ${tree.out}; package ${pkg.name}@${pkg.version}`); // 5. artefactul, din arborele comis fs.mkdirSync(outDir, { recursive: true }); let tgz; try { tgz = pos.packNpmFromTree(radGit, `${commit.out}:${rel}`, outDir); } catch (e) { throw new Refuz(`npm pack of the committed tree failed: ${mesajBib(e, sec)}`); } if (carlige.has('foreign-artifact')) { strica(tgz); jurnal('TEST HOOK foreign-artifact: one byte of the artifact changed BEFORE attestation'); } // 6. atestarea let att; try { att = await pos.attest({ artifacts: [tgz], name: pkg.name, version: pkg.version, sourcePath: sursaAbs, build: 'npm-pack', keys, cwd: radGit }); } catch (e) { throw new Refuz(`${keys ? 'attestation or signing failed' : 'attestation failed'}: ${mesajBib(e, sec)}`); } const s = att.statement.source || {}; if (s.commit !== commit.out || s.tree !== tree.out || s.path !== rel || s.pathDirty !== false) { throw new Refuz(`the checkout changed during the run (the statement names ${String(s.commit).slice(0, 12)}:${s.path}, tree ${String(s.tree).slice(0, 12)}, pathDirty ${s.pathDirty}); nothing is attested`); } const art = att.statement.artifacts[0]; jurnal(`artifact: ${art.name}, ${art.bytes} bytes, sha256 ${art.sha256}`); jurnal(`signature: ${att.signature ? `hybrid ${att.signature.alg}, classical public key ${att.signature.classicalPublicKey}` : 'none (no signing-key given)'}`); jurnal(`statementHash ${att.statementHash}`); if (carlige.has('alter-after-attest')) { strica(tgz); jurnal('TEST HOOK alter-after-attest: one byte of the artifact changed AFTER attestation'); } // 7. verificarea, prin reconstructie dintr-o clona curata a commitului (--no-local: obiectele trec prin transportul git // si se re-hashuiesc; --no-checkout: nu se face copie de lucru, deci nicio conversie de sfarsit de rand) let clona = null; let r; try { if (verifRebuild) { clona = fs.mkdtempSync(path.join(tmpBaza, 'aere-pos-clone-')); const c = git(path.dirname(clona), ['clone', '--quiet', '--no-local', '--no-checkout', radGit, clona]); if (!c.ok) throw new Refuz(`could not make a clean clone of the checkout for the rebuild: ${bib(c.err).slice(0, 200)}`); } r = await pos.verify(att, [tgz], { rebuildFrom: clona }); } finally { if (clona) { try { fs.rmSync(clona, { recursive: true, force: true, maxRetries: 3 }); } catch { /* curatenie; nu schimba verdictul */ } } } for (const c of r.checks) jurnal(`${c.pass === true ? 'OK ' : c.pass === false ? 'FAIL' : '-- '} ${c.name}${c.detail ? ' (' + c.detail + ')' : ''}`); const picate = r.checks.filter((c) => c.pass === false); const linii = picate.map((c) => `FAIL ${c.name}${c.detail ? ' (' + c.detail + ')' : ''}`).join('\n'); if (picate.some((c) => /^(rebuild|source)/.test(c.name))) throw new Refuz(`the rebuild from a clean clone of ${commit.out.slice(0, 12)} does NOT reproduce the artifact byte for byte; nothing is notarized:\n${linii}`); if (!r.valid) throw new Refuz(`verification of the fresh attestation failed; nothing is notarized:\n${linii}`); const reconstruit = r.checks.some((c) => c.name.startsWith('rebuild: npm pack of the attested tree reproduces') && c.pass === true); if (verifRebuild && !reconstruit) throw new Refuz('the rebuild was not confirmed (no passing rebuild check); nothing is notarized'); // 8. atestarea pe disc (inca nenotarizata; daca notarizarea cade, fisierul spune singur "notarization": null) const attPath = path.join(outDir, path.basename(tgz).replace(/\.tgz$/, '') + '.attestation.json'); fs.writeFileSync(attPath, JSON.stringify(att, null, 1)); stare.atestare = attPath; // 9. notarizarea, numai cu cheie, numai dupa verificare let proofUrl = null; if (apiBrut) { let cod = null; const f = async (url, o) => { const x = await fetch(url, { ...o, redirect: 'error', signal: AbortSignal.timeout(120000) }); cod = x.status; return x; }; let d; try { d = await new AereCloud({ apiKey: apiBrut, baseUrl: apiBase, fetch: f }).notarize(att.statementHash); } catch (e) { if (cod !== null && cod !== 200) { const motiv = e && e.body && e.body.error ? ` (${bib(String(e.body.error)).replace(/\s+/g, ' ').slice(0, 120)})` : ''; throw new Refuz(`notarization refused: HTTP ${cod}${motiv}; the attestation is NOT notarized`); } throw new Refuz(`notarization request failed: ${mesajBib(e, sec)}; the attestation is NOT notarized`); } if (cod !== 200) throw new Refuz(`notarization answered HTTP ${cod}, not 200; the attestation is NOT notarized`); if (!d || !/^0x[0-9a-fA-F]{64}$/.test(String(d.txHash || '')) || !Number.isInteger(d.block) || d.block <= 0) throw new Refuz('notarization answered 200 without a transaction hash and a block; the attestation is NOT notarized'); if (d.hash !== undefined && String(d.hash).toLowerCase() !== att.statementHash.toLowerCase()) throw new Refuz('notarization answered for another hash than the statementHash; the attestation is NOT notarized'); // aceeasi forma ca pos.mjs attest(), ca `pos.mjs verify` sa citeasca chitanta (block, txHash) fara nicio diferenta att.notarization = { txHash: d.txHash, block: d.block, firstSeenAt: d.firstSeenAt ?? null, firstTime: d.firstTime ?? null, contract: d.contract ?? null, chainId: d.chainId ?? null, proof: '/v1/proof/' + att.statementHash }; proofUrl = `${apiBase}/proof/${att.statementHash}`; stare.notarizare = { tx: d.txHash, block: d.block }; fs.writeFileSync(attPath, JSON.stringify(att, null, 1)); jurnal(`notarized: tx ${d.txHash}, block ${d.block}${d.firstTime === false ? ' (this hash was already notarized earlier; first seen at ' + d.firstSeenAt + ')' : ''}; proof ${proofUrl}`); } else jurnal('notarization: skipped, no aere-api-key given; the attestation is NOT notarized'); // 10. ce se incarca e ce se verifica: atestarea recitita de pe disc const inapoi = JSON.parse(fs.readFileSync(attPath, 'utf8')); const r2 = await pos.verify(inapoi, [tgz]); if (!r2.valid || inapoi.statementHash !== att.statementHash) throw new Refuz('the attestation file read back from disk does not verify'); // 11. iesirile si rezumatul const iesiri = { 'attestation-path': attPath, 'artifact-path': tgz, 'artifact-sha256': art.sha256, tree: tree.out, 'statement-hash': att.statementHash }; if (stare.notarizare) { iesiri['notarized-tx'] = stare.notarizare.tx; iesiri['proof-url'] = proofUrl; } const text = Object.entries(iesiri).map(([k, v]) => linieIesire(k, sec.curata(v))).join(''); if (env.GITHUB_OUTPUT) fs.appendFileSync(env.GITHUB_OUTPUT, text); else jurnal(`outputs (GITHUB_OUTPUT is not set):\n${text.trim()}`); const relAtt = path.basename(attPath), relArt = path.basename(tgz); sumar([ '## Aere Proof of Software', '', '| | |', '|---|---|', `| package | \`${pkg.name}@${pkg.version}\` |`, `| artifact | \`${art.name}\`, ${art.bytes} bytes |`, `| artifact sha256 | \`${art.sha256}\` |`, `| source | commit \`${commit.out}\`, path \`${rel}\`, git tree \`${tree.out}\` |`, `| build | npm pack of the committed tree (npm ${att.statement.build && att.statement.build.npm}) |`, `| signature | ${att.signature ? `hybrid \`${att.signature.alg}\`, classical public key \`${att.signature.classicalPublicKey}\`` : '**unsigned**: no signing-key given'} |`, `| rebuild | ${verifRebuild ? 'reproduced byte for byte from a clean clone of the commit' : '**NOT verified**: verify-rebuild is false'} |`, `| notarization | ${stare.notarizare ? `tx \`${stare.notarizare.tx}\` in block ${stare.notarizare.block}; proof: ${proofUrl}` : '**NOT notarized**: no aere-api-key given, so the attestation carries no time on chain'} |`, `| statementHash | \`${att.statementHash}\` |`, '', 'Anyone can check it without trusting this run, from their own clone of the repository:', '', '```', `node tools/proof-of-software/pos.mjs verify ${relAtt} --rebuild-from ${relArt}`, '```', '', ].join('\n')); return 0; } catch (e) { const msg = sec.curata(e instanceof Refuz ? e.message : `unexpected error: ${mesajBib(e, sec)}`); scrie(`::error::${escData(msg)}\n`); process.stderr.write(msg + '\n'); sumar([ '## Aere Proof of Software: FAILED', '', '```', msg, '```', '', stare.notarizare ? `Notarized before the failure: tx \`${stare.notarizare.tx}\`, block ${stare.notarizare.block}.` : 'The attestation is NOT notarized.', stare.atestare ? `An attestation file was written before the failure (\`${path.basename(stare.atestare)}\`); this run did not complete, so do not publish it.` : '', '', ].join('\n')); process.exitCode = 1; return 1; } } if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) ruleaza().catch((e) => { process.stderr.write(`unexpected error: ${mesajBib(e)}\n`); process.exitCode = 1; });