Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/components/credentials/AsqavApi.credential.ts
Original file line number Diff line number Diff line change
@@ -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 }
162 changes: 162 additions & 0 deletions packages/components/nodes/tools/AsqavSignAction/AsqavSignAction.ts
Original file line number Diff line number Diff line change
@@ -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<string | ICommonObject> {
const credentialData = await getCredentialData(nodeData.credential ?? '', options ?? {})
const apiKey = getCredentialParam('asqavApiKey', credentialData, nodeData)
Comment on lines +70 to +71

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Validate that the apiKey is present before proceeding. If it is missing, throw an error early to provide a clear and helpful error message to the user.

        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.')
        }
References
  1. When a feature requires a specific configuration (e.g., an API key), it is preferable to throw an error if the configuration is missing rather than silently falling back to a different implementation.

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<string, unknown> = {}
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.')
}
Comment on lines +87 to +91

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use parseJsonBody instead of JSON.parse. parseJsonBody is a robust utility in Flowise that handles common JSON formatting issues such as trailing commas, single quotes, and comments, providing a much better user experience.

                    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<string, unknown>
}
}

const baseUrl = ((nodeData.inputs?.baseUrl as string) || DEFAULT_BASE_URL).replace(/\/+$/, '')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Since baseUrl is a user-configurable input, an attacker could supply an internal IP address or hostname (e.g., http://127.0.0.1 or AWS metadata endpoints) to perform a Server-Side Request Forgery (SSRF) attack. Call await checkDenyList(baseUrl) to validate the URL against Flowise's security policy before making any network requests.

        const baseUrl = ((nodeData.inputs?.baseUrl as string) || DEFAULT_BASE_URL).replace(/\n+$/, '')
        await checkDenyList(baseUrl)

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 }
4 changes: 4 additions & 0 deletions packages/components/nodes/tools/AsqavSignAction/asqav.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.