import fs from "node:fs/promises"; import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; import type { A2spaPayloadContent, JsonValue } from "a2spa"; const require = createRequire(import.meta.url); const a2spa = require("a2spa") as typeof import("a2spa"); export const buildPayloadFields = a2spa.buildPayloadFields; export const buildSignedRequest = a2spa.buildSignedRequest; export const policyInput = a2spa.policyInput; 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; interface SendResult { success?: boolean; decision?: string; reason_code?: string; delivery?: { status?: string }; error?: string; receipt?: unknown; [key: string]: unknown; } interface LogEntry { action?: string; [key: string]: unknown; } // Install with: npm install a2spa function requireEnv(name: string): string { const value = process.env[name]; if (!value) { throw new Error(`Missing required environment variable: ${name}`); } return value; } export class A2SPAClient { private apiBase: string; private apiKey: string; private agentId: string; private privateKeyPath: string; private privateKeyPem?: string; constructor(options: { apiBase: string; apiKey: string; agentId: string; privateKeyPath: string }) { this.apiBase = options.apiBase.replace(/\/$/, ""); this.apiKey = options.apiKey; this.agentId = options.agentId; this.privateKeyPath = options.privateKeyPath; } static fromEnv(): A2SPAClient { 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"), }); } private async loadPrivateKeyPem(): Promise { if (!this.privateKeyPem) { this.privateKeyPem = await fs.readFile(this.privateKeyPath, "utf8"); } return this.privateKeyPem; } async buildSignedRequest( targetAgentId: string, input: A2spaPayloadContent, output: A2spaPayloadContent = { status: "ready" }, options: { alertThreshold?: number; stateContinuity?: JsonValue | null } = {}, ): Promise> { 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: ReturnType): Promise { 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: string, input: A2spaPayloadContent, output: A2spaPayloadContent = { status: "ready" }, options: { alertThreshold?: number; stateContinuity?: JsonValue | null } = {}, debug = false, ): Promise { const requestBody = await this.buildSignedRequest(targetAgentId, input, output, options); const response = await this.postRequestBody(requestBody); const body = (await response.json().catch(() => ({}))) as SendResult; if (!response.ok) { if (debug) { console.error(JSON.stringify({ status: response.status, error: body.error, reason_code: body.reason_code }, null, 2)); } throw new Error(body.reason_code || body.error || `A2SPA did not authorize delivery (${response.status})`); } return requireAuthorizedResult(body) as SendResult; } async fetchLogs(agentId = this.agentId): Promise { 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()) as { logs?: LogEntry[] }; return body.logs || []; } async fetchInbox(agentId = this.agentId, after = "", limit = 50): Promise { 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()) as { messages?: LogEntry[] }; return body.messages || []; } async prepareAndSend( targetAgentId: string, input: A2spaPayloadContent, runner: (input: A2spaPayloadContent) => Promise | A2spaPayloadContent, options: { alertThreshold?: number; stateContinuity?: JsonValue | null } = {}, ): Promise { // 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: A2spaPayloadContent): Promise { const message = typeof input === "object" && input && !Array.isArray(input) ? (input as { message?: string }).message : undefined; return { status: "ready", summary: `Processed: ${message ?? "no message"}`, }; } async function main(): Promise { const client = A2SPAClient.fromEnv(); const input = policyInput({ action: "send_message", workflowScope: "messages:send", message: "Hello from my existing TypeScript 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); }); }