From 99543909be46eb7f9be511b06582d275a6842cd3 Mon Sep 17 00:00:00 2001 From: jagmarques <32335502+jagmarques@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:33:56 +0200 Subject: [PATCH 1/2] Add Asqav Sign Action tool node --- .../credentials/AsqavApi.credential.ts | 26 ++++ .../tools/AsqavSignAction/AsqavSignAction.ts | 145 ++++++++++++++++++ .../nodes/tools/AsqavSignAction/asqav.svg | 4 + 3 files changed, 175 insertions(+) create mode 100644 packages/components/credentials/AsqavApi.credential.ts create mode 100644 packages/components/nodes/tools/AsqavSignAction/AsqavSignAction.ts create mode 100644 packages/components/nodes/tools/AsqavSignAction/asqav.svg 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..5df14e4a305 --- /dev/null +++ b/packages/components/nodes/tools/AsqavSignAction/AsqavSignAction.ts @@ -0,0 +1,145 @@ +import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' +import { getCredentialData, getCredentialParam } from '../../../src/utils' + +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: '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) + + 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 = JSON.parse(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(/\/+$/, '') + const headers = { + 'X-API-Key': apiKey, + 'Content-Type': 'application/json' + } + + 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 } + + const signResponse = await fetch(`${baseUrl}/agents/${agent.agent_id}/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 @@ + + + + From 2f217ba14701ad3e66f5155faac78aea0ef27b6a Mon Sep 17 00:00:00 2001 From: jagmarques <32335502+jagmarques@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:58:47 +0200 Subject: [PATCH 2/2] Harden Asqav Sign Action node per Gemini review - Route baseUrl through checkDenyList before any outbound request (SSRF guard) - Require the API key up front and fail fast with a clear message - Parse the Context input with the parseJsonBody helper - Add an optional Agent ID input to reuse an existing agent and skip creation --- .../tools/AsqavSignAction/AsqavSignAction.ts | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/packages/components/nodes/tools/AsqavSignAction/AsqavSignAction.ts b/packages/components/nodes/tools/AsqavSignAction/AsqavSignAction.ts index 5df14e4a305..c2d9acd261d 100644 --- a/packages/components/nodes/tools/AsqavSignAction/AsqavSignAction.ts +++ b/packages/components/nodes/tools/AsqavSignAction/AsqavSignAction.ts @@ -1,5 +1,6 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' -import { getCredentialData, getCredentialParam } from '../../../src/utils' +import { getCredentialData, getCredentialParam, parseJsonBody } from '../../../src/utils' +import { checkDenyList } from '../../../src/httpSecurity' const DEFAULT_BASE_URL = 'https://api.asqav.com/api/v1' @@ -46,6 +47,14 @@ class AsqavSignAction_Tools implements INode { 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', @@ -60,6 +69,9 @@ class AsqavSignAction_Tools implements INode { 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) { @@ -73,7 +85,7 @@ class AsqavSignAction_Tools implements INode { const trimmed = rawContext.trim() if (trimmed.length) { try { - context = JSON.parse(trimmed) + context = parseJsonBody(trimmed) } catch { throw new Error('Asqav Sign Action: "Context" must be valid JSON.') } @@ -84,29 +96,34 @@ class AsqavSignAction_Tools implements INode { } const baseUrl = ((nodeData.inputs?.baseUrl as string) || DEFAULT_BASE_URL).replace(/\/+$/, '') + await checkDenyList(baseUrl) const headers = { 'X-API-Key': apiKey, 'Content-Type': 'application/json' } - const agentResponse = await fetch(`${baseUrl}/agents/create`, { - method: 'POST', - headers, - body: JSON.stringify({ - name: 'flowise', - algorithm: 'ml-dsa-65', - capabilities: [] + 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}` : ''}` - ) + 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 agent = (await agentResponse.json()) as { agent_id: string } - const signResponse = await fetch(`${baseUrl}/agents/${agent.agent_id}/sign`, { + const signResponse = await fetch(`${baseUrl}/agents/${agentId}/sign`, { method: 'POST', headers, body: JSON.stringify({