n8n-nodes-jitterflow 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jitterflow
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,70 @@
1
+ # 🔌 n8n-nodes-jitterflow
2
+
3
+ An [n8n](https://n8n.io) community node for [Jitterflow](https://jitterflow.io) — jittered, rate-limited webhook delivery with a dead-letter queue, right inside your workflows.
4
+
5
+ > **🔑 What this buys you:** send a webhook through Jitterflow's pacing/retry engine, then list, replay, or resolve anything that ends up in the dead-letter queue — without leaving n8n.
6
+
7
+ All jitter math, TPS guarding, and DLQ decisioning happen server-side in Jitterflow. This node is a thin, typed client over the real REST API — the same contract [`@jitterflow/sdk-node`](https://jitterflow.io) uses.
8
+
9
+ ---
10
+
11
+ ## ✨ What it does
12
+
13
+ | Resource | Operation | What happens |
14
+ |---|---|---|
15
+ | **Webhook** | Send | Queues a webhook for delayed, jittered redelivery via `POST /v1/ingest/:endpointKey` |
16
+ | **DLQ** | List | Lists dead-letter queue entries, filterable by resolved/unresolved |
17
+ | **DLQ** | Replay | One-click re-enqueues a failed job for immediate redelivery |
18
+ | **DLQ** | Resolve | Marks an entry resolved without retrying it |
19
+
20
+ **Two details worth knowing about Send:**
21
+
22
+ - **Target Identifier** — optional; paces delivery per this identifier (a mailbox, a lead ID, whatever) instead of per endpoint. Defaults server-side to the endpoint key if left blank.
23
+ - **Idempotency Key** — defaults to `{{$execution.id}}-{{$itemIndex}}`, so a workflow retry never double-sends the same item.
24
+
25
+ ---
26
+
27
+ ## 📦 Install
28
+
29
+ | Platform | How |
30
+ |---|---|
31
+ | **Self-hosted n8n** | Settings → Community Nodes → install `n8n-nodes-jitterflow` |
32
+ | **n8n Cloud** | Available once the node clears n8n's verification review (submitted via the [Creator Portal](https://creators.n8n.io/nodes)) |
33
+
34
+ ---
35
+
36
+ ## 🔑 Credential
37
+
38
+ Create a **Jitterflow API** credential:
39
+
40
+ - [ ] Grab your tenant API key from the Jitterflow dashboard → **API Keys** (starts with `wjg_`)
41
+ - [ ] Paste it into the credential's **API Key** field
42
+ - [ ] Hit **Test** — it calls `GET /v1/endpoints` to confirm the key actually works
43
+
44
+ ---
45
+
46
+ ## 🛠️ Development
47
+
48
+ > This package was split out of the main [jitterflow-core-app](https://github.com/jitterflow/jitterflow-core-app) monorepo so it could be a public repo, per n8n's verification requirements. Day-to-day development — including integration/e2e tests against a real Jitterflow API instance — still happens there. This repo carries the standalone-buildable unit tests and is what actually gets published.
49
+
50
+ ```bash
51
+ npm ci
52
+ npm run build # n8n-node build
53
+ npm run lint # n8n-node lint — the verification-readiness check
54
+ npm test
55
+ ```
56
+
57
+ ---
58
+
59
+ ## 🚀 Release
60
+
61
+ ```bash
62
+ npm run release
63
+ ```
64
+
65
+ | Step | What it does |
66
+ |---|---|
67
+ | `npm run release` | Bumps the version, tags, and pushes |
68
+ | `.github/workflows/publish.yml` | Takes it from there — build, `n8n-node lint`, publish to npm with provenance |
69
+
70
+ > **⚠️ Requires** the `NPM_TOKEN` repo secret to be set.
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JitterflowApi = void 0;
4
+ class JitterflowApi {
5
+ name = 'jitterflowApi';
6
+ displayName = 'Jitterflow API';
7
+ icon = 'file:jitterflow.svg';
8
+ documentationUrl = 'https://jitterflow.io/docs';
9
+ properties = [
10
+ {
11
+ displayName: 'API Key',
12
+ name: 'apiKey',
13
+ type: 'string',
14
+ typeOptions: { password: true },
15
+ default: '',
16
+ required: true,
17
+ description: 'Your Jitterflow tenant API key (starts with wjg_). Find it in the Jitterflow dashboard under API Keys.',
18
+ },
19
+ {
20
+ displayName: 'Base URL',
21
+ name: 'baseUrl',
22
+ type: 'string',
23
+ default: 'https://jitterflow.io',
24
+ description: 'Override only for a self-hosted or non-production Jitterflow API origin.',
25
+ },
26
+ ];
27
+ // Generic (non-declarative-node) auth — every request this package makes
28
+ // goes through GenericFunctions.jitterflowApiRequest, which calls
29
+ // helpers.httpRequestWithAuthentication with this credential name, so
30
+ // this header injection is the single place API auth is applied.
31
+ authenticate = {
32
+ type: 'generic',
33
+ properties: {
34
+ headers: {
35
+ Authorization: '=Bearer {{$credentials.apiKey}}',
36
+ },
37
+ },
38
+ };
39
+ // Backs the "Test" button in the credential UI — GET /v1/endpoints is the
40
+ // cheapest authenticated read on the real API (see apps/api/src/routes/
41
+ // endpoints.ts), so a passing test here means the key genuinely works.
42
+ test = {
43
+ request: {
44
+ baseURL: '={{$credentials.baseUrl}}',
45
+ url: '/v1/endpoints',
46
+ method: 'GET',
47
+ },
48
+ };
49
+ }
50
+ exports.JitterflowApi = JitterflowApi;
@@ -0,0 +1,4 @@
1
+ <svg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="60" height="60" rx="12" fill="#4F46E5"/>
3
+ <path d="M14 38 L22 38 L26 24 L32 44 L36 30 L40 38 L46 38" stroke="#FFFFFF" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
4
+ </svg>
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.jitterflowApiRequest = jitterflowApiRequest;
4
+ async function jitterflowApiRequest(method, path, body, qs) {
5
+ const credentials = await this.getCredentials('jitterflowApi');
6
+ const baseUrl = String(credentials.baseUrl || 'https://jitterflow.io').replace(/\/+$/, '');
7
+ const options = {
8
+ method,
9
+ url: `${baseUrl}${path}`,
10
+ body,
11
+ qs,
12
+ json: true,
13
+ };
14
+ return this.helpers.httpRequestWithAuthentication.call(this, 'jitterflowApi', options);
15
+ }
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Jitterflow = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const GenericFunctions_1 = require("./GenericFunctions");
6
+ class Jitterflow {
7
+ description = {
8
+ displayName: 'Jitterflow',
9
+ name: 'jitterflow',
10
+ icon: 'file:jitterflow.svg',
11
+ group: ['transform'],
12
+ version: 1,
13
+ subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
14
+ description: 'Send webhooks through jittered, rate-limited delivery and manage the dead-letter queue',
15
+ defaults: { name: 'Jitterflow' },
16
+ usableAsTool: true,
17
+ inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
18
+ outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
19
+ credentials: [
20
+ {
21
+ name: 'jitterflowApi',
22
+ required: true,
23
+ },
24
+ ],
25
+ properties: [
26
+ {
27
+ displayName: 'Resource',
28
+ name: 'resource',
29
+ type: 'options',
30
+ noDataExpression: true,
31
+ options: [
32
+ { name: 'Webhook', value: 'webhook' },
33
+ { name: 'DLQ', value: 'dlq' },
34
+ ],
35
+ default: 'webhook',
36
+ },
37
+ // --- Webhook ---------------------------------------------------
38
+ {
39
+ displayName: 'Operation',
40
+ name: 'operation',
41
+ type: 'options',
42
+ noDataExpression: true,
43
+ displayOptions: { show: { resource: ['webhook'] } },
44
+ options: [
45
+ {
46
+ name: 'Send',
47
+ value: 'send',
48
+ description: 'Queue a webhook for jittered, rate-limited delivery',
49
+ action: 'Send a webhook',
50
+ },
51
+ ],
52
+ default: 'send',
53
+ },
54
+ {
55
+ displayName: 'Endpoint Key',
56
+ name: 'endpointKey',
57
+ type: 'string',
58
+ required: true,
59
+ default: '',
60
+ displayOptions: { show: { resource: ['webhook'], operation: ['send'] } },
61
+ description: 'The endpoint key from your Jitterflow dashboard (Endpoints -> Endpoint Key)',
62
+ },
63
+ {
64
+ displayName: 'Target Identifier',
65
+ name: 'targetIdentifier',
66
+ type: 'string',
67
+ default: '',
68
+ displayOptions: { show: { resource: ['webhook'], operation: ['send'] } },
69
+ description: 'Optional — paces delivery per this identifier (e.g. a mailbox or lead ID) instead of per endpoint. Defaults server-side to the endpoint key if left blank.',
70
+ },
71
+ {
72
+ displayName: 'Idempotency Key',
73
+ name: 'idempotencyKey',
74
+ type: 'string',
75
+ default: '={{$execution.id}}-{{$itemIndex}}',
76
+ displayOptions: { show: { resource: ['webhook'], operation: ['send'] } },
77
+ description: 'Repeating the same key returns the original job instead of creating a second one, so a workflow retry never double-sends. Defaults to the execution ID + item index.',
78
+ },
79
+ {
80
+ displayName: 'Payload',
81
+ name: 'payload',
82
+ type: 'json',
83
+ default: '={{ $json }}',
84
+ displayOptions: { show: { resource: ['webhook'], operation: ['send'] } },
85
+ description: "The JSON body to deliver to the endpoint's destination URL. Defaults to the whole input item.",
86
+ },
87
+ // --- DLQ ---------------------------------------------------------
88
+ {
89
+ displayName: 'Operation',
90
+ name: 'operation',
91
+ type: 'options',
92
+ noDataExpression: true,
93
+ displayOptions: { show: { resource: ['dlq'] } },
94
+ options: [
95
+ {
96
+ name: 'List',
97
+ value: 'list',
98
+ description: 'List dead-letter queue entries',
99
+ action: 'List DLQ entries',
100
+ },
101
+ {
102
+ name: 'Replay',
103
+ value: 'replay',
104
+ description: 'Re-enqueue a failed job for immediate delivery',
105
+ action: 'Replay a DLQ job',
106
+ },
107
+ {
108
+ name: 'Resolve',
109
+ value: 'resolve',
110
+ description: 'Mark a DLQ entry resolved without retrying it',
111
+ action: 'Resolve a DLQ entry',
112
+ },
113
+ ],
114
+ default: 'list',
115
+ },
116
+ {
117
+ displayName: 'Filter',
118
+ name: 'resolvedFilter',
119
+ type: 'options',
120
+ displayOptions: { show: { resource: ['dlq'], operation: ['list'] } },
121
+ options: [
122
+ { name: 'All', value: 'all' },
123
+ { name: 'Unresolved Only', value: 'unresolved' },
124
+ { name: 'Resolved Only', value: 'resolved' },
125
+ ],
126
+ default: 'unresolved',
127
+ },
128
+ {
129
+ displayName: 'Job ID',
130
+ name: 'jobId',
131
+ type: 'string',
132
+ required: true,
133
+ default: '',
134
+ displayOptions: { show: { resource: ['dlq'], operation: ['replay', 'resolve'] } },
135
+ description: 'The WebhookJob ID to replay or resolve (from a prior List DLQ Entries call)',
136
+ },
137
+ ],
138
+ };
139
+ async execute() {
140
+ const items = this.getInputData();
141
+ const returnData = [];
142
+ const resource = this.getNodeParameter('resource', 0);
143
+ const operation = this.getNodeParameter('operation', 0);
144
+ for (let i = 0; i < items.length; i++) {
145
+ try {
146
+ let responseData;
147
+ if (resource === 'webhook' && operation === 'send') {
148
+ const endpointKey = this.getNodeParameter('endpointKey', i);
149
+ const targetIdentifier = this.getNodeParameter('targetIdentifier', i);
150
+ const idempotencyKey = this.getNodeParameter('idempotencyKey', i);
151
+ const payloadRaw = this.getNodeParameter('payload', i);
152
+ const payload = (typeof payloadRaw === 'string' ? JSON.parse(payloadRaw) : payloadRaw);
153
+ const body = { payload };
154
+ if (targetIdentifier)
155
+ body.targetIdentifier = targetIdentifier;
156
+ if (idempotencyKey)
157
+ body.idempotencyKey = idempotencyKey;
158
+ responseData = (await GenericFunctions_1.jitterflowApiRequest.call(this, 'POST', `/v1/ingest/${endpointKey}`, body));
159
+ }
160
+ else if (resource === 'dlq' && operation === 'list') {
161
+ const resolvedFilter = this.getNodeParameter('resolvedFilter', i);
162
+ const qs = {};
163
+ if (resolvedFilter === 'resolved')
164
+ qs.resolved = 'true';
165
+ if (resolvedFilter === 'unresolved')
166
+ qs.resolved = 'false';
167
+ responseData = (await GenericFunctions_1.jitterflowApiRequest.call(this, 'GET', '/v1/dlq', undefined, qs));
168
+ }
169
+ else if (resource === 'dlq' && operation === 'replay') {
170
+ const jobId = this.getNodeParameter('jobId', i);
171
+ responseData = (await GenericFunctions_1.jitterflowApiRequest.call(this, 'POST', `/v1/dlq/${jobId}/retry`));
172
+ }
173
+ else if (resource === 'dlq' && operation === 'resolve') {
174
+ const jobId = this.getNodeParameter('jobId', i);
175
+ responseData = (await GenericFunctions_1.jitterflowApiRequest.call(this, 'POST', `/v1/dlq/${jobId}/resolve`));
176
+ }
177
+ else {
178
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), {
179
+ message: `Unknown resource/operation combination: ${resource}/${operation}`,
180
+ });
181
+ }
182
+ if (Array.isArray(responseData)) {
183
+ returnData.push(...responseData.map((entry) => ({ json: entry, pairedItem: { item: i } })));
184
+ }
185
+ else {
186
+ returnData.push({ json: responseData, pairedItem: { item: i } });
187
+ }
188
+ }
189
+ catch (error) {
190
+ if (this.continueOnFail()) {
191
+ returnData.push({
192
+ json: { error: error.message },
193
+ pairedItem: { item: i },
194
+ });
195
+ continue;
196
+ }
197
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), error);
198
+ }
199
+ }
200
+ return [returnData];
201
+ }
202
+ }
203
+ exports.Jitterflow = Jitterflow;
@@ -0,0 +1,4 @@
1
+ <svg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="60" height="60" rx="12" fill="#4F46E5"/>
3
+ <path d="M14 38 L22 38 L26 24 L32 44 L36 30 L40 38 L46 38" stroke="#FFFFFF" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
4
+ </svg>
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "n8n-nodes-jitterflow",
3
+ "version": "0.1.0",
4
+ "description": "n8n community node for Jitterflow — send webhooks through jittered, rate-limited delivery and manage the dead-letter queue (list, replay, resolve) from inside a workflow.",
5
+ "keywords": [
6
+ "n8n-community-node-package"
7
+ ],
8
+ "license": "MIT",
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "homepage": "https://jitterflow.io/integrations/n8n",
13
+ "bugs": {
14
+ "url": "https://github.com/jitterflow/n8n-nodes-jitterflow/issues"
15
+ },
16
+ "author": {
17
+ "name": "Jitterflow",
18
+ "email": "support@jitterflow.io"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/jitterflow/n8n-nodes-jitterflow.git"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "n8n": {
28
+ "n8nNodesApiVersion": 1,
29
+ "credentials": [
30
+ "dist/credentials/JitterflowApi.credentials.js"
31
+ ],
32
+ "nodes": [
33
+ "dist/nodes/Jitterflow/Jitterflow.node.js"
34
+ ]
35
+ },
36
+ "scripts": {
37
+ "build": "n8n-node build",
38
+ "dev": "n8n-node dev",
39
+ "lint": "n8n-node lint",
40
+ "release": "n8n-node release",
41
+ "test": "jest"
42
+ },
43
+ "peerDependencies": {
44
+ "n8n-workflow": "*"
45
+ },
46
+ "devDependencies": {
47
+ "@n8n/node-cli": "^0.44.5",
48
+ "n8n-workflow": "^2.16.0",
49
+ "typescript": "^5.6.3",
50
+ "jest": "^29.7.0",
51
+ "ts-jest": "^29.2.5",
52
+ "@types/jest": "^29.5.13",
53
+ "@types/node": "^20.16.10"
54
+ }
55
+ }