import fs from "node:fs/promises"; import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; const require = createRequire(import.meta.url); const a2spa = require("a2spa"); const { buildPayloadFields, buildSignedRequest, policyInput, requireAuthorizedResult } = a2spa; export const canonicalizeA2spaPayload = a2spa.canonicalizeA2spaPayload; export const computePayloadHash = a2spa.computePayloadHash; export const prepareSignedBytes = a2spa.prepareSignedBytes; export const finalizePayload = a2spa.finalizePayload; export const buildRequestBody = a2spa.buildRequestBody; export const signPayload = a2spa.signPayload; export { buildPayloadFields, buildSignedRequest, policyInput, requireAuthorizedResult }; // Install with: npm install a2spa function requireEnv(name) { const value = process.env[name]; if (!value) { throw new Error(`Missing required environment variable: ${name}`); } return value; } export class A2SPAClient { constructor({ apiBase, apiKey, agentId, privateKeyPath, timeoutMs = 20000 }) { this.apiBase = apiBase.replace(/\/$/, ""); this.apiKey = apiKey; this.agentId = agentId; this.privateKeyPath = privateKeyPath; this.timeoutMs = timeoutMs; this.privateKeyPem = null; } static fromEnv() { return new A2SPAClient({ apiBase: process.env.A2SPA_API_BASE || "https://api.aimodularity.com/A2SPA", apiKey: requireEnv("A2SPA_API_KEY"), agentId: requireEnv("A2SPA_AGENT_ID"), privateKeyPath: requireEnv("A2SPA_PRIVATE_KEY_PATH"), }); } async loadPrivateKeyPem() { if (!this.privateKeyPem) { this.privateKeyPem = await fs.readFile(this.privateKeyPath, "utf8"); } return this.privateKeyPem; } async buildSignedRequest(targetAgentId, input, output = { status: "ready" }, options = {}) { const payload = buildPayloadFields({ agentId: this.agentId, targetAgentId, input, output, alertThreshold: options.alertThreshold ?? 10, stateContinuity: options.stateContinuity ?? null, }); return buildSignedRequest(payload, await this.loadPrivateKeyPem()); } async postRequestBody(requestBody) { return fetch(`${this.apiBase}/api/verify_payload`, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": this.apiKey, }, body: JSON.stringify(requestBody), }); } async sendPayload(targetAgentId, input, output = { status: "ready" }, options = {}, debug = false) { const requestBody = await this.buildSignedRequest(targetAgentId, input, output, options); const response = await this.postRequestBody(requestBody); const result = await response.json().catch(() => ({})); if (!response.ok) { if (debug) { console.error(JSON.stringify({ status: response.status, error: result.error, reason_code: result.reason_code }, null, 2)); } throw new Error(result.reason_code || result.error || `A2SPA did not authorize delivery (${response.status})`); } return requireAuthorizedResult(result); } async fetchLogs(agentId = this.agentId) { const params = new URLSearchParams({ agent_id: agentId }); const response = await fetch(`${this.apiBase}/api/logs_for_agent?${params.toString()}`, { headers: { "x-api-key": this.apiKey }, }); if (!response.ok) { throw new Error(await response.text()); } const body = await response.json(); return body.logs || []; } async fetchInbox(agentId = this.agentId, after = "", limit = 50) { const params = new URLSearchParams({ agent_id: agentId, limit: String(limit) }); if (after) params.set("after", after); const response = await fetch(`${this.apiBase}/api/inbox_for_agent?${params.toString()}`, { headers: { "x-api-key": this.apiKey }, }); if (!response.ok) { throw new Error(await response.text()); } const body = await response.json(); return body.messages || []; } async prepareAndSend(targetAgentId, input, runner, options = {}) { // The runner must only prepare output. Do not perform side effects before A2SPA authorization. const output = await runner(input); return this.sendPayload(targetAgentId, input, output, options); } } async function existingAgent(input) { return { status: "ready", summary: `Processed: ${input.message ?? "no message"}`, }; } async function main() { const client = A2SPAClient.fromEnv(); const input = policyInput({ action: "send_message", workflowScope: "messages:send", message: "Hello from my existing Node agent" }); const output = await existingAgent(input); const requestBody = await client.buildSignedRequest(process.env.A2SPA_TARGET_AGENT_ID, input, output); const response = await client.postRequestBody(requestBody); console.log(`A2SPA verify response: ${response.status} ${await response.text()}`); } if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { main().catch((error) => { console.error(error); process.exit(1); }); }