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
85 changes: 85 additions & 0 deletions examples/agent-frameworks/n8n/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# n8n APort Verification Node

This community node lets n8n workflows verify an APort passport against a policy pack and route each item into `Approved`, `Denied`, or `Error` outputs.

## Features

- Verifies a passport ID against any APort policy pack.
- Supports context from the incoming item JSON, a JSON object, or key/value fields.
- Routes workflow items by verification decision instead of requiring a separate IF node.
- Uses n8n credentials for the APort API key and base URL.
- Includes a sample workflow and unit tests for routing behavior.

## Installation

After the package is published, install it from an n8n instance that allows community nodes:

```bash
npm install n8n-nodes-aport
```

For local development from this repository:

```bash
cd examples/agent-frameworks/n8n
npm test
```

The package has no runtime dependencies; the tests use Node.js' built-in test runner. Link or copy the package into your n8n custom nodes directory according to your n8n deployment model.

To inspect the publishable package contents locally:

```bash
npm pack --dry-run
```

## Credentials

Create an **APort API** credential in n8n:

- `API Key`: optional bearer token for authenticated APort endpoints.
- `Base URL`: defaults to `https://aport.io`.

The verification node can still be configured without an API key when the target APort endpoint supports public verification.

## Node Inputs

| Field | Description |
| --- | --- |
| `Passport ID` | Agent passport identifier. The default expression reads `agent_id`, `passport_id`, or `passportId` from incoming JSON. |
| `Policy Pack` | APort policy identifier, for example `finance.payment.refund.v1`. |
| `Context Source` | Choose incoming item JSON, a JSON object, or key/value fields. |
| `Include Input Data` | Keeps the original item JSON in the routed output. |

## Outputs

1. `Approved`: APort returned an allow/approved/verified decision.
2. `Denied`: APort responded successfully but did not approve the action.
3. `Error`: Missing configuration, invalid context JSON, or request errors.

Each output item includes:

```json
{
"aport": {
"approved": true,
"passportId": "agt_inst_demo_123",
"policyPack": "finance.payment.refund.v1",
"response": {
"verified": true
}
}
}
```

## Example Workflow

Import `workflows/aport-verify-routing.json` into n8n. The workflow creates a sample refund context, verifies it with the APort node, then exposes the three routing branches for downstream actions such as approvals, Slack alerts, or manual review.

## Verification

```bash
npm test
```

The tests cover context construction, decision parsing, approved routing, denied routing, and error routing.
48 changes: 48 additions & 0 deletions examples/agent-frameworks/n8n/credentials/APortApi.credentials.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
class APortApi {
constructor() {
this.name = "aportApi";
this.displayName = "APort API";
this.documentationUrl = "https://aport.io/docs";
this.properties = [
{
displayName: "API Key",
name: "apiKey",
type: "string",
typeOptions: {
password: true,
},
default: "",
required: false,
description:
"Bearer token for APort API calls. Some verification endpoints may be usable without a key.",
},
{
displayName: "Base URL",
name: "baseUrl",
type: "string",
default: "https://aport.io",
required: true,
description: "APort API base URL.",
},
];
this.authenticate = {
type: "generic",
properties: {
headers: {
Authorization: "=Bearer {{$credentials.apiKey}}",
},
},
};
this.test = {
request: {
baseURL: "={{$credentials.baseUrl}}",
url: "/api/policies",
method: "GET",
},
};
}
}

module.exports = {
APortApi,
};
253 changes: 253 additions & 0 deletions examples/agent-frameworks/n8n/nodes/APortVerify/APortVerify.node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
const {
buildContext,
formatError,
isApproved,
normalizeBaseUrl,
} = require("./utils");

