The published repository served the 0.3.0 line from May while the install page already pointed here. This carries the uncommitted line: 35+ typed contract clients over the canonical address book, the compliance surface, and the hybrid PQC wallet helpers.
95 lines
4.1 KiB
JavaScript
95 lines
4.1 KiB
JavaScript
// agent-402-pqc-client.js
|
|
//
|
|
// Drop-in example: agent-side AERE402FacilitatorPQC client. The agent's spending
|
|
// authority is rooted in a quantum-durable Falcon key (an AereAgentDID); each payment
|
|
// is authorized by a cheap, short-lived, revocable secp256k1 SESSION key. Rotating or
|
|
// revoking the Falcon root instantly halts every downstream payment.
|
|
//
|
|
// The agent calls a paid endpoint, receives 402 + a PQC quote, signs the DID action
|
|
// digest with its SESSION key (a single ecrecover on-chain), and retries. The Falcon
|
|
// root is never touched on the payment hot path — it only ever issues sessions.
|
|
//
|
|
// Run:
|
|
// yarn add ethers
|
|
// SESSION_PRIVATE_KEY=0x... ENDPOINT=http://localhost:3000/v1/inference node agent-402-pqc-client.js
|
|
//
|
|
// Prereq: an AereAgentDID agent (Falcon root) exists, a session is issued for
|
|
// SESSION_PRIVATE_KEY's address (with scope + spend cap + expiry), and the agent's
|
|
// AERE402FacilitatorPQC vault is funded. The server issues the 402 quote; the agent only
|
|
// needs its session key here.
|
|
|
|
const { ethers } = require('ethers');
|
|
|
|
const SESSION_PK = process.env.SESSION_PRIVATE_KEY;
|
|
const ENDPOINT = process.env.ENDPOINT ?? 'http://localhost:3000/v1/inference';
|
|
|
|
if (!SESSION_PK) {
|
|
console.error('SESSION_PRIVATE_KEY env required');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Domain tags — mirror the contracts exactly.
|
|
const PAYMENT_DOMAIN = ethers.keccak256(ethers.toUtf8Bytes('AERE402FacilitatorPQC.v1.payment'));
|
|
const ACTION_DOMAIN = ethers.keccak256(ethers.toUtf8Bytes('AereAgentDID.v1.action'));
|
|
|
|
// keccak256(abi.encode(PAYMENT_DOMAIN, chainId, facilitator, token, payee, resourceId, deadline))
|
|
function paymentActionHash(q) {
|
|
const enc = ethers.AbiCoder.defaultAbiCoder().encode(
|
|
['bytes32', 'uint256', 'address', 'address', 'address', 'bytes32', 'uint256'],
|
|
[PAYMENT_DOMAIN, q.chainId, q.facilitator, q.token, q.payee, q.resourceId, BigInt(q.deadline)],
|
|
);
|
|
return ethers.keccak256(enc);
|
|
}
|
|
|
|
// keccak256(abi.encode(ACTION_DOMAIN, chainId, did, sessionId, scope, actionHash, amount, actionNonce))
|
|
function actionDigest(q, actionHash) {
|
|
const enc = ethers.AbiCoder.defaultAbiCoder().encode(
|
|
['bytes32', 'uint256', 'address', 'uint256', 'bytes32', 'bytes32', 'uint256', 'uint64'],
|
|
[ACTION_DOMAIN, q.chainId, q.did, BigInt(q.sessionId), q.scope, actionHash, BigInt(q.amount), BigInt(q.actionNonce)],
|
|
);
|
|
return ethers.keccak256(enc);
|
|
}
|
|
|
|
const session = new ethers.SigningKey(SESSION_PK);
|
|
|
|
(async () => {
|
|
console.log(`session ${ethers.computeAddress(session)} calling ${ENDPOINT}...`);
|
|
|
|
// 1. Unpaid request.
|
|
const r1 = await fetch(ENDPOINT);
|
|
if (r1.status !== 402) {
|
|
console.log(`unexpected status ${r1.status} on first call`);
|
|
console.log(await r1.text());
|
|
return;
|
|
}
|
|
const quoteJson = r1.headers.get('x-aere402pqc-quote');
|
|
if (!quoteJson) throw new Error('server did not return X-AERE402PQC-Quote');
|
|
const quote = JSON.parse(quoteJson);
|
|
// quote = { chainId, facilitator, did, sessionId, scope, token, payee, amount,
|
|
// resourceId, deadline, actionNonce }
|
|
console.log(' received quote:', quote);
|
|
|
|
// 2. Derive the actionHash (binds the payment to THIS facilitator + terms) and the DID
|
|
// action digest the session key must sign.
|
|
const actionHash = paymentActionHash(quote);
|
|
const digest = actionDigest(quote, actionHash);
|
|
|
|
// 3. Sign the raw digest with the SESSION key (canonical low-s; matches on-chain ecrecover).
|
|
const signature = session.sign(digest).serialized;
|
|
console.log(` signed: ${signature.slice(0, 18)}...`);
|
|
|
|
// 4. Retry with the signature. The provider POSTs
|
|
// settle(sessionId, scope, token, payee, amount, resourceId, deadline, signature)
|
|
// to AERE402FacilitatorPQC, which enforces the Falcon root lifecycle + scope + spend
|
|
// cap + this session signature + anti-replay in one call, then pays out.
|
|
const r2 = await fetch(ENDPOINT, {
|
|
headers: {
|
|
'X-AERE402PQC-Sig': signature,
|
|
'X-AERE402PQC-Quote': quoteJson,
|
|
},
|
|
});
|
|
console.log(` retry status: ${r2.status}`);
|
|
console.log(` tx hash: ${r2.headers.get('x-aere402pqc-txhash')}`);
|
|
console.log(' body:', await r2.json());
|
|
})();
|