// Proba portofelului de plati x402 al agentilor (wallet.mjs + client.mjs + resource-server.mjs), fara retea: un facilitator LOCAL cu // aceleasi verificari ca cel de pe testnet (facilitator-local.mjs), semnaturi EIP-712 reale, chei ML-DSA-65 reale. Adversarul are cheia // AGENTULUI (isi poate scrie si re-semna registrul oricum), nu cheia portofelului si nici cheile oamenilor. Fiecare afirmatie cu perechea // ei negativa. // node proba-wallet.mjs iesire 0 = toate cum trebuia import crypto from 'node:crypto'; import { definePolicy } from '../agent-policy.mjs'; import { newAgentIdentity, openLedger, resumeLedger, verifyEquivocation, canonical } from '../agent-ledger.mjs'; import { newHumanIdentity, approve, revoke } from '../agent-aprobare.mjs'; import { createWallet, serve, x402Action, assetId, nonceForEntry, incarcaEthers } from './wallet.mjs'; import { payWithAgent, walletOverHttp, dinB64json } from './client.mjs'; import { createResourceServer } from './resource-server.mjs'; import { createLocalFacilitator } from './facilitator-local.mjs'; const ethers = incarcaEthers(); let ok = 0, rau = 0; const cer = (c, ce) => { console.log((c ? ' OK ' : ' RAU ') + ce); c ? ok++ : rau++; }; const arunca = (fn) => { try { fn(); return null; } catch (e) { return e.message; } }; const NET = 'eip155:28001'; const TOKEN = { address: '0x8215bA247a3574af8EBC36606eB437811E318FBd', name: 'AereTestUSD', version: '2' }; const ASSET = assetId(NET, TOKEN.address); const cerinta = (payTo, amount = '10000') => ({ scheme: 'exact', network: NET, amount, asset: TOKEN.address, payTo, maxTimeoutSeconds: 60, extra: { name: TOKEN.name, version: TOKEN.version } }); const acum = () => Math.floor(Date.now() / 1000); // o intrare scrisa si semnata de mana cu cheia agentului (adversarul ocoleste biblioteca registrului) function intrareFalsa(exp, identity, body) { const r = JSON.parse(JSON.stringify(exp)); const seq = r.entries.length; const prev = seq ? r.entries[seq - 1].hash : '0'.repeat(64); const b = { v: 2, agentId: r.agentId, policyHash: r.policyHash, session: r.session, provenance: null, ...body, seq }; const signature = crypto.sign(null, Buffer.from(`${seq}|${prev}|${canonical(b)}`, 'utf8'), identity.privateKey).toString('base64'); r.entries.push({ seq, prev, body: b, signature, hash: crypto.createHash('sha256').update(`${seq}|${prev}|${canonical(b)}|${signature}`).digest('hex') }); return r; } // un agent cu portofelul lui: politica numeste portofelul, limita 50000 pe ora in activul x402 function agentCuPortofel({ limita = '50000', extra = {}, payee, state } = {}) { const agent = newAgentIdentity(); const cheie = ethers.Wallet.createRandom(); const { policy, policyHash } = definePolicy({ agentId: agent.agentId, spend: { amount: limita, windowSeconds: 3600, asset: ASSET }, recipients: payee ? [payee] : undefined, wallet: cheie.address, ...extra }); const wallet = createWallet({ policy, policyHash, evmPrivateKey: cheie.privateKey, network: NET, token: TOKEN, state }); return { agent, cheie, policy, policyHash, wallet }; } // serverele deschise se inchid in `finally`, si cand proba s-a oprit la jumatate: altfel procesul ramane viu dupa rezumat // (masurat: controlul negativ astepta 240 s pe o copie oprita la jumatate) const deschise = []; const inchide = (s) => { if (!s) return; try { s.closeAllConnections && s.closeAllConnections(); s.close(); } catch { /* deja inchis */ } }; // un pas care arunca (o plantare care strica un raspuns) inroseste proba cu motivul, nu o opreste inainte de rezumat try { const payee = ethers.Wallet.createRandom().address.toLowerCase(); const A = agentCuPortofel({ payee }); const fac = createLocalFacilitator({ network: NET, token: TOKEN, balances: { [A.cheie.address]: '1000000' } }); const facUrl = await fac.listen(); deschise.push(fac.server); const rs = createResourceServer({ requirement: cerinta(payee), facilitator: facUrl }); const url = await rs.listen(); deschise.push(rs.server); const L = openLedger({ identity: A.agent, policy: A.policy, policyHash: A.policyHash }); // 1. cinci cumparaturi platite prin facilitator, fiecare scrisa intai in registru const plati = []; for (let i = 0; i < 5; i++) plati.push(await payWithAgent({ url, ledger: L, wallet: A.wallet })); cer(plati.every((p) => p.paid && p.status === 200 && p.body === 'the paid content' && p.settlement.success), `1. cinci cumparaturi de 10000: platite si servite (${plati.map((p) => p.status).join(',')})`); cer(fac.balanceOf(payee) === 50000n && fac.balanceOf(A.cheie.address) === 950000n, `1. soldurile la facilitator: vanzatorul 50000, portofelul 950000 (${fac.balanceOf(payee)})`); cer(plati.every((p) => p.receipt.nonce === nonceForEntry(p.entry.hash) && fac.used.has(`${A.cheie.address.toLowerCase()}:${p.receipt.nonce}`)), '1. nonce-ul fiecarei autorizari EIP-3009 e sha256(hash-ul intrarii din registru), si chiar el e consumat'); cer(plati.every((p) => p.decision.statement.allowed === true && p.decision.statement.policyHash === A.policyHash), '1. fiecare plata vine cu plicul AIP-23 al deciziei, sub politica fixata'); // 2. a sasea depaseste limita: politica agentului o refuza, portofelul nici nu e chemat const p6 = await payWithAgent({ url, ledger: L, wallet: A.wallet }); cer(!p6.paid && /policy refused.*over the limit/.test(p6.reason) && fac.balanceOf(payee) === 50000n && A.wallet.status().signed === 5, `2. CONTROL: a sasea cumparatura depaseste limita: refuzata inainte de portofel, soldul neschimbat (${p6.reason})`); // 3. ATAC: agentul ocoleste biblioteca si scrie singur o plata "permisa" peste limita, re-semnata cu cheia lui const r0 = await fetch(url); const pr = dinB64json(r0.headers.get('payment-required')); const falsa = intrareFalsa(L.export(), A.agent, { at: acum(), action: { ...x402Action(A.cheie.address, pr.resource, pr.accepts[0]), at: acum() }, decision: { allowed: true, reason: 'under the limit' } }); const r3 = await A.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: falsa }); cer(!r3.ok && /false decision/.test(r3.error) && A.wallet.status().signed === 5, `3. ATAC: plata "permisa" peste limita, scrisa si semnata de agent -> portofelul re-ruleaza politica si refuza (${(r3.error || "").slice(0, 90)})`); // 4. ATAC: intrarea numeste alta cumparatura decat cea platita (alta suma / alt destinatar) const B = agentCuPortofel({ payee }); const LB = openLedger({ identity: B.agent, policy: B.policy, policyHash: B.policyHash }); LB.record({ ...x402Action(B.cheie.address, pr.resource, pr.accepts[0]), amount: '100' }); const r4 = await B.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: LB.export() }); cer(!r4.ok && /another amount/.test(r4.error), `4. ATAC: intrarea spune 100, cumparatura costa 10000 -> refuzat (${r4.error})`); const LB2 = openLedger({ identity: B.agent, policy: B.policy, policyHash: B.policyHash }); // o intrare curata, pentru controlul pozitiv LB2.record(x402Action(B.cheie.address, pr.resource, pr.accepts[0])); const r4b = await B.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: LB2.export() }); cer(r4b.ok, '4. CONTROL: aceeasi cumparatura, cu intrarea care o numeste exact -> semnata'); // 5. ATAC: acelasi registru trimis din nou (aceeasi ultima intrare) const r5 = await B.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: LB2.export() }); cer(!r5.ok && /already seen/.test(r5.error), `5. ATAC: aceeasi intrare a doua oara -> refuzat (${r5.error})`); // 6. ATAC: o ramura (registrul luat de la un prefix si continuat altfel) -> refuzat, cu dovada de echivocare LB2.record(x402Action(B.cheie.address, pr.resource, pr.accepts[0])); const r6a = await B.wallet.authorize({ requirements: pr.accepts[0], resource: pr.resource, ledger: LB2.export() }); const prefix = { ...LB2.export(), entries: LB2.export().entries.slice(0, 1) }; const ramura = resumeLedger({ identity: B.agent, policy: B.policy, ledger: prefix }); ramura.record(x402Action(B.cheie.address, { url: 'http://altundeva/x' }, pr.accepts[0])); const r6 = await B.wallet.authorize({ requirements: pr.accepts[0], resource: { url: 'http://altundeva/x' }, ledger: ramura.export() }); cer(r6a.ok && !r6.ok && /another branch/.test(r6.error), `6. ATAC: o ramura a registrului -> refuzata (${(r6.error || "").slice(0, 80)})`); cer(!!r6.equivocation && verifyEquivocation(r6.equivocation).ok, '6. si portofelul da dovada de echivocare, verificabila de oricine cu cheia publica a agentului'); // 7. politica schimbata: registrul nou incepe de la zero, portofelul isi aminteste ce a semnat const C = agentCuPortofel({ payee }); const LC = openLedger({ identity: C.agent, policy: C.policy, policyHash: C.policyHash }); let semnate = 0; for (let i = 0; i < 5; i++) { LC.record(x402Action(C.cheie.address, { url: 'http://r/' + i }, cerinta(payee))); if ((await C.wallet.authorize({ requirements: cerinta(payee), resource: { url: 'http://r/' + i }, ledger: LC.export() })).ok) semnate++; } const polB = definePolicy({ ...C.policy, tools: ['search'] }); const Cb = createWallet({ policy: polB.policy, policyHash: polB.policyHash, evmPrivateKey: C.cheie.privateKey, network: NET, token: TOKEN, state: C.wallet.exportState() }); const LCb = openLedger({ identity: C.agent, policy: polB.policy, policyHash: polB.policyHash }); const rc = LCb.record(x402Action(C.cheie.address, { url: 'http://r/nou' }, cerinta(payee))); const r7 = await Cb.authorize({ requirements: cerinta(payee), resource: { url: 'http://r/nou' }, ledger: LCb.export() }); cer(semnate === 5 && rc.allowed && !r7.ok && /by what this wallet has signed.*over the limit/.test(r7.error), `7. politica noua, registru nou (el permite: ${rc.allowed}), dar portofelul a semnat deja 50000 in ora asta -> refuzat (${(r7.error || '').slice(0, 70)})`); // 8. revocarea data portofelului de proprietar, pe care agentul nu o scrie in registru const O = newHumanIdentity(); const D = agentCuPortofel({ payee, extra: { owner: O.humanId } }); const LD = openLedger({ identity: D.agent, policy: D.policy, policyHash: D.policyHash }); LD.record(x402Action(D.cheie.address, { url: 'http://d/1' }, cerinta(payee))); const r8a = await D.wallet.authorize({ requirements: cerinta(payee), resource: { url: 'http://d/1' }, ledger: LD.export() }); cer(!D.wallet.addRevocation(revoke({ owner: newHumanIdentity(), agentId: D.agent.agentId, policyHash: D.policyHash, revokedAt: acum() - 5 })).ok, '8. CONTROL: o revocare semnata de altcineva decat proprietarul e refuzata de portofel'); cer(D.wallet.addRevocation(revoke({ owner: O, agentId: D.agent.agentId, policyHash: D.policyHash, revokedAt: acum() - 5 })).ok, '8. revocarea proprietarului e primita de portofel'); LD.record(x402Action(D.cheie.address, { url: 'http://d/2' }, cerinta(payee))); const r8 = await D.wallet.authorize({ requirements: cerinta(payee), resource: { url: 'http://d/2' }, ledger: LD.export() }); cer(r8a.ok && !r8.ok && /false decision.*revoked/.test(r8.error), `8. dupa revocare, plata urmatoare e refuzata desi agentul nu a scris revocarea (${(r8.error || '').slice(0, 80)})`); // 9. aprobarea umana: peste 20000 cere un om const H = newHumanIdentity(); const E = agentCuPortofel({ payee, limita: '100000', extra: { approval: { approvers: [H.humanId], threshold: 1, above: '20000' } } }); const LE = openLedger({ identity: E.agent, policy: E.policy, policyHash: E.policyHash }); const mare = cerinta(payee, '30000'); const fara = LE.record(x402Action(E.cheie.address, { url: 'http://e/1' }, mare)); const act = x402Action(E.cheie.address, { url: 'http://e/2' }, mare); const apr = approve({ human: H, agentId: E.agent.agentId, policyHash: E.policyHash, action: act, issuedAt: acum() - 5, expiresAt: acum() + 300 }); LE.record(act, { approvals: [apr] }); const r9 = await E.wallet.authorize({ requirements: mare, resource: { url: 'http://e/2' }, ledger: LE.export() }); cer(!fara.allowed && r9.ok, `9. 30000 fara aprobare: refuzat de politica; cu aprobarea omului numit: semnat (${fara.reason})`); // 10. ATAC: o plata antedatata (ceasul agentului dat inapoi) const F = agentCuPortofel({ payee }); const LF = openLedger({ identity: F.agent, policy: F.policy, policyHash: F.policyHash, now: () => acum() - 600 }); LF.record(x402Action(F.cheie.address, { url: 'http://f/1' }, cerinta(payee))); const r10 = await F.wallet.authorize({ requirements: cerinta(payee), resource: { url: 'http://f/1' }, ledger: LF.export() }); cer(!r10.ok && /not now/.test(r10.error), `10. ATAC: plata datata cu 10 minute in urma -> refuzata (${(r10.error || '').slice(0, 70)})`); // 11. serverul de resurse: plata pentru alta cerinta, si reluarea aceleiasi plati const alta = await fetch(url, { headers: { 'PAYMENT-SIGNATURE': Buffer.from(JSON.stringify({ ...r4b.paymentPayload, accepted: cerinta(payee, '1') })).toString('base64') } }); cer(alta.status === 402 && /another requirement/.test(await alta.text()), '11. CONTROL: o plata pentru alta cerinta decat a emis serverul -> 402'); fac.fund(B.cheie.address, '100000'); // portofelul lui B are acum sold la facilitator const reluare = await fetch(url, { headers: { 'PAYMENT-SIGNATURE': Buffer.from(JSON.stringify(r4b.paymentPayload)).toString('base64') } }); const reluare2 = await fetch(url, { headers: { 'PAYMENT-SIGNATURE': Buffer.from(JSON.stringify(r4b.paymentPayload)).toString('base64') } }); cer(reluare.status === 200 && reluare2.status === 402 && /nonce_already_used/.test(await reluare2.text()), '11. aceeasi plata de doua ori: prima data servita, a doua oara 402 (nonce folosit la facilitator)'); // 12. doua ramuri la aceeasi pozitie, trimise DEODATA: portofelul semneaza cel mult una const G = agentCuPortofel({ payee }); const LG1 = openLedger({ identity: G.agent, policy: G.policy, policyHash: G.policyHash }); const LG2 = openLedger({ identity: G.agent, policy: G.policy, policyHash: G.policyHash }); LG1.record(x402Action(G.cheie.address, { url: 'http://g/1' }, cerinta(payee, '50000'))); LG2.record(x402Action(G.cheie.address, { url: 'http://g/2' }, cerinta(payee, '50000'))); const [g1, g2] = await Promise.all([G.wallet.authorize({ requirements: cerinta(payee, '50000'), resource: { url: 'http://g/1' }, ledger: LG1.export() }), G.wallet.authorize({ requirements: cerinta(payee, '50000'), resource: { url: 'http://g/2' }, ledger: LG2.export() })]); cer([g1, g2].filter((x) => x.ok).length === 1 && G.wallet.status().signed === 1, `12. ATAC: doua ramuri de cate 50000 trimise deodata (limita 50000) -> exact una semnata (${[g1.ok, g2.ok]})`); // 13. portofelul prin HTTP, cap la cap const srv = await serve(A.wallet, { port: 0 }); deschise.push(srv); const W = walletOverHttp(`http://127.0.0.1:${srv.address().port}`); const st = await W.status(); const LA = resumeLedger({ identity: A.agent, policy: A.policy, ledger: L.export() }); const p13 = await payWithAgent({ url, ledger: LA, wallet: W }); cer(st.address === A.cheie.address && st.policyHash === A.policyHash && !p13.paid && /over the limit/.test(p13.reason), `13. prin HTTP: /status spune portofelul si politica; limita tine si dupa reluarea registrului (${(p13.reason || '').slice(0, 60)})`); const H2 = agentCuPortofel({ payee }); const srv2 = await serve(H2.wallet, { port: 0 }); deschise.push(srv2); const rs2 = createResourceServer({ requirement: cerinta(payee), facilitator: facUrl }); const url2 = await rs2.listen(); deschise.push(rs2.server); const LH = openLedger({ identity: H2.agent, policy: H2.policy, policyHash: H2.policyHash }); const p13b = await payWithAgent({ url: url2, ledger: LH, wallet: walletOverHttp(`http://127.0.0.1:${srv2.address().port}`) }); cer(p13b.status === 402 && /wallet refused|insufficient_funds|not valid/.test(p13b.reason || ''), `13. CONTROL: un portofel fara sold -> plata nu trece la facilitator (${(p13b.reason || '').slice(0, 70)})`); // 14. portofelul refuza sa porneasca pe o politica ce nu il numeste const alt = ethers.Wallet.createRandom(); cer(/does not name this wallet/.test(arunca(() => createWallet({ policy: A.policy, evmPrivateKey: alt.privateKey, network: NET, token: TOKEN })) || ''), '14. CONTROL: o politica ce numeste alt portofel -> portofelul nu porneste'); cer(/pinned policyHash/.test(arunca(() => createWallet({ policy: A.policy, policyHash: '0x' + '00'.repeat(32), evmPrivateKey: A.cheie.privateKey, network: NET, token: TOKEN })) || ''), '14. CONTROL: un hash fixat care nu e al politicii -> portofelul nu porneste'); const faraLimita = definePolicy({ agentId: A.agent.agentId, wallet: A.cheie.address }).policy; const altActiv = definePolicy({ agentId: A.agent.agentId, wallet: A.cheie.address, spend: { amount: '1', windowSeconds: 60 } }).policy; cer(/spending limit in this wallet's asset/.test(arunca(() => createWallet({ policy: faraLimita, evmPrivateKey: A.cheie.privateKey, network: NET, token: TOKEN })) || '') && /spending limit in this wallet's asset/.test(arunca(() => createWallet({ policy: altActiv, evmPrivateKey: A.cheie.privateKey, network: NET, token: TOKEN })) || ''), '14. CONTROL: o politica fara limita, sau cu limita in alt activ (AERE) -> portofelul nu porneste'); } catch (e) { cer(false, `proba s-a oprit: ${String(e.message || e).slice(0, 160)}`); } finally { for (const s of deschise) inchide(s); } console.log(`\nagent-wallet: ${ok}/${ok + rau} ${rau ? 'CU ESECURI' : 'cum trebuia'}`); process.exitCode = rau ? 1 : 0;