@wesell/n8n-nodes-confirmx 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 +21 -0
- package/README.md +77 -0
- package/credentials/ConfirmXApi.credentials.d.ts +18 -0
- package/credentials/ConfirmXApi.credentials.js +31 -0
- package/credentials/ConfirmXApi.credentials.js.map +1 -0
- package/credentials/ConfirmXApi.credentials.ts +40 -0
- package/index.d.ts +14 -0
- package/index.js +26 -0
- package/nodes/ConfirmX/ConfirmXAccount.node.d.ts +5 -0
- package/nodes/ConfirmX/ConfirmXAccount.node.js +81 -0
- package/nodes/ConfirmX/ConfirmXAccount.node.js.map +1 -0
- package/nodes/ConfirmX/ConfirmXAccount.node.ts +81 -0
- package/nodes/ConfirmX/ConfirmXConversation.node.d.ts +13 -0
- package/nodes/ConfirmX/ConfirmXConversation.node.js +266 -0
- package/nodes/ConfirmX/ConfirmXConversation.node.js.map +1 -0
- package/nodes/ConfirmX/ConfirmXConversation.node.ts +263 -0
- package/nodes/ConfirmX/ConfirmXMessage.node.d.ts +13 -0
- package/nodes/ConfirmX/ConfirmXMessage.node.js +364 -0
- package/nodes/ConfirmX/ConfirmXMessage.node.js.map +1 -0
- package/nodes/ConfirmX/ConfirmXMessage.node.ts +361 -0
- package/nodes/ConfirmX/ConfirmXShippingZone.node.d.ts +5 -0
- package/nodes/ConfirmX/ConfirmXShippingZone.node.js +100 -0
- package/nodes/ConfirmX/ConfirmXShippingZone.node.js.map +1 -0
- package/nodes/ConfirmX/ConfirmXShippingZone.node.ts +103 -0
- package/nodes/ConfirmX/ConfirmXTemplate.node.d.ts +13 -0
- package/nodes/ConfirmX/ConfirmXTemplate.node.js +310 -0
- package/nodes/ConfirmX/ConfirmXTemplate.node.js.map +1 -0
- package/nodes/ConfirmX/ConfirmXTemplate.node.ts +310 -0
- package/nodes/ConfirmX/ConfirmXTrigger.node.d.ts +29 -0
- package/nodes/ConfirmX/ConfirmXTrigger.node.js +190 -0
- package/nodes/ConfirmX/ConfirmXTrigger.node.js.map +1 -0
- package/nodes/ConfirmX/ConfirmXTrigger.node.ts +245 -0
- package/nodes/ConfirmX/ConfirmXWebhook.node.d.ts +5 -0
- package/nodes/ConfirmX/ConfirmXWebhook.node.js +169 -0
- package/nodes/ConfirmX/ConfirmXWebhook.node.js.map +1 -0
- package/nodes/ConfirmX/ConfirmXWebhook.node.ts +163 -0
- package/nodes/ConfirmX/confirmx.svg +4 -0
- package/package.json +69 -0
- package/transports/http.d.ts +43 -0
- package/transports/http.js +117 -0
- package/transports/http.js.map +1 -0
- package/transports/http.ts +170 -0
- package/transports/signature.d.ts +21 -0
- package/transports/signature.js +50 -0
- package/transports/signature.js.map +1 -0
- package/transports/signature.ts +55 -0
- package/types/api.d.ts +199 -0
- package/types/api.js +21 -0
- package/types/api.js.map +1 -0
- package/types/api.ts +238 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { INodeType, INodeTypeDescription } from 'n8n-workflow'
|
|
2
|
+
import { confirmxApiRequest } from '../../transports/http'
|
|
3
|
+
import { WEBHOOK_EVENT_TYPES } from '../../types/api'
|
|
4
|
+
|
|
5
|
+
export class ConfirmXWebhook implements INodeType {
|
|
6
|
+
description: INodeTypeDescription = {
|
|
7
|
+
displayName: 'ConfirmX Webhook',
|
|
8
|
+
name: 'confirmXWebhook',
|
|
9
|
+
icon: 'file:confirmx.svg',
|
|
10
|
+
group: ['transform'],
|
|
11
|
+
version: 1,
|
|
12
|
+
subtitle: '={{$parameter["operation"]}}',
|
|
13
|
+
description:
|
|
14
|
+
'Manage ConfirmX outbound webhook subscriptions (list, create, update, delete). Use the ConfirmX Trigger node to receive events automatically.',
|
|
15
|
+
defaults: { name: 'ConfirmX Webhook' },
|
|
16
|
+
inputs: ['main'],
|
|
17
|
+
outputs: ['main'],
|
|
18
|
+
credentials: [{ name: 'confirmXApi', required: true }],
|
|
19
|
+
// Admin-only — not exposed as an AI tool.
|
|
20
|
+
properties: [
|
|
21
|
+
{
|
|
22
|
+
displayName: 'Operation',
|
|
23
|
+
name: 'operation',
|
|
24
|
+
type: 'options',
|
|
25
|
+
noDataExpression: true,
|
|
26
|
+
options: [
|
|
27
|
+
{ name: 'List', value: 'list', action: 'List webhook subscriptions' },
|
|
28
|
+
{ name: 'Create', value: 'create', action: 'Create a webhook subscription' },
|
|
29
|
+
{ name: 'Update', value: 'update', action: 'Update a webhook subscription' },
|
|
30
|
+
{ name: 'Delete', value: 'delete', action: 'Delete a webhook subscription' },
|
|
31
|
+
],
|
|
32
|
+
default: 'list',
|
|
33
|
+
},
|
|
34
|
+
// --- Common ---
|
|
35
|
+
{
|
|
36
|
+
displayName: 'Webhook ID',
|
|
37
|
+
name: 'webhookId',
|
|
38
|
+
type: 'string',
|
|
39
|
+
default: '',
|
|
40
|
+
required: true,
|
|
41
|
+
displayOptions: { show: { operation: ['update', 'delete'] } },
|
|
42
|
+
description: 'The webhook subscription ID',
|
|
43
|
+
},
|
|
44
|
+
// --- Create / Update ---
|
|
45
|
+
{
|
|
46
|
+
displayName: 'URL',
|
|
47
|
+
name: 'url',
|
|
48
|
+
type: 'string',
|
|
49
|
+
default: '',
|
|
50
|
+
required: true,
|
|
51
|
+
displayOptions: { show: { operation: ['create', 'update'] } },
|
|
52
|
+
placeholder: 'https://example.com/webhook',
|
|
53
|
+
description: 'Public URL that ConfirmX will POST events to',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
displayName: 'Events',
|
|
57
|
+
name: 'events',
|
|
58
|
+
type: 'multiOptions',
|
|
59
|
+
default: ['message.received'],
|
|
60
|
+
required: true,
|
|
61
|
+
displayOptions: { show: { operation: ['create', 'update'] } },
|
|
62
|
+
options: WEBHOOK_EVENT_TYPES.map((e) => ({ name: e, value: e })),
|
|
63
|
+
description: 'Event types this subscription should receive',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
displayName: 'Description',
|
|
67
|
+
name: 'description',
|
|
68
|
+
type: 'string',
|
|
69
|
+
default: '',
|
|
70
|
+
displayOptions: { show: { operation: ['create', 'update'] } },
|
|
71
|
+
description: 'Optional human-readable description (max 256 chars)',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
displayName: 'Active',
|
|
75
|
+
name: 'isActive',
|
|
76
|
+
type: 'boolean',
|
|
77
|
+
default: true,
|
|
78
|
+
displayOptions: { show: { operation: ['update'] } },
|
|
79
|
+
description: 'Whether the subscription is active',
|
|
80
|
+
},
|
|
81
|
+
// --- List ---
|
|
82
|
+
{
|
|
83
|
+
displayName: 'Return All',
|
|
84
|
+
name: 'returnAll',
|
|
85
|
+
type: 'boolean',
|
|
86
|
+
default: false,
|
|
87
|
+
displayOptions: { show: { operation: ['list'] } },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
displayName: 'Limit',
|
|
91
|
+
name: 'limit',
|
|
92
|
+
type: 'number',
|
|
93
|
+
default: 50,
|
|
94
|
+
typeOptions: { minValue: 1, maxValue: 200 },
|
|
95
|
+
displayOptions: { show: { operation: ['list'], returnAll: [false] } },
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async execute(this: any) {
|
|
101
|
+
const items = this.getInputData()
|
|
102
|
+
const returnData: any[] = []
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < items.length; i++) {
|
|
105
|
+
const operation = this.getNodeParameter('operation', i) as string
|
|
106
|
+
|
|
107
|
+
if (operation === 'list') {
|
|
108
|
+
const returnAll = this.getNodeParameter('returnAll', i) as boolean
|
|
109
|
+
const limit = (this.getNodeParameter('limit', i) as number) || 50
|
|
110
|
+
const res = await confirmxApiRequest<{ webhooks: any[] }>(this, {
|
|
111
|
+
method: 'GET',
|
|
112
|
+
endpoint: '/webhooks',
|
|
113
|
+
})
|
|
114
|
+
const sliced = returnAll ? res.webhooks || [] : (res.webhooks || []).slice(0, limit)
|
|
115
|
+
returnData.push(...sliced.map((w) => ({ json: w })))
|
|
116
|
+
} else if (operation === 'create') {
|
|
117
|
+
const body = {
|
|
118
|
+
url: this.getNodeParameter('url', i) as string,
|
|
119
|
+
events: this.getNodeParameter('events', i) as string[],
|
|
120
|
+
description: (this.getNodeParameter('description', i) as string) || undefined,
|
|
121
|
+
}
|
|
122
|
+
const res = await confirmxApiRequest<any>(this, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
endpoint: '/webhooks',
|
|
125
|
+
body,
|
|
126
|
+
})
|
|
127
|
+
// Surface the secret prominently — it is returned ONLY at create time.
|
|
128
|
+
returnData.push({
|
|
129
|
+
json: {
|
|
130
|
+
...res,
|
|
131
|
+
_warning: res.secretNote || 'Save the secret now — it cannot be retrieved later.',
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
} else if (operation === 'update') {
|
|
135
|
+
const webhookId = this.getNodeParameter('webhookId', i) as string
|
|
136
|
+
const body: Record<string, any> = {}
|
|
137
|
+
const url = this.getNodeParameter('url', i) as string
|
|
138
|
+
const events = this.getNodeParameter('events', i) as string[]
|
|
139
|
+
const description = this.getNodeParameter('description', i) as string
|
|
140
|
+
const isActive = this.getNodeParameter('isActive', i, true) as boolean
|
|
141
|
+
if (url) body.url = url
|
|
142
|
+
if (events && events.length) body.events = events
|
|
143
|
+
if (description !== undefined) body.description = description
|
|
144
|
+
body.isActive = isActive
|
|
145
|
+
const res = await confirmxApiRequest<{ webhook: any }>(this, {
|
|
146
|
+
method: 'PATCH',
|
|
147
|
+
endpoint: `/webhooks/${webhookId}`,
|
|
148
|
+
body,
|
|
149
|
+
})
|
|
150
|
+
returnData.push({ json: res.webhook })
|
|
151
|
+
} else if (operation === 'delete') {
|
|
152
|
+
const webhookId = this.getNodeParameter('webhookId', i) as string
|
|
153
|
+
const res = await confirmxApiRequest<any>(this, {
|
|
154
|
+
method: 'DELETE',
|
|
155
|
+
endpoint: `/webhooks/${webhookId}`,
|
|
156
|
+
})
|
|
157
|
+
returnData.push({ json: { success: true, webhookId, ...res } })
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return [returnData]
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" width="60" height="60">
|
|
2
|
+
<rect width="60" height="60" rx="12" fill="#25D366"/>
|
|
3
|
+
<path d="M30 12c-9.9 0-18 8.1-18 18 0 3.2.8 6.3 2.4 9l-2.4 9 9.2-2.4c2.6 1.4 5.5 2.2 8.8 2.2 9.9 0 18-8.1 18-18S39.9 12 30 12zm10.7 25.7c-.4 1.2-2.4 2.3-3.3 2.4-.8.1-1.9.2-3-.2-.7-.2-1.6-.5-2.7-1-4.8-2.1-7.9-6.9-8.1-7.2-.2-.3-1.9-2.5-1.9-4.8 0-2.3 1.2-3.4 1.6-3.9.4-.5.9-.6 1.2-.6h.9c.3 0 .7-.1 1 .7.4 1 1.3 3.4 1.4 3.6.1.2.2.5 0 .8-.2.3-.2.5-.5.8-.2.2-.5.6-.7.8-.2.2-.5.5-.2.9.3.5 1.3 2.1 2.7 3.4 1.8 1.6 3.3 2.1 3.8 2.3.5.2.8.2 1.1-.1.3-.3 1.2-1.4 1.5-1.9.3-.5.6-.4 1.1-.3.5.2 3 1.4 3.5 1.7.5.2.9.4 1 .6.1.2.1 1.1-.3 2.2z" fill="#fff"/>
|
|
4
|
+
</svg>
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wesell/n8n-nodes-confirmx",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "n8n community nodes for the ConfirmX WhatsApp Business Platform Public REST API. Exposes 6 resource nodes + 1 webhook trigger, all eligible as AI Agent tools via n8n's usableAsTool.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "wesell",
|
|
8
|
+
"url": "https://github.com/ConfirmX/n8n-nodes-confirmx"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/ConfirmX/n8n-nodes-confirmx#readme",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/ConfirmX/n8n-nodes-confirmx.git"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/ConfirmX/n8n-nodes-confirmx/issues"
|
|
17
|
+
},
|
|
18
|
+
"main": "index.js",
|
|
19
|
+
"types": "index.d.ts",
|
|
20
|
+
"n8n": {
|
|
21
|
+
"n8nNodesApiVersion": 1,
|
|
22
|
+
"credentials": [
|
|
23
|
+
"credentials/ConfirmXApi.credentials.js"
|
|
24
|
+
],
|
|
25
|
+
"nodes": [
|
|
26
|
+
"nodes/ConfirmX/ConfirmXAccount.node.js",
|
|
27
|
+
"nodes/ConfirmX/ConfirmXTemplate.node.js",
|
|
28
|
+
"nodes/ConfirmX/ConfirmXConversation.node.js",
|
|
29
|
+
"nodes/ConfirmX/ConfirmXMessage.node.js",
|
|
30
|
+
"nodes/ConfirmX/ConfirmXWebhook.node.js",
|
|
31
|
+
"nodes/ConfirmX/ConfirmXShippingZone.node.js",
|
|
32
|
+
"nodes/ConfirmX/ConfirmXTrigger.node.js"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"index.js",
|
|
37
|
+
"index.d.ts",
|
|
38
|
+
"credentials/**/*",
|
|
39
|
+
"nodes/**/*",
|
|
40
|
+
"transports/**/*",
|
|
41
|
+
"types/**/*"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc",
|
|
45
|
+
"dev": "tsc --watch",
|
|
46
|
+
"lint": "eslint . --ext .ts",
|
|
47
|
+
"prepack": "tsc",
|
|
48
|
+
"release": "npm run build && npm publish --access public"
|
|
49
|
+
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"n8n-community-node",
|
|
52
|
+
"n8n",
|
|
53
|
+
"confirmx",
|
|
54
|
+
"whatsapp",
|
|
55
|
+
"ai-agent",
|
|
56
|
+
"langchain"
|
|
57
|
+
],
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=18"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"n8n-workflow": "*"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@types/node": "^20.10.0",
|
|
66
|
+
"n8n-workflow": "^2.16.0",
|
|
67
|
+
"typescript": "^5.4.0"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTTP transport for every ConfirmX node in this package.
|
|
3
|
+
*
|
|
4
|
+
* Single `confirmxApiRequest()` function used by all 6 action nodes +
|
|
5
|
+
* the trigger's self-registration logic. Adds the Authorization header,
|
|
6
|
+
* maps ConfirmX's 402 Payment Required into a rich NodeOperationError,
|
|
7
|
+
* and retries 429/5xx with exponential backoff.
|
|
8
|
+
*/
|
|
9
|
+
import type { IExecuteFunctions, IHookFunctions, ILoadOptionsFunctions, ITriggerFunctions, IWebhookFunctions } from 'n8n-workflow';
|
|
10
|
+
type Ctx = IExecuteFunctions | IHookFunctions | ILoadOptionsFunctions | ITriggerFunctions | IWebhookFunctions;
|
|
11
|
+
export interface ApiRequestOpts {
|
|
12
|
+
method: 'GET' | 'POST' | 'PATCH' | 'DELETE';
|
|
13
|
+
endpoint: string;
|
|
14
|
+
body?: unknown;
|
|
15
|
+
qs?: Record<string, string | number | boolean | undefined>;
|
|
16
|
+
headers?: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
export interface ConfirmXCredentials {
|
|
19
|
+
apiKey: string;
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Execute a v1 API request against ConfirmX and return the parsed JSON
|
|
24
|
+
* response. Throws NodeOperationError / NodeApiError on failure.
|
|
25
|
+
*
|
|
26
|
+
* - 402 → NodeOperationError with the topup URL and balance figures
|
|
27
|
+
* (both in USD).
|
|
28
|
+
* - 401/403 → NodeApiError — never retried.
|
|
29
|
+
* - 4xx → NodeApiError with the API error message — never retried.
|
|
30
|
+
* - 429 + 5xx → exponential backoff (500ms, 1s, 2s); max 3 attempts.
|
|
31
|
+
*/
|
|
32
|
+
export declare function confirmxApiRequest<T = any>(ctx: Ctx, opts: ApiRequestOpts): Promise<T>;
|
|
33
|
+
/**
|
|
34
|
+
* Load options helper for `accountId` pickers. Calls
|
|
35
|
+
* GET /api/v1/accounts and maps to {name, value} pairs for n8n's
|
|
36
|
+
* resourceLocator. Caches for 60s in workflow staticData to avoid N+1
|
|
37
|
+
* on flows with many nodes.
|
|
38
|
+
*/
|
|
39
|
+
export declare function loadAccountOptions(ctx: ILoadOptionsFunctions | IExecuteFunctions): Promise<Array<{
|
|
40
|
+
name: string;
|
|
41
|
+
value: string;
|
|
42
|
+
}>>;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.confirmxApiRequest = confirmxApiRequest;
|
|
4
|
+
exports.loadAccountOptions = loadAccountOptions;
|
|
5
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
6
|
+
const MAX_ATTEMPTS = 3;
|
|
7
|
+
const BACKOFF_BASE_MS = 500;
|
|
8
|
+
const REQUEST_TIMEOUT_MS = 30000;
|
|
9
|
+
/**
|
|
10
|
+
* Execute a v1 API request against ConfirmX and return the parsed JSON
|
|
11
|
+
* response. Throws NodeOperationError / NodeApiError on failure.
|
|
12
|
+
*
|
|
13
|
+
* - 402 → NodeOperationError with the topup URL and balance figures
|
|
14
|
+
* (both in USD).
|
|
15
|
+
* - 401/403 → NodeApiError — never retried.
|
|
16
|
+
* - 4xx → NodeApiError with the API error message — never retried.
|
|
17
|
+
* - 429 + 5xx → exponential backoff (500ms, 1s, 2s); max 3 attempts.
|
|
18
|
+
*/
|
|
19
|
+
async function confirmxApiRequest(ctx, opts) {
|
|
20
|
+
const creds = (await ctx.getCredentials('confirmXApi'));
|
|
21
|
+
const baseUrl = (creds.baseUrl || '').replace(/\/$/, '');
|
|
22
|
+
if (!baseUrl) {
|
|
23
|
+
throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), 'ConfirmX Base URL is empty.');
|
|
24
|
+
}
|
|
25
|
+
const url = `${baseUrl}${opts.endpoint}`;
|
|
26
|
+
const headers = {
|
|
27
|
+
Authorization: `Bearer ${creds.apiKey}`,
|
|
28
|
+
Accept: 'application/json',
|
|
29
|
+
...(opts.body !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
30
|
+
...(opts.headers ?? {}),
|
|
31
|
+
};
|
|
32
|
+
let lastError = null;
|
|
33
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
34
|
+
try {
|
|
35
|
+
const response = await ctx.helpers.httpRequest({
|
|
36
|
+
method: opts.method,
|
|
37
|
+
url,
|
|
38
|
+
headers,
|
|
39
|
+
qs: opts.qs,
|
|
40
|
+
body: opts.body !== undefined ? opts.body : undefined,
|
|
41
|
+
json: true,
|
|
42
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
43
|
+
});
|
|
44
|
+
// Empty body (e.g. 204) → return {}
|
|
45
|
+
if (response === undefined || response === null)
|
|
46
|
+
return {};
|
|
47
|
+
return response;
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
lastError = err;
|
|
51
|
+
const status = err?.statusCode ?? err?.response?.statusCode;
|
|
52
|
+
const body = err?.response?.body ?? err?.error ?? null;
|
|
53
|
+
// 402 Payment Required — surface rich error, never retry.
|
|
54
|
+
if (status === 402) {
|
|
55
|
+
const parsed = typeof body === 'string' ? tryParse(body) : body;
|
|
56
|
+
const required = Number(parsed?.requiredMicros ?? 0) / 1000000;
|
|
57
|
+
const current = Number(parsed?.currentBalanceMicros ?? 0) / 1000000;
|
|
58
|
+
const topupUrl = parsed?.topupUrl || '(no topup URL returned)';
|
|
59
|
+
throw new n8n_workflow_1.NodeOperationError(ctx.getNode(), `ConfirmX wallet balance too low ($${current.toFixed(6)} available; $${required.toFixed(6)} required). Top up at ${topupUrl}.`, { description: parsed?.message ?? 'Insufficient wallet balance.' });
|
|
60
|
+
}
|
|
61
|
+
// 401/403 — bad credentials / scope — never retry.
|
|
62
|
+
if (status === 401 || status === 403) {
|
|
63
|
+
throw new n8n_workflow_1.NodeApiError(ctx.getNode(), err, {
|
|
64
|
+
message: `ConfirmX auth failed (${status}). Check the API key and required scopes.`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// 4xx — don't retry; show API error message.
|
|
68
|
+
if (status && status >= 400 && status < 500) {
|
|
69
|
+
const parsed = typeof body === 'string' ? tryParse(body) : body;
|
|
70
|
+
const apiMessage = parsed?.error ?? parsed?.message ?? err?.message ?? 'Unknown error';
|
|
71
|
+
throw new n8n_workflow_1.NodeApiError(ctx.getNode(), err, {
|
|
72
|
+
message: `ConfirmX ${opts.method} ${opts.endpoint} → ${status}: ${apiMessage}`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// 429 or 5xx — backoff and retry.
|
|
76
|
+
if (attempt < MAX_ATTEMPTS) {
|
|
77
|
+
const wait = BACKOFF_BASE_MS * 2 ** (attempt - 1);
|
|
78
|
+
await sleep(wait);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
throw new n8n_workflow_1.NodeApiError(ctx.getNode(), lastError, {
|
|
84
|
+
message: `ConfirmX ${opts.method} ${opts.endpoint} failed after ${MAX_ATTEMPTS} attempts.`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function sleep(ms) {
|
|
88
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
89
|
+
}
|
|
90
|
+
function tryParse(s) {
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(s);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Load options helper for `accountId` pickers. Calls
|
|
100
|
+
* GET /api/v1/accounts and maps to {name, value} pairs for n8n's
|
|
101
|
+
* resourceLocator. Caches for 60s in workflow staticData to avoid N+1
|
|
102
|
+
* on flows with many nodes.
|
|
103
|
+
*/
|
|
104
|
+
async function loadAccountOptions(ctx) {
|
|
105
|
+
const staticData = ctx.getWorkflowStaticData('global');
|
|
106
|
+
const cached = staticData.__confirmx_accounts;
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
if (cached && now - cached.ts < 60000)
|
|
109
|
+
return cached.options;
|
|
110
|
+
const res = await confirmxApiRequest(ctx, { method: 'GET', endpoint: '/accounts' });
|
|
111
|
+
const options = (res.accounts || [])
|
|
112
|
+
.filter((a) => a.isActive)
|
|
113
|
+
.map((a) => ({ name: a.label || a.id, value: a.id }));
|
|
114
|
+
staticData.__confirmx_accounts = { ts: now, options };
|
|
115
|
+
return options;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HTTP transport for every ConfirmX node in this package.
|
|
3
|
+
*
|
|
4
|
+
* Single `confirmxApiRequest()` function used by all 6 action nodes +
|
|
5
|
+
* the trigger's self-registration logic. Adds the Authorization header,
|
|
6
|
+
* maps ConfirmX's 402 Payment Required into a rich NodeOperationError,
|
|
7
|
+
* and retries 429/5xx with exponential backoff.
|
|
8
|
+
*/
|
|
9
|
+
import type {
|
|
10
|
+
IExecuteFunctions,
|
|
11
|
+
IHookFunctions,
|
|
12
|
+
ILoadOptionsFunctions,
|
|
13
|
+
ITriggerFunctions,
|
|
14
|
+
IWebhookFunctions,
|
|
15
|
+
} from 'n8n-workflow'
|
|
16
|
+
import { NodeApiError, NodeOperationError } from 'n8n-workflow'
|
|
17
|
+
|
|
18
|
+
type Ctx =
|
|
19
|
+
| IExecuteFunctions
|
|
20
|
+
| IHookFunctions
|
|
21
|
+
| ILoadOptionsFunctions
|
|
22
|
+
| ITriggerFunctions
|
|
23
|
+
| IWebhookFunctions
|
|
24
|
+
|
|
25
|
+
export interface ApiRequestOpts {
|
|
26
|
+
method: 'GET' | 'POST' | 'PATCH' | 'DELETE'
|
|
27
|
+
endpoint: string
|
|
28
|
+
body?: unknown
|
|
29
|
+
qs?: Record<string, string | number | boolean | undefined>
|
|
30
|
+
headers?: Record<string, string>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ConfirmXCredentials {
|
|
34
|
+
apiKey: string
|
|
35
|
+
baseUrl: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const MAX_ATTEMPTS = 3
|
|
39
|
+
const BACKOFF_BASE_MS = 500
|
|
40
|
+
const REQUEST_TIMEOUT_MS = 30_000
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Execute a v1 API request against ConfirmX and return the parsed JSON
|
|
44
|
+
* response. Throws NodeOperationError / NodeApiError on failure.
|
|
45
|
+
*
|
|
46
|
+
* - 402 → NodeOperationError with the topup URL and balance figures
|
|
47
|
+
* (both in USD).
|
|
48
|
+
* - 401/403 → NodeApiError — never retried.
|
|
49
|
+
* - 4xx → NodeApiError with the API error message — never retried.
|
|
50
|
+
* - 429 + 5xx → exponential backoff (500ms, 1s, 2s); max 3 attempts.
|
|
51
|
+
*/
|
|
52
|
+
export async function confirmxApiRequest<T = any>(
|
|
53
|
+
ctx: Ctx,
|
|
54
|
+
opts: ApiRequestOpts,
|
|
55
|
+
): Promise<T> {
|
|
56
|
+
const creds = (await ctx.getCredentials('confirmXApi')) as unknown as ConfirmXCredentials
|
|
57
|
+
const baseUrl = (creds.baseUrl || '').replace(/\/$/, '')
|
|
58
|
+
if (!baseUrl) {
|
|
59
|
+
throw new NodeOperationError(ctx.getNode(), 'ConfirmX Base URL is empty.')
|
|
60
|
+
}
|
|
61
|
+
const url = `${baseUrl}${opts.endpoint}`
|
|
62
|
+
|
|
63
|
+
const headers: Record<string, string> = {
|
|
64
|
+
Authorization: `Bearer ${creds.apiKey}`,
|
|
65
|
+
Accept: 'application/json',
|
|
66
|
+
...(opts.body !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
67
|
+
...(opts.headers ?? {}),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let lastError: unknown = null
|
|
71
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
72
|
+
try {
|
|
73
|
+
const response = await ctx.helpers.httpRequest({
|
|
74
|
+
method: opts.method,
|
|
75
|
+
url,
|
|
76
|
+
headers,
|
|
77
|
+
qs: opts.qs,
|
|
78
|
+
body: opts.body !== undefined ? (opts.body as any) : undefined,
|
|
79
|
+
json: true,
|
|
80
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
81
|
+
})
|
|
82
|
+
// Empty body (e.g. 204) → return {}
|
|
83
|
+
if (response === undefined || response === null) return {} as T
|
|
84
|
+
return response as T
|
|
85
|
+
} catch (err: any) {
|
|
86
|
+
lastError = err
|
|
87
|
+
const status: number | undefined = err?.statusCode ?? err?.response?.statusCode
|
|
88
|
+
const body: any = err?.response?.body ?? err?.error ?? null
|
|
89
|
+
|
|
90
|
+
// 402 Payment Required — surface rich error, never retry.
|
|
91
|
+
if (status === 402) {
|
|
92
|
+
const parsed = typeof body === 'string' ? tryParse(body) : body
|
|
93
|
+
const required = Number(parsed?.requiredMicros ?? 0) / 1_000_000
|
|
94
|
+
const current = Number(parsed?.currentBalanceMicros ?? 0) / 1_000_000
|
|
95
|
+
const topupUrl = parsed?.topupUrl || '(no topup URL returned)'
|
|
96
|
+
throw new NodeOperationError(
|
|
97
|
+
ctx.getNode(),
|
|
98
|
+
`ConfirmX wallet balance too low ($${current.toFixed(6)} available; $${required.toFixed(6)} required). Top up at ${topupUrl}.`,
|
|
99
|
+
{ description: parsed?.message ?? 'Insufficient wallet balance.' },
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 401/403 — bad credentials / scope — never retry.
|
|
104
|
+
if (status === 401 || status === 403) {
|
|
105
|
+
throw new NodeApiError(ctx.getNode(), err, {
|
|
106
|
+
message: `ConfirmX auth failed (${status}). Check the API key and required scopes.`,
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 4xx — don't retry; show API error message.
|
|
111
|
+
if (status && status >= 400 && status < 500) {
|
|
112
|
+
const parsed = typeof body === 'string' ? tryParse(body) : body
|
|
113
|
+
const apiMessage = parsed?.error ?? parsed?.message ?? err?.message ?? 'Unknown error'
|
|
114
|
+
throw new NodeApiError(ctx.getNode(), err, {
|
|
115
|
+
message: `ConfirmX ${opts.method} ${opts.endpoint} → ${status}: ${apiMessage}`,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 429 or 5xx — backoff and retry.
|
|
120
|
+
if (attempt < MAX_ATTEMPTS) {
|
|
121
|
+
const wait = BACKOFF_BASE_MS * 2 ** (attempt - 1)
|
|
122
|
+
await sleep(wait)
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
throw new NodeApiError(ctx.getNode(), lastError as any, {
|
|
129
|
+
message: `ConfirmX ${opts.method} ${opts.endpoint} failed after ${MAX_ATTEMPTS} attempts.`,
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function sleep(ms: number): Promise<void> {
|
|
134
|
+
return new Promise((r) => setTimeout(r, ms))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function tryParse(s: string): any {
|
|
138
|
+
try {
|
|
139
|
+
return JSON.parse(s)
|
|
140
|
+
} catch {
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Load options helper for `accountId` pickers. Calls
|
|
147
|
+
* GET /api/v1/accounts and maps to {name, value} pairs for n8n's
|
|
148
|
+
* resourceLocator. Caches for 60s in workflow staticData to avoid N+1
|
|
149
|
+
* on flows with many nodes.
|
|
150
|
+
*/
|
|
151
|
+
export async function loadAccountOptions(
|
|
152
|
+
ctx: ILoadOptionsFunctions | IExecuteFunctions,
|
|
153
|
+
): Promise<Array<{ name: string; value: string }>> {
|
|
154
|
+
const staticData = ctx.getWorkflowStaticData('global') as Record<string, any>
|
|
155
|
+
const cached = staticData.__confirmx_accounts as
|
|
156
|
+
| { ts: number; options: Array<{ name: string; value: string }> }
|
|
157
|
+
| undefined
|
|
158
|
+
const now = Date.now()
|
|
159
|
+
if (cached && now - cached.ts < 60_000) return cached.options
|
|
160
|
+
|
|
161
|
+
const res = await confirmxApiRequest<{ accounts: Array<{ id: string; label: string; isActive: boolean }> }>(
|
|
162
|
+
ctx as any,
|
|
163
|
+
{ method: 'GET', endpoint: '/accounts' },
|
|
164
|
+
)
|
|
165
|
+
const options = (res.accounts || [])
|
|
166
|
+
.filter((a) => a.isActive)
|
|
167
|
+
.map((a) => ({ name: a.label || a.id, value: a.id }))
|
|
168
|
+
staticData.__confirmx_accounts = { ts: now, options }
|
|
169
|
+
return options
|
|
170
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface VerifyOpts {
|
|
2
|
+
secret: string;
|
|
3
|
+
body: string;
|
|
4
|
+
timestamp: string;
|
|
5
|
+
signatureHeader: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Constant-time verification of the X-ConfirmX-Signature header against
|
|
9
|
+
* HMAC-SHA256(secret, `${timestamp}.${body}`).
|
|
10
|
+
*
|
|
11
|
+
* Returns false on any mismatch, length mismatch, or non-string input.
|
|
12
|
+
* Never throws.
|
|
13
|
+
*/
|
|
14
|
+
export declare function verifyPayload(opts: VerifyOpts): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Returns the absolute skew (in seconds) between the supplied timestamp
|
|
17
|
+
* and now. Replay window enforced by ConfirmX is 300s.
|
|
18
|
+
*/
|
|
19
|
+
export declare function timestampSkewSeconds(timestamp: string, now?: number): number;
|
|
20
|
+
/** Replay window in seconds — must match the receiver convention. */
|
|
21
|
+
export declare const REPLAY_WINDOW_SECONDS = 300;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REPLAY_WINDOW_SECONDS = void 0;
|
|
4
|
+
exports.verifyPayload = verifyPayload;
|
|
5
|
+
exports.timestampSkewSeconds = timestampSkewSeconds;
|
|
6
|
+
/**
|
|
7
|
+
* HMAC-SHA256 signature verification for inbound ConfirmX webhooks.
|
|
8
|
+
*
|
|
9
|
+
* Must stay byte-compatible with the verifier in
|
|
10
|
+
* apps/worker/src/lib/webhooks.ts::verifyPayload (and the corresponding
|
|
11
|
+
* outbound signer in apps/api/src/lib/webhooks.ts::signPayload). The
|
|
12
|
+
* signing scheme is `${timestamp}.${body}` keyed by the per-webhook
|
|
13
|
+
* plaintext secret; the receiver must also enforce a 300s skew window
|
|
14
|
+
* to defeat replay.
|
|
15
|
+
*/
|
|
16
|
+
const node_crypto_1 = require("node:crypto");
|
|
17
|
+
/**
|
|
18
|
+
* Constant-time verification of the X-ConfirmX-Signature header against
|
|
19
|
+
* HMAC-SHA256(secret, `${timestamp}.${body}`).
|
|
20
|
+
*
|
|
21
|
+
* Returns false on any mismatch, length mismatch, or non-string input.
|
|
22
|
+
* Never throws.
|
|
23
|
+
*/
|
|
24
|
+
function verifyPayload(opts) {
|
|
25
|
+
const { secret, body, timestamp, signatureHeader } = opts;
|
|
26
|
+
if (typeof secret !== 'string' || typeof body !== 'string')
|
|
27
|
+
return false;
|
|
28
|
+
if (typeof timestamp !== 'string' || typeof signatureHeader !== 'string') {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const expected = 'sha256=' + (0, node_crypto_1.createHmac)('sha256', secret).update(`${timestamp}.${body}`).digest('hex');
|
|
32
|
+
const expectedBuf = Buffer.from(expected, 'utf8');
|
|
33
|
+
const providedBuf = Buffer.from(signatureHeader, 'utf8');
|
|
34
|
+
if (expectedBuf.length !== providedBuf.length)
|
|
35
|
+
return false;
|
|
36
|
+
return (0, node_crypto_1.timingSafeEqual)(expectedBuf, providedBuf);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Returns the absolute skew (in seconds) between the supplied timestamp
|
|
40
|
+
* and now. Replay window enforced by ConfirmX is 300s.
|
|
41
|
+
*/
|
|
42
|
+
function timestampSkewSeconds(timestamp, now = Date.now()) {
|
|
43
|
+
const ts = parseInt(timestamp, 10);
|
|
44
|
+
if (!Number.isFinite(ts))
|
|
45
|
+
return Number.POSITIVE_INFINITY;
|
|
46
|
+
return Math.abs(now / 1000 - ts);
|
|
47
|
+
}
|
|
48
|
+
/** Replay window in seconds — must match the receiver convention. */
|
|
49
|
+
exports.REPLAY_WINDOW_SECONDS = 300;
|
|
50
|
+
//# sourceMappingURL=signature.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signature.js","sourceRoot":"","sources":["signature.ts"],"names":[],"mappings":";;;AA0BA,sCAeC;AAMD,oDAIC;AAnDD;;;;;;;;;GASG;AACH,6CAAyD;AASzD;;;;;;GAMG;AACH,SAAgB,aAAa,CAAC,IAAgB;IAC5C,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,eAAe,EAAE,GAAG,IAAI,CAAA;IAEzD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IACxE,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,OAAO,eAAe,KAAK,QAAQ,EAAE,CAAC;QACzE,OAAO,KAAK,CAAA;IACd,CAAC;IAED,MAAM,QAAQ,GACZ,SAAS,GAAG,IAAA,wBAAU,EAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,GAAG,SAAS,IAAI,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAEvF,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;IACjD,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC,CAAA;IACxD,IAAI,WAAW,CAAC,MAAM,KAAK,WAAW,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IAC3D,OAAO,IAAA,6BAAe,EAAC,WAAW,EAAE,WAAW,CAAC,CAAA;AAClD,CAAC;AAED;;;GAGG;AACH,SAAgB,oBAAoB,CAAC,SAAiB,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IAC9E,MAAM,EAAE,GAAG,QAAQ,CAAC,SAAS,EAAE,EAAE,CAAC,CAAA;IAClC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAAE,OAAO,MAAM,CAAC,iBAAiB,CAAA;IACzD,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC,CAAA;AAClC,CAAC;AAED,qEAAqE;AACxD,QAAA,qBAAqB,GAAG,GAAG,CAAA"}
|