class APortVerify {
constructor() {
this.description = {
displayName: "APort Verify",
name: "aportVerify",
group: ["transform"],
version: 1,
description:
"Verify an APort passport against a policy and route the workflow by decision.",
defaults: {
name: "APort Verify",
},
inputs: ["main"],
outputs: ["main", "main", "main"],
outputNames: ["Approved", "Denied", "Error"],
credentials: [
{
name: "aportApi",
required: false,
},
],
properties: [
{
displayName: "Passport ID",
name: "passportId",
type: "string",
default: "={{$json.agent_id || $json.passport_id || $json.passportId || ''}}",
required: true,
description: "Agent passport identifier to verify.",
},
{
displayName: "Policy Pack",
name: "policyPack",
type: "string",
default: "code.repository.merge.v1",
required: true,
description: "APort policy pack identifier.",
},
{
displayName: "Context Source",
name: "contextMode",
type: "options",
default: "input",
options: [
{
name: "Input JSON",
value: "input",
description: "Send the incoming item JSON as APort context.",
},
{
name: "JSON Object",
value: "json",
description: "Send a JSON object configured on this node.",
},
{
name: "Key/Value Fields",
value: "fields",
description: "Build context from manually configured fields.",
},
],
},
{
displayName: "Context JSON",
name: "contextJson",
type: "json",
default: "{}",
displayOptions: {
show: {
contextMode: ["json"],
},
},
description: "JSON object sent as the APort verification context.",
},
{
displayName: "Context Fields",
name: "contextFields",
type: "fixedCollection",
default: {},
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
contextMode: ["fields"],
},
},
options: [
{
name: "field",
displayName: "Field",
values: [
{
displayName: "Key",
name: "key",
type: "string",
default: "",
},
{
displayName: "Value",
name: "value",
type: "string",
default: "",
description:
"String values are passed as-is. JSON-looking values are parsed.",
},
],
},
],
},
{
displayName: "Include Input Data",
name: "includeInputData",
type: "boolean",
default: true,
description:
"Whether to include the incoming item JSON in the routed output item.",
},
{
displayName: "Base URL Override",
name: "baseUrlOverride",
type: "string",
default: "",
placeholder: "https://aport.io",
description:
"Optional base URL used when no APort credential base URL is configured.",
},
],
};
}

async execute() {
const items = this.getInputData();
const approvedItems = [];
const deniedItems = [];
const errorItems = [];

let credentials = {};
try {
credentials = await this.getCredentials("aportApi");
} catch (_error) {
credentials = {};
}

for (let itemIndex = 0; itemIndex < items.length; itemIndex += 1) {
const inputJson = items[itemIndex].json || {};
const includeInputData = this.getNodeParameter(
"includeInputData",
itemIndex,
true
);

try {
const passportId = this.getNodeParameter("passportId", itemIndex, "");
const policyPack = this.getNodeParameter("policyPack", itemIndex, "");
const contextMode = this.getNodeParameter("contextMode", itemIndex, "input");
const contextJson = this.getNodeParameter("contextJson", itemIndex, "{}");
const contextFields = this.getNodeParameter("contextFields", itemIndex, {});
const baseUrlOverride = this.getNodeParameter(
"baseUrlOverride",
itemIndex,
""
);

if (!passportId) {
throw new Error("Passport ID is required.");
}

if (!policyPack) {
throw new Error("Policy Pack is required.");
}

const context = buildContext({
mode: contextMode,
inputJson,
contextJson,
fields: contextFields,
});
const baseUrl = normalizeBaseUrl(credentials.baseUrl || baseUrlOverride);
const headers = {
"Content-Type": "application/json",
};

if (credentials.apiKey) {
headers.Authorization = `Bearer ${credentials.apiKey}`;
}

const response = await this.helpers.httpRequest.call(this, {
method: "POST",
url: `${baseUrl}/api/verify/policy/${encodeURIComponent(policyPack)}`,
headers,
body: {
context: {
agent_id: passportId,
passport_id: passportId,
policy_id: policyPack,
context,
},
},
json: true,
timeout: 10000,
});

const approved = isApproved(response);
const outputItem = {
json: {
...(includeInputData ? inputJson : {}),
aport: {
approved,
passportId,
policyPack,
response,
},
},
pairedItem: {
item: itemIndex,
},
};

if (approved) {
approvedItems.push(outputItem);
} else {
deniedItems.push(outputItem);
}
} catch (error) {
errorItems.push({
json: {
...(includeInputData ? inputJson : {}),
aport: {
approved: false,
error: formatError(error),
},
},
pairedItem: {
item: itemIndex,
},
});
}
}

return [approvedItems, deniedItems, errorItems];
}
}

module.exports = {
APortVerify,
};
Loading