diff --git a/packages/components/credentials/AsqavApi.credential.ts b/packages/components/credentials/AsqavApi.credential.ts new file mode 100644 index 00000000000..6d91ce1f047 --- /dev/null +++ b/packages/components/credentials/AsqavApi.credential.ts @@ -0,0 +1,26 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class AsqavApi implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'Asqav API' + this.name = 'asqavApi' + this.version = 1.0 + this.description = + 'API key for Asqav. Issued in the Asqav dashboard. Used to sign agent actions and obtain cryptographic compliance receipts.' + this.inputs = [ + { + label: 'Asqav API Key', + name: 'asqavApiKey', + type: 'password' + } + ] + } +} + +module.exports = { credClass: AsqavApi } diff --git a/packages/components/nodes/tools/AsqavSignAction/AsqavSignAction.ts b/packages/components/nodes/tools/AsqavSignAction/AsqavSignAction.ts new file mode 100644 index 00000000000..c2d9acd261d --- /dev/null +++ b/packages/components/nodes/tools/AsqavSignAction/AsqavSignAction.ts @@ -0,0 +1,162 @@ +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getCredentialData, getCredentialParam, parseJsonBody } from '../../../src/utils' +import { checkDenyList } from '../../../src/httpSecurity' + +const DEFAULT_BASE_URL = 'https://api.asqav.com/api/v1' + +class AsqavSignAction_Tools implements INode { + label: string + name: string + version: number + type: string + icon: string + category: string + description: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Asqav Sign Action' + this.name = 'asqavSignAction' + this.version = 1.0 + this.type = 'AsqavSignAction' + this.icon = 'asqav.svg' + this.category = 'Tools' + this.description = + 'Sign an agent action with Asqav and return the cryptographic compliance receipt (signature id, verification URL, timestamp)' + this.baseClasses = [this.type] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['asqavApi'] + } + this.inputs = [ + { + label: 'Action Type', + name: 'actionType', + type: 'string', + placeholder: 'api:call', + description: 'The type of action being signed, e.g. "api:call" or "tool:invoke"' + }, + { + label: 'Context', + name: 'context', + type: 'json', + optional: true, + description: 'Optional JSON metadata describing the action. Only the values you pass are hashed into the receipt.' + }, + { + label: 'Agent ID', + name: 'agentId', + type: 'string', + optional: true, + description: + 'Optional existing Asqav agent id to sign with. When omitted, a fresh ephemeral agent is created for the run.' + }, + { + label: 'Base URL', + name: 'baseUrl', + type: 'string', + optional: true, + placeholder: 'https://api.asqav.com/api/v1', + description: 'Asqav API base URL. Defaults to the production endpoint. Override for a self-hosted Asqav instance.' + } + ] + } + + async run(nodeData: INodeData, _input: string, options?: ICommonObject): Promise { + const credentialData = await getCredentialData(nodeData.credential ?? '', options ?? {}) + const apiKey = getCredentialParam('asqavApiKey', credentialData, nodeData) + if (!apiKey) { + throw new Error('Asqav Sign Action: API Key is required. Please configure the Asqav API credential.') + } + + const actionType = (nodeData.inputs?.actionType as string) ?? '' + if (!actionType) { + throw new Error('Asqav Sign Action: "Action Type" is required.') + } + + let context: Record = {} + const rawContext = nodeData.inputs?.context + if (rawContext) { + if (typeof rawContext === 'string') { + const trimmed = rawContext.trim() + if (trimmed.length) { + try { + context = parseJsonBody(trimmed) + } catch { + throw new Error('Asqav Sign Action: "Context" must be valid JSON.') + } + } + } else if (typeof rawContext === 'object') { + context = rawContext as Record + } + } + + const baseUrl = ((nodeData.inputs?.baseUrl as string) || DEFAULT_BASE_URL).replace(/\/+$/, '') + await checkDenyList(baseUrl) + const headers = { + 'X-API-Key': apiKey, + 'Content-Type': 'application/json' + } + + let agentId = ((nodeData.inputs?.agentId as string) ?? '').trim() + if (!agentId) { + const agentResponse = await fetch(`${baseUrl}/agents/create`, { + method: 'POST', + headers, + body: JSON.stringify({ + name: 'flowise', + algorithm: 'ml-dsa-65', + capabilities: [] + }) + }) + if (!agentResponse.ok) { + const detail = await agentResponse.text().catch(() => '') + throw new Error( + `Asqav Sign Action: agent create failed (${agentResponse.status} ${agentResponse.statusText})${detail ? `: ${detail}` : ''}` + ) + } + const agent = (await agentResponse.json()) as { agent_id: string } + agentId = agent.agent_id + } + + const signResponse = await fetch(`${baseUrl}/agents/${agentId}/sign`, { + method: 'POST', + headers, + body: JSON.stringify({ + action_type: actionType, + context, + session_id: null, + compliance_mode: true + }) + }) + if (!signResponse.ok) { + const detail = await signResponse.text().catch(() => '') + throw new Error( + `Asqav Sign Action: sign failed (${signResponse.status} ${signResponse.statusText})${detail ? `: ${detail}` : ''}` + ) + } + const receipt = (await signResponse.json()) as { + signature_id: string + action_id: string + verification_url: string + timestamp: string + algorithm: string + [key: string]: unknown + } + + return { + signatureId: receipt.signature_id, + actionId: receipt.action_id, + verificationUrl: receipt.verification_url, + timestamp: receipt.timestamp, + algorithm: receipt.algorithm, + receipt + } + } +} + +module.exports = { nodeClass: AsqavSignAction_Tools } diff --git a/packages/components/nodes/tools/AsqavSignAction/asqav.svg b/packages/components/nodes/tools/AsqavSignAction/asqav.svg new file mode 100644 index 00000000000..b80b825f0ec --- /dev/null +++ b/packages/components/nodes/tools/AsqavSignAction/asqav.svg @@ -0,0 +1,4 @@ + + + +