n8n-nodes-soterai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +190 -0
- package/dist/credentials/SoterApi.credentials.d.ts +7 -0
- package/dist/credentials/SoterApi.credentials.js +36 -0
- package/dist/nodes/SoterGuard.node.d.ts +5 -0
- package/dist/nodes/SoterGuard.node.js +504 -0
- package/dist/nodes/soterai.png +0 -0
- package/nodes/soterai.png +0 -0
- package/package.json +58 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `n8n-nodes-soterai` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-06-26
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Initial release of the SoterAI community node for n8n.
|
|
10
|
+
- **Input Guard** action: scan user messages for prompt injection, jailbreaks, and other threats before they reach the LLM.
|
|
11
|
+
- **Output Guard** action: scan AI-generated responses for unsafe content before sending to users.
|
|
12
|
+
- **PII Redactor** action: detect and redact sensitive data (emails, phone numbers, secrets) with configurable redaction modes (partial, full, hash).
|
|
13
|
+
- **RAG Scanner** action: scan documents and chunks for embedded threats before adding to vector databases.
|
|
14
|
+
- **Incident Logger** action: log security incidents to the SoterAI ops dashboard with platform, workflow ID, risk score, and reason.
|
|
15
|
+
- Configurable **Policy Mode** (Monitor, Balanced, Strict) for input and output guards.
|
|
16
|
+
- Configurable **On Threat** behavior (Block, Redact, Warn, Continue).
|
|
17
|
+
- Optional per-node **Project ID** override and **Metadata JSON** for audit trails.
|
|
18
|
+
- Example workflow: Protected Chatbot with input guard, threat routing, and output guard.
|
|
19
|
+
- SoterAI API credential type with API key, base URL, and default project ID.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 SoterAI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# n8n-nodes-soterai
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/n8n-nodes-soterai)
|
|
4
|
+
[](https://n8n.io)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
SoterAI community node for [n8n](https://n8n.io) -- protect your AI workflows from prompt injection, jailbreaks, PII leakage, and unsafe outputs.
|
|
8
|
+
|
|
9
|
+
SoterAI is an AI security platform that sits between your users and your LLMs. It analyses every input and output in real time, blocks threats, redacts sensitive data, and records incidents for audit review. This n8n node lets you add that protection to any n8n workflow with drag-and-drop.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
### From the n8n GUI (recommended)
|
|
14
|
+
|
|
15
|
+
1. Open your n8n instance.
|
|
16
|
+
2. Go to **Settings > Community Nodes**.
|
|
17
|
+
3. Enter `n8n-nodes-soterai` and click **Install**.
|
|
18
|
+
4. The **SoterAI** node will appear in your node panel.
|
|
19
|
+
|
|
20
|
+
### From npm
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cd ~/.n8n
|
|
24
|
+
npm install n8n-nodes-soterai
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Restart n8n after installation.
|
|
28
|
+
|
|
29
|
+
## Credentials Setup
|
|
30
|
+
|
|
31
|
+
1. Sign up at [https://app.soterai.dev](https://app.soterai.dev) and create a project.
|
|
32
|
+
2. Generate an API key from the project dashboard (it starts with `sk_`).
|
|
33
|
+
3. In n8n, go to **Credentials > New Credential > SoterAI API**.
|
|
34
|
+
4. Paste your API key.
|
|
35
|
+
5. (Optional) Set a **Base URL** if you are running a self-hosted SoterAI instance.
|
|
36
|
+
6. (Optional) Set a default **Project ID** -- this can also be overridden per node.
|
|
37
|
+
|
|
38
|
+
Full credential documentation: [https://docs.soterai.dev/integrations/n8n](https://docs.soterai.dev/integrations/n8n)
|
|
39
|
+
|
|
40
|
+
## Actions
|
|
41
|
+
|
|
42
|
+
### SoterAI Input Guard
|
|
43
|
+
|
|
44
|
+
Check user messages for prompt injection, jailbreaks, and other threats **before** they reach the LLM.
|
|
45
|
+
|
|
46
|
+
| Field | Type | Description |
|
|
47
|
+
|-------|------|-------------|
|
|
48
|
+
| Input Text | string | The user message to analyse |
|
|
49
|
+
| Policy Mode | MONITOR / BALANCED / STRICT | Server-side policy strictness |
|
|
50
|
+
| On Threat | BLOCK / REDACT / WARN / CONTINUE | Local behavior when a threat is detected |
|
|
51
|
+
| Project ID | string | Optional project override |
|
|
52
|
+
| Metadata JSON | string | Optional audit metadata (JSON object) |
|
|
53
|
+
|
|
54
|
+
### SoterAI Output Guard
|
|
55
|
+
|
|
56
|
+
Check AI-generated responses for unsafe content, hallucinated PII, or policy violations **before** sending them to users.
|
|
57
|
+
|
|
58
|
+
| Field | Type | Description |
|
|
59
|
+
|-------|------|-------------|
|
|
60
|
+
| AI Output Text | string | The AI response to analyse |
|
|
61
|
+
| Policy Mode | MONITOR / BALANCED / STRICT | Server-side policy strictness |
|
|
62
|
+
| On Threat | BLOCK / REDACT / WARN / CONTINUE | Local behavior when a threat is detected |
|
|
63
|
+
| Project ID | string | Optional project override |
|
|
64
|
+
| Metadata JSON | string | Optional audit metadata (JSON object) |
|
|
65
|
+
|
|
66
|
+
### SoterAI PII Redactor
|
|
67
|
+
|
|
68
|
+
Detect and redact sensitive data (emails, phone numbers, API keys, secrets) from any text.
|
|
69
|
+
|
|
70
|
+
| Field | Type | Description |
|
|
71
|
+
|-------|------|-------------|
|
|
72
|
+
| Text | string | The text to scan for PII |
|
|
73
|
+
| Redaction Mode | PARTIAL / FULL / HASH | How detected PII is replaced |
|
|
74
|
+
| Project ID | string | Optional project override |
|
|
75
|
+
| Metadata JSON | string | Optional audit metadata (JSON object) |
|
|
76
|
+
|
|
77
|
+
### SoterAI RAG Scanner
|
|
78
|
+
|
|
79
|
+
Scan documents and text chunks for embedded threats, hidden instructions, or data poisoning **before** adding them to a vector database.
|
|
80
|
+
|
|
81
|
+
| Field | Type | Description |
|
|
82
|
+
|-------|------|-------------|
|
|
83
|
+
| Document Text | string | The document or chunk text to scan |
|
|
84
|
+
| Source Name | string | Optional label for the document source |
|
|
85
|
+
| Project ID | string | Optional project override |
|
|
86
|
+
| Metadata JSON | string | Optional audit metadata (JSON object) |
|
|
87
|
+
|
|
88
|
+
### SoterAI Incident Logger
|
|
89
|
+
|
|
90
|
+
Log a security incident to the SoterAI ops dashboard for security review. This action requires admin-level API access and will return a graceful no-op if the caller lacks the required permissions.
|
|
91
|
+
|
|
92
|
+
| Field | Type | Description |
|
|
93
|
+
|-------|------|-------------|
|
|
94
|
+
| Platform | string | Where the incident originated (default: `n8n`) |
|
|
95
|
+
| Workflow ID | string | ID of the workflow where the incident was detected |
|
|
96
|
+
| Risk Score | number | Risk score from 0.0 to 1.0 |
|
|
97
|
+
| Reason | string | Human-readable incident description |
|
|
98
|
+
| Project ID | string | Optional project override |
|
|
99
|
+
| Metadata JSON | string | Optional audit metadata (JSON object) |
|
|
100
|
+
|
|
101
|
+
## Output Fields
|
|
102
|
+
|
|
103
|
+
### Input Guard / Output Guard
|
|
104
|
+
|
|
105
|
+
| Field | Type | Description |
|
|
106
|
+
|-------|------|-------------|
|
|
107
|
+
| `allowed` | boolean | Whether the API considers the text safe |
|
|
108
|
+
| `blocked` | boolean | Whether the node blocked the item (based on On Threat) |
|
|
109
|
+
| `riskScore` | number | Risk score from 0.0 to 1.0 |
|
|
110
|
+
| `categories` | string[] | Array of detected risk types |
|
|
111
|
+
| `safeText` | string | Redacted/safe version of the text |
|
|
112
|
+
| `outputText` | string | The text to use downstream (empty if blocked) |
|
|
113
|
+
| `reason` | string | Human-readable explanation |
|
|
114
|
+
| `warning` | string | Present when On Threat is WARN |
|
|
115
|
+
| `incidentId` | string | Incident ID if one was created |
|
|
116
|
+
| `rawResponse` | object | Full API response for advanced use |
|
|
117
|
+
|
|
118
|
+
### PII Redactor
|
|
119
|
+
|
|
120
|
+
| Field | Type | Description |
|
|
121
|
+
|-------|------|-------------|
|
|
122
|
+
| `safeText` | string | Text with PII redacted |
|
|
123
|
+
| `detectedEntities` | array | List of detected PII entities with type, label, severity |
|
|
124
|
+
| `riskScore` | number | Overall risk score |
|
|
125
|
+
| `rawResponse` | object | Full API response |
|
|
126
|
+
|
|
127
|
+
### RAG Scanner
|
|
128
|
+
|
|
129
|
+
| Field | Type | Description |
|
|
130
|
+
|-------|------|-------------|
|
|
131
|
+
| `allowed` | boolean | Whether the document is safe to ingest |
|
|
132
|
+
| `riskScore` | number | Overall risk score |
|
|
133
|
+
| `issues` | array | List of issues with type, severity, message |
|
|
134
|
+
| `safeText` | string | Cleaned version of the document |
|
|
135
|
+
| `incidentId` | string | Incident ID if one was created |
|
|
136
|
+
| `rawResponse` | object | Full API response |
|
|
137
|
+
|
|
138
|
+
### Incident Logger
|
|
139
|
+
|
|
140
|
+
| Field | Type | Description |
|
|
141
|
+
|-------|------|-------------|
|
|
142
|
+
| `logged` | boolean | Whether the incident was successfully logged |
|
|
143
|
+
| `incidentId` | string | ID of the created incident (if logged) |
|
|
144
|
+
| `reason` | string | Explanation if logging failed (e.g. insufficient permissions) |
|
|
145
|
+
| `rawResponse` | object | Full API response |
|
|
146
|
+
|
|
147
|
+
## Example Workflow
|
|
148
|
+
|
|
149
|
+
An importable example workflow is included at `examples/protected-chatbot-workflow.json`.
|
|
150
|
+
|
|
151
|
+
The workflow implements a protected chatbot pattern:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
[Webhook] -> [SoterAI Input Guard] -> [IF blocked?]
|
|
155
|
+
|-- Yes -> [Reply: "Blocked"]
|
|
156
|
+
|-- No -> [OpenAI Chat] -> [SoterAI Output Guard] -> [Reply]
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
To import: in n8n, go to **Workflows > Import from File** and select the JSON file.
|
|
160
|
+
|
|
161
|
+
## Privacy and Security
|
|
162
|
+
|
|
163
|
+
- **API keys** are stored in n8n's encrypted credential store and are never logged or exposed in workflow outputs.
|
|
164
|
+
- **All security analysis** happens server-side via the SoterAI policy engine. No user data is analysed locally.
|
|
165
|
+
- **Nodes are stateless connectors** -- no user data, messages, or documents are stored locally by the node.
|
|
166
|
+
- **Network traffic** uses HTTPS exclusively. The node communicates only with your configured SoterAI API endpoint.
|
|
167
|
+
- **No telemetry** is collected by this node.
|
|
168
|
+
|
|
169
|
+
## Resources
|
|
170
|
+
|
|
171
|
+
- [SoterAI Documentation](https://docs.soterai.dev)
|
|
172
|
+
- [n8n Integration Guide](https://docs.soterai.dev/integrations/n8n)
|
|
173
|
+
- [SoterAI Dashboard](https://app.soterai.dev)
|
|
174
|
+
- [Privacy Policy](https://soterai.dev/privacy)
|
|
175
|
+
- [Terms of Service](https://soterai.dev/terms)
|
|
176
|
+
- [Pricing](https://soterai.dev/pricing)
|
|
177
|
+
- [Status](https://soterai.dev/status)
|
|
178
|
+
- [Support](https://soterai.dev/support)
|
|
179
|
+
- [GitHub Repository](https://github.com/SoterAI/n8n-nodes-soterai)
|
|
180
|
+
- [npm Package](https://www.npmjs.com/package/n8n-nodes-soterai)
|
|
181
|
+
|
|
182
|
+
## Support
|
|
183
|
+
|
|
184
|
+
- **Email**: support@soterai.dev
|
|
185
|
+
- **GitHub Issues**: [https://github.com/SoterAI/n8n-nodes-soterai/issues](https://github.com/SoterAI/n8n-nodes-soterai/issues)
|
|
186
|
+
- **Documentation**: [https://docs.soterai.dev](https://docs.soterai.dev)
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SoterApi = void 0;
|
|
4
|
+
class SoterApi {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.name = "soterApi";
|
|
7
|
+
this.displayName = "SoterAI API";
|
|
8
|
+
this.documentationUrl = "https://docs.soterai.dev/integrations/n8n";
|
|
9
|
+
this.properties = [
|
|
10
|
+
{
|
|
11
|
+
displayName: "API Key",
|
|
12
|
+
name: "apiKey",
|
|
13
|
+
type: "string",
|
|
14
|
+
typeOptions: { password: true },
|
|
15
|
+
default: "",
|
|
16
|
+
required: true,
|
|
17
|
+
description: "Your SoterAI API key (sk_...)",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
displayName: "Base URL",
|
|
21
|
+
name: "baseUrl",
|
|
22
|
+
type: "string",
|
|
23
|
+
default: "https://api.soterai.dev",
|
|
24
|
+
description: "SoterAI API base URL. Change only for self-hosted deployments.",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
displayName: "Project ID",
|
|
28
|
+
name: "projectId",
|
|
29
|
+
type: "string",
|
|
30
|
+
default: "",
|
|
31
|
+
description: "Default project ID for all requests (optional, can be set per node)",
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.SoterApi = SoterApi;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from "n8n-workflow";
|
|
2
|
+
export declare class SoterGuard implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SoterGuard = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
class SoterGuard {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.description = {
|
|
8
|
+
displayName: "SoterAI",
|
|
9
|
+
name: "soterGuard",
|
|
10
|
+
icon: "file:soterai.png",
|
|
11
|
+
group: ["transform"],
|
|
12
|
+
version: 1,
|
|
13
|
+
subtitle: '={{$parameter["action"]}}',
|
|
14
|
+
description: "Protect AI workflows with SoterAI — input/output guard, PII redaction, RAG document scanning",
|
|
15
|
+
defaults: {
|
|
16
|
+
name: "SoterAI",
|
|
17
|
+
},
|
|
18
|
+
inputs: ["main"],
|
|
19
|
+
outputs: ["main"],
|
|
20
|
+
credentials: [
|
|
21
|
+
{
|
|
22
|
+
name: "soterApi",
|
|
23
|
+
required: true,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
properties: [
|
|
27
|
+
{
|
|
28
|
+
displayName: "Action",
|
|
29
|
+
name: "action",
|
|
30
|
+
type: "options",
|
|
31
|
+
noDataExpression: true,
|
|
32
|
+
options: [
|
|
33
|
+
{
|
|
34
|
+
name: "SoterAI Input Guard",
|
|
35
|
+
value: "inputGuard",
|
|
36
|
+
description: "Check user message before it reaches the LLM",
|
|
37
|
+
action: "Check user input for threats",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "SoterAI Output Guard",
|
|
41
|
+
value: "outputGuard",
|
|
42
|
+
description: "Check AI response before it is sent to the user",
|
|
43
|
+
action: "Check AI output for threats",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "SoterAI PII Redactor",
|
|
47
|
+
value: "piiRedactor",
|
|
48
|
+
description: "Redact sensitive data (PII, secrets) from any text",
|
|
49
|
+
action: "Redact PII from text",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "SoterAI RAG Scanner",
|
|
53
|
+
value: "ragScanner",
|
|
54
|
+
description: "Scan documents/chunks before adding to RAG/vector DB",
|
|
55
|
+
action: "Scan RAG document for threats",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "SoterAI Incident Logger",
|
|
59
|
+
value: "incidentLogger",
|
|
60
|
+
description: "Log a security incident to the SoterAI ops dashboard. Requires admin access; returns a graceful no-op if the caller lacks permissions.",
|
|
61
|
+
action: "Log security incident",
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
default: "inputGuard",
|
|
65
|
+
},
|
|
66
|
+
// ── Input Guard fields ──
|
|
67
|
+
{
|
|
68
|
+
displayName: "Input Text",
|
|
69
|
+
name: "inputText",
|
|
70
|
+
type: "string",
|
|
71
|
+
typeOptions: { rows: 4 },
|
|
72
|
+
default: "",
|
|
73
|
+
required: true,
|
|
74
|
+
displayOptions: { show: { action: ["inputGuard"] } },
|
|
75
|
+
description: "The user message to check for prompt injection, jailbreaks, and other threats",
|
|
76
|
+
},
|
|
77
|
+
// ── Output Guard fields ──
|
|
78
|
+
{
|
|
79
|
+
displayName: "AI Output Text",
|
|
80
|
+
name: "outputText",
|
|
81
|
+
type: "string",
|
|
82
|
+
typeOptions: { rows: 4 },
|
|
83
|
+
default: "",
|
|
84
|
+
required: true,
|
|
85
|
+
displayOptions: { show: { action: ["outputGuard"] } },
|
|
86
|
+
description: "The AI-generated response to check before sending to the user",
|
|
87
|
+
},
|
|
88
|
+
// ── PII Redactor fields ──
|
|
89
|
+
{
|
|
90
|
+
displayName: "Text",
|
|
91
|
+
name: "piiText",
|
|
92
|
+
type: "string",
|
|
93
|
+
typeOptions: { rows: 4 },
|
|
94
|
+
default: "",
|
|
95
|
+
required: true,
|
|
96
|
+
displayOptions: { show: { action: ["piiRedactor"] } },
|
|
97
|
+
description: "The text to scan and redact PII from",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
displayName: "Redaction Mode",
|
|
101
|
+
name: "redactionMode",
|
|
102
|
+
type: "options",
|
|
103
|
+
options: [
|
|
104
|
+
{ name: "Partial", value: "PARTIAL", description: "Partially mask sensitive data" },
|
|
105
|
+
{ name: "Full", value: "FULL", description: "Fully replace sensitive data with tokens" },
|
|
106
|
+
{ name: "Hash", value: "HASH", description: "Replace with deterministic hashes" },
|
|
107
|
+
],
|
|
108
|
+
default: "PARTIAL",
|
|
109
|
+
displayOptions: { show: { action: ["piiRedactor"] } },
|
|
110
|
+
description: "How to redact detected PII",
|
|
111
|
+
},
|
|
112
|
+
// ── RAG Scanner fields ──
|
|
113
|
+
{
|
|
114
|
+
displayName: "Document Text",
|
|
115
|
+
name: "ragText",
|
|
116
|
+
type: "string",
|
|
117
|
+
typeOptions: { rows: 4 },
|
|
118
|
+
default: "",
|
|
119
|
+
required: true,
|
|
120
|
+
displayOptions: { show: { action: ["ragScanner"] } },
|
|
121
|
+
description: "Document or chunk text to scan before adding to a vector database",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
displayName: "Source Name",
|
|
125
|
+
name: "sourceName",
|
|
126
|
+
type: "string",
|
|
127
|
+
default: "",
|
|
128
|
+
displayOptions: { show: { action: ["ragScanner"] } },
|
|
129
|
+
description: 'Optional label for the document source (e.g. "uploaded-pdf", "web-scrape")',
|
|
130
|
+
},
|
|
131
|
+
// ── Incident Logger fields ──
|
|
132
|
+
{
|
|
133
|
+
displayName: "Platform",
|
|
134
|
+
name: "incidentPlatform",
|
|
135
|
+
type: "string",
|
|
136
|
+
default: "n8n",
|
|
137
|
+
required: true,
|
|
138
|
+
displayOptions: { show: { action: ["incidentLogger"] } },
|
|
139
|
+
description: 'Platform where the incident originated (e.g. "n8n", "slack", "api")',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
displayName: "Workflow ID",
|
|
143
|
+
name: "incidentWorkflowId",
|
|
144
|
+
type: "string",
|
|
145
|
+
default: "",
|
|
146
|
+
required: true,
|
|
147
|
+
displayOptions: { show: { action: ["incidentLogger"] } },
|
|
148
|
+
description: "ID of the workflow or process where the incident was detected",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
displayName: "Risk Score",
|
|
152
|
+
name: "incidentRiskScore",
|
|
153
|
+
type: "number",
|
|
154
|
+
typeOptions: { minValue: 0, maxValue: 1, numberStepSize: 0.01 },
|
|
155
|
+
default: 0,
|
|
156
|
+
required: true,
|
|
157
|
+
displayOptions: { show: { action: ["incidentLogger"] } },
|
|
158
|
+
description: "Risk score from 0.0 (safe) to 1.0 (critical)",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
displayName: "Reason",
|
|
162
|
+
name: "incidentReason",
|
|
163
|
+
type: "string",
|
|
164
|
+
typeOptions: { rows: 2 },
|
|
165
|
+
default: "",
|
|
166
|
+
required: true,
|
|
167
|
+
displayOptions: { show: { action: ["incidentLogger"] } },
|
|
168
|
+
description: "Human-readable reason or description for the incident",
|
|
169
|
+
},
|
|
170
|
+
// ── Common fields ──
|
|
171
|
+
{
|
|
172
|
+
displayName: "Project ID",
|
|
173
|
+
name: "projectId",
|
|
174
|
+
type: "string",
|
|
175
|
+
default: "",
|
|
176
|
+
description: "SoterAI project ID (overrides the credential default)",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
displayName: "Policy Mode",
|
|
180
|
+
name: "policyMode",
|
|
181
|
+
type: "options",
|
|
182
|
+
options: [
|
|
183
|
+
{ name: "Monitor", value: "MONITOR", description: "Log threats but allow all traffic" },
|
|
184
|
+
{ name: "Balanced", value: "BALANCED", description: "Block high-risk, allow medium with redaction" },
|
|
185
|
+
{ name: "Strict", value: "STRICT", description: "Block anything above low risk" },
|
|
186
|
+
],
|
|
187
|
+
default: "BALANCED",
|
|
188
|
+
displayOptions: { show: { action: ["inputGuard", "outputGuard"] } },
|
|
189
|
+
description: "Server-side policy strictness",
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
displayName: "On Threat",
|
|
193
|
+
name: "onThreat",
|
|
194
|
+
type: "options",
|
|
195
|
+
options: [
|
|
196
|
+
{ name: "Block", value: "BLOCK", description: "Stop the workflow item" },
|
|
197
|
+
{ name: "Redact", value: "REDACT", description: "Continue with redacted safe text" },
|
|
198
|
+
{ name: "Warn", value: "WARN", description: "Continue but flag the threat in output" },
|
|
199
|
+
{ name: "Continue", value: "CONTINUE", description: "Ignore the threat and continue" },
|
|
200
|
+
],
|
|
201
|
+
default: "BLOCK",
|
|
202
|
+
displayOptions: { show: { action: ["inputGuard", "outputGuard"] } },
|
|
203
|
+
description: "What to do locally when SoterAI flags a threat",
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
displayName: "Metadata JSON",
|
|
207
|
+
name: "metadata",
|
|
208
|
+
type: "string",
|
|
209
|
+
typeOptions: { rows: 2 },
|
|
210
|
+
default: "",
|
|
211
|
+
description: "Optional JSON metadata to attach to the request for audit logging",
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
async execute() {
|
|
217
|
+
const items = this.getInputData();
|
|
218
|
+
const returnData = [];
|
|
219
|
+
const credentials = await this.getCredentials("soterApi");
|
|
220
|
+
const apiKey = credentials.apiKey;
|
|
221
|
+
const baseUrl = credentials.baseUrl || "https://api.soterai.dev";
|
|
222
|
+
const credentialProjectId = credentials.projectId || undefined;
|
|
223
|
+
for (let i = 0; i < items.length; i++) {
|
|
224
|
+
try {
|
|
225
|
+
const action = this.getNodeParameter("action", i);
|
|
226
|
+
const projectId = this.getNodeParameter("projectId", i, "") || credentialProjectId;
|
|
227
|
+
const metadataRaw = this.getNodeParameter("metadata", i, "");
|
|
228
|
+
const metadata = metadataRaw ? parseMetadata(metadataRaw) : undefined;
|
|
229
|
+
let result;
|
|
230
|
+
switch (action) {
|
|
231
|
+
case "inputGuard": {
|
|
232
|
+
const text = this.getNodeParameter("inputText", i);
|
|
233
|
+
const policyMode = this.getNodeParameter("policyMode", i);
|
|
234
|
+
const onThreat = this.getNodeParameter("onThreat", i);
|
|
235
|
+
result = await executeInputGuard(apiKey, baseUrl, {
|
|
236
|
+
text, projectId, policyMode, onThreat, metadata,
|
|
237
|
+
});
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case "outputGuard": {
|
|
241
|
+
const text = this.getNodeParameter("outputText", i);
|
|
242
|
+
const policyMode = this.getNodeParameter("policyMode", i);
|
|
243
|
+
const onThreat = this.getNodeParameter("onThreat", i);
|
|
244
|
+
result = await executeOutputGuard(apiKey, baseUrl, {
|
|
245
|
+
text, projectId, policyMode, onThreat, metadata,
|
|
246
|
+
});
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
case "piiRedactor": {
|
|
250
|
+
const text = this.getNodeParameter("piiText", i);
|
|
251
|
+
const redactionMode = this.getNodeParameter("redactionMode", i);
|
|
252
|
+
result = await executePiiRedactor(apiKey, baseUrl, {
|
|
253
|
+
text, projectId, redactionMode, metadata,
|
|
254
|
+
});
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
case "ragScanner": {
|
|
258
|
+
const text = this.getNodeParameter("ragText", i);
|
|
259
|
+
const sourceName = this.getNodeParameter("sourceName", i, "");
|
|
260
|
+
result = await executeRagScanner(apiKey, baseUrl, {
|
|
261
|
+
text, projectId, sourceName, metadata,
|
|
262
|
+
});
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
case "incidentLogger": {
|
|
266
|
+
const platform = this.getNodeParameter("incidentPlatform", i);
|
|
267
|
+
const workflowId = this.getNodeParameter("incidentWorkflowId", i);
|
|
268
|
+
const riskScore = this.getNodeParameter("incidentRiskScore", i);
|
|
269
|
+
const reason = this.getNodeParameter("incidentReason", i);
|
|
270
|
+
result = await executeIncidentLogger(apiKey, baseUrl, {
|
|
271
|
+
platform, workflowId, riskScore, reason, projectId, metadata,
|
|
272
|
+
});
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
default:
|
|
276
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown action: ${action}`, { itemIndex: i });
|
|
277
|
+
}
|
|
278
|
+
returnData.push({ json: result });
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
if (this.continueOnFail()) {
|
|
282
|
+
returnData.push({
|
|
283
|
+
json: {
|
|
284
|
+
error: true,
|
|
285
|
+
message: error instanceof Error ? error.message : "SoterAI request failed.",
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return [returnData];
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
exports.SoterGuard = SoterGuard;
|
|
297
|
+
async function soterPost(apiKey, baseUrl, path, body) {
|
|
298
|
+
const url = `${baseUrl.replace(/\/$/, "")}${path}`;
|
|
299
|
+
const response = await fetch(url, {
|
|
300
|
+
method: "POST",
|
|
301
|
+
headers: {
|
|
302
|
+
"Content-Type": "application/json",
|
|
303
|
+
"x-api-key": apiKey,
|
|
304
|
+
"User-Agent": "n8n-nodes-soterai/0.1.0",
|
|
305
|
+
},
|
|
306
|
+
body: JSON.stringify(body),
|
|
307
|
+
});
|
|
308
|
+
const data = await response.json();
|
|
309
|
+
if (!response.ok) {
|
|
310
|
+
const msg = typeof data.message === "string" ? data.message : `SoterAI API error ${response.status}`;
|
|
311
|
+
throw new Error(msg);
|
|
312
|
+
}
|
|
313
|
+
return data;
|
|
314
|
+
}
|
|
315
|
+
async function executeInputGuard(apiKey, baseUrl, params) {
|
|
316
|
+
const meta = { ...params.metadata };
|
|
317
|
+
if (params.projectId)
|
|
318
|
+
meta.projectId = params.projectId;
|
|
319
|
+
if (params.policyMode)
|
|
320
|
+
meta.policyMode = params.policyMode;
|
|
321
|
+
const raw = await soterPost(apiKey, baseUrl, "/api/guard/input", {
|
|
322
|
+
message: params.text,
|
|
323
|
+
metadata: meta,
|
|
324
|
+
});
|
|
325
|
+
const allowed = raw.allowed;
|
|
326
|
+
const result = {
|
|
327
|
+
allowed,
|
|
328
|
+
riskScore: raw.riskScore ?? 0,
|
|
329
|
+
categories: raw.riskTypes ?? [],
|
|
330
|
+
safeText: raw.safeText ?? raw.redactedText ?? params.text,
|
|
331
|
+
reason: raw.reason ?? "",
|
|
332
|
+
incidentId: raw.incidentId ?? null,
|
|
333
|
+
rawResponse: raw,
|
|
334
|
+
};
|
|
335
|
+
if (!allowed && params.onThreat) {
|
|
336
|
+
switch (params.onThreat) {
|
|
337
|
+
case "BLOCK":
|
|
338
|
+
result.blocked = true;
|
|
339
|
+
result.outputText = "";
|
|
340
|
+
break;
|
|
341
|
+
case "REDACT":
|
|
342
|
+
result.blocked = false;
|
|
343
|
+
result.outputText = raw.safeText ?? raw.redactedText ?? "[REDACTED]";
|
|
344
|
+
break;
|
|
345
|
+
case "WARN":
|
|
346
|
+
result.blocked = false;
|
|
347
|
+
result.outputText = params.text;
|
|
348
|
+
result.warning = raw.reason ?? "";
|
|
349
|
+
break;
|
|
350
|
+
case "CONTINUE":
|
|
351
|
+
result.blocked = false;
|
|
352
|
+
result.outputText = params.text;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
result.blocked = false;
|
|
358
|
+
result.outputText = raw.safeText ?? raw.redactedText ?? params.text;
|
|
359
|
+
}
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
async function executeOutputGuard(apiKey, baseUrl, params) {
|
|
363
|
+
const meta = { ...params.metadata };
|
|
364
|
+
if (params.projectId)
|
|
365
|
+
meta.projectId = params.projectId;
|
|
366
|
+
if (params.policyMode)
|
|
367
|
+
meta.policyMode = params.policyMode;
|
|
368
|
+
const raw = await soterPost(apiKey, baseUrl, "/api/guard/output", {
|
|
369
|
+
aiResponse: params.text,
|
|
370
|
+
metadata: meta,
|
|
371
|
+
});
|
|
372
|
+
const allowed = raw.allowed;
|
|
373
|
+
const result = {
|
|
374
|
+
allowed,
|
|
375
|
+
riskScore: raw.riskScore ?? 0,
|
|
376
|
+
categories: raw.riskTypes ?? [],
|
|
377
|
+
safeText: raw.safeText ?? raw.redactedText ?? params.text,
|
|
378
|
+
reason: raw.reason ?? "",
|
|
379
|
+
incidentId: raw.incidentId ?? null,
|
|
380
|
+
rawResponse: raw,
|
|
381
|
+
};
|
|
382
|
+
if (!allowed && params.onThreat) {
|
|
383
|
+
switch (params.onThreat) {
|
|
384
|
+
case "BLOCK":
|
|
385
|
+
result.blocked = true;
|
|
386
|
+
result.outputText = "";
|
|
387
|
+
break;
|
|
388
|
+
case "REDACT":
|
|
389
|
+
result.blocked = false;
|
|
390
|
+
result.outputText = raw.safeText ?? raw.redactedText ?? "[REDACTED]";
|
|
391
|
+
break;
|
|
392
|
+
case "WARN":
|
|
393
|
+
result.blocked = false;
|
|
394
|
+
result.outputText = params.text;
|
|
395
|
+
result.warning = raw.reason ?? "";
|
|
396
|
+
break;
|
|
397
|
+
case "CONTINUE":
|
|
398
|
+
result.blocked = false;
|
|
399
|
+
result.outputText = params.text;
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
result.blocked = false;
|
|
405
|
+
result.outputText = raw.safeText ?? raw.redactedText ?? params.text;
|
|
406
|
+
}
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
async function executePiiRedactor(apiKey, baseUrl, params) {
|
|
410
|
+
const meta = { ...params.metadata };
|
|
411
|
+
if (params.projectId)
|
|
412
|
+
meta.projectId = params.projectId;
|
|
413
|
+
if (params.redactionMode)
|
|
414
|
+
meta._redactionMode = params.redactionMode;
|
|
415
|
+
const raw = await soterPost(apiKey, baseUrl, "/api/guard/input", {
|
|
416
|
+
message: params.text,
|
|
417
|
+
metadata: meta,
|
|
418
|
+
});
|
|
419
|
+
const findings = raw.findings ?? [];
|
|
420
|
+
const piiEntities = findings
|
|
421
|
+
.filter((f) => f.type === "PII_DETECTED" || f.type === "INDIA_PII_DETECTED" || f.type === "SECRET_DETECTED")
|
|
422
|
+
.map((f) => ({
|
|
423
|
+
type: f.type,
|
|
424
|
+
label: f.label,
|
|
425
|
+
severity: f.severity,
|
|
426
|
+
}));
|
|
427
|
+
return {
|
|
428
|
+
safeText: raw.safeText ?? raw.redactedText ?? params.text,
|
|
429
|
+
detectedEntities: piiEntities,
|
|
430
|
+
riskScore: raw.riskScore ?? 0,
|
|
431
|
+
rawResponse: raw,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
async function executeRagScanner(apiKey, baseUrl, params) {
|
|
435
|
+
const meta = { ...params.metadata, _ragScan: true };
|
|
436
|
+
if (params.projectId)
|
|
437
|
+
meta.projectId = params.projectId;
|
|
438
|
+
if (params.sourceName)
|
|
439
|
+
meta._sourceName = params.sourceName;
|
|
440
|
+
const raw = await soterPost(apiKey, baseUrl, "/api/guard/input", {
|
|
441
|
+
message: params.text,
|
|
442
|
+
metadata: meta,
|
|
443
|
+
});
|
|
444
|
+
const findings = raw.findings ?? [];
|
|
445
|
+
const issues = findings.map((f) => ({
|
|
446
|
+
type: f.type,
|
|
447
|
+
severity: f.severity,
|
|
448
|
+
message: f.message ?? f.label,
|
|
449
|
+
}));
|
|
450
|
+
return {
|
|
451
|
+
allowed: raw.allowed,
|
|
452
|
+
riskScore: raw.riskScore ?? 0,
|
|
453
|
+
issues: issues,
|
|
454
|
+
safeText: raw.safeText ?? raw.redactedText ?? params.text,
|
|
455
|
+
incidentId: raw.incidentId ?? null,
|
|
456
|
+
rawResponse: raw,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
async function executeIncidentLogger(apiKey, baseUrl, params) {
|
|
460
|
+
const body = {
|
|
461
|
+
platform: params.platform,
|
|
462
|
+
workflowId: params.workflowId,
|
|
463
|
+
riskScore: params.riskScore,
|
|
464
|
+
reason: params.reason,
|
|
465
|
+
};
|
|
466
|
+
if (params.projectId)
|
|
467
|
+
body.projectId = params.projectId;
|
|
468
|
+
if (params.metadata)
|
|
469
|
+
body.metadata = params.metadata;
|
|
470
|
+
try {
|
|
471
|
+
const raw = await soterPost(apiKey, baseUrl, "/api/ops/incidents", body);
|
|
472
|
+
return {
|
|
473
|
+
logged: true,
|
|
474
|
+
incidentId: raw.incidentId ?? raw.id ?? null,
|
|
475
|
+
rawResponse: raw,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
// Graceful no-op: if the caller lacks admin permissions, return success with logged=false
|
|
480
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
481
|
+
if (message.includes("403") || message.includes("401") || message.includes("Forbidden") || message.includes("Unauthorized")) {
|
|
482
|
+
return {
|
|
483
|
+
logged: false,
|
|
484
|
+
reason: "Insufficient permissions to log incidents. Admin access is required.",
|
|
485
|
+
rawResponse: { error: message },
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function parseMetadata(raw) {
|
|
492
|
+
if (!raw.trim())
|
|
493
|
+
return undefined;
|
|
494
|
+
try {
|
|
495
|
+
const parsed = JSON.parse(raw);
|
|
496
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
497
|
+
return parsed;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
// ignore invalid JSON — don't break the workflow for optional metadata
|
|
502
|
+
}
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
Binary file
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-soterai",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SoterAI adds prompt injection, jailbreak, PII leakage, and unsafe output protection to AI workflows.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "SoterAI",
|
|
8
|
+
"email": "support@soterai.dev",
|
|
9
|
+
"url": "https://soterai.dev"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/SoterAI/n8n-nodes-soterai",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/SoterAI/n8n-nodes-soterai.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"n8n-community-node-package",
|
|
18
|
+
"n8n",
|
|
19
|
+
"ai-security",
|
|
20
|
+
"prompt-injection",
|
|
21
|
+
"jailbreak",
|
|
22
|
+
"pii",
|
|
23
|
+
"soter",
|
|
24
|
+
"soterai",
|
|
25
|
+
"guardrails"
|
|
26
|
+
],
|
|
27
|
+
"n8n": {
|
|
28
|
+
"n8nNodesApiVersion": 1,
|
|
29
|
+
"credentials": [
|
|
30
|
+
"dist/credentials/SoterApi.credentials.js"
|
|
31
|
+
],
|
|
32
|
+
"nodes": [
|
|
33
|
+
"dist/nodes/SoterGuard.node.js"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"main": "dist/nodes/SoterGuard.node.js",
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"nodes/*.png",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE",
|
|
42
|
+
"CHANGELOG.md"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc && copyfiles \"nodes/*.png\" dist",
|
|
46
|
+
"dev": "tsc --watch",
|
|
47
|
+
"lint": "tsc --noEmit",
|
|
48
|
+
"prepublishOnly": "npm run build"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"n8n-workflow": ">=1.0.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"copyfiles": "^2.4.1",
|
|
55
|
+
"n8n-workflow": "^1.0.0",
|
|
56
|
+
"typescript": "^5.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|