@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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/credentials/ConfirmXApi.credentials.d.ts +18 -0
  4. package/credentials/ConfirmXApi.credentials.js +31 -0
  5. package/credentials/ConfirmXApi.credentials.js.map +1 -0
  6. package/credentials/ConfirmXApi.credentials.ts +40 -0
  7. package/index.d.ts +14 -0
  8. package/index.js +26 -0
  9. package/nodes/ConfirmX/ConfirmXAccount.node.d.ts +5 -0
  10. package/nodes/ConfirmX/ConfirmXAccount.node.js +81 -0
  11. package/nodes/ConfirmX/ConfirmXAccount.node.js.map +1 -0
  12. package/nodes/ConfirmX/ConfirmXAccount.node.ts +81 -0
  13. package/nodes/ConfirmX/ConfirmXConversation.node.d.ts +13 -0
  14. package/nodes/ConfirmX/ConfirmXConversation.node.js +266 -0
  15. package/nodes/ConfirmX/ConfirmXConversation.node.js.map +1 -0
  16. package/nodes/ConfirmX/ConfirmXConversation.node.ts +263 -0
  17. package/nodes/ConfirmX/ConfirmXMessage.node.d.ts +13 -0
  18. package/nodes/ConfirmX/ConfirmXMessage.node.js +364 -0
  19. package/nodes/ConfirmX/ConfirmXMessage.node.js.map +1 -0
  20. package/nodes/ConfirmX/ConfirmXMessage.node.ts +361 -0
  21. package/nodes/ConfirmX/ConfirmXShippingZone.node.d.ts +5 -0
  22. package/nodes/ConfirmX/ConfirmXShippingZone.node.js +100 -0
  23. package/nodes/ConfirmX/ConfirmXShippingZone.node.js.map +1 -0
  24. package/nodes/ConfirmX/ConfirmXShippingZone.node.ts +103 -0
  25. package/nodes/ConfirmX/ConfirmXTemplate.node.d.ts +13 -0
  26. package/nodes/ConfirmX/ConfirmXTemplate.node.js +310 -0
  27. package/nodes/ConfirmX/ConfirmXTemplate.node.js.map +1 -0
  28. package/nodes/ConfirmX/ConfirmXTemplate.node.ts +310 -0
  29. package/nodes/ConfirmX/ConfirmXTrigger.node.d.ts +29 -0
  30. package/nodes/ConfirmX/ConfirmXTrigger.node.js +190 -0
  31. package/nodes/ConfirmX/ConfirmXTrigger.node.js.map +1 -0
  32. package/nodes/ConfirmX/ConfirmXTrigger.node.ts +245 -0
  33. package/nodes/ConfirmX/ConfirmXWebhook.node.d.ts +5 -0
  34. package/nodes/ConfirmX/ConfirmXWebhook.node.js +169 -0
  35. package/nodes/ConfirmX/ConfirmXWebhook.node.js.map +1 -0
  36. package/nodes/ConfirmX/ConfirmXWebhook.node.ts +163 -0
  37. package/nodes/ConfirmX/confirmx.svg +4 -0
  38. package/package.json +69 -0
  39. package/transports/http.d.ts +43 -0
  40. package/transports/http.js +117 -0
  41. package/transports/http.js.map +1 -0
  42. package/transports/http.ts +170 -0
  43. package/transports/signature.d.ts +21 -0
  44. package/transports/signature.js +50 -0
  45. package/transports/signature.js.map +1 -0
  46. package/transports/signature.ts +55 -0
  47. package/types/api.d.ts +199 -0
  48. package/types/api.js +21 -0
  49. package/types/api.js.map +1 -0
  50. 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"}