@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,55 @@
1
+ /**
2
+ * HMAC-SHA256 signature verification for inbound ConfirmX webhooks.
3
+ *
4
+ * Must stay byte-compatible with the verifier in
5
+ * apps/worker/src/lib/webhooks.ts::verifyPayload (and the corresponding
6
+ * outbound signer in apps/api/src/lib/webhooks.ts::signPayload). The
7
+ * signing scheme is `${timestamp}.${body}` keyed by the per-webhook
8
+ * plaintext secret; the receiver must also enforce a 300s skew window
9
+ * to defeat replay.
10
+ */
11
+ import { createHmac, timingSafeEqual } from 'node:crypto'
12
+
13
+ export interface VerifyOpts {
14
+ secret: string
15
+ body: string
16
+ timestamp: string
17
+ signatureHeader: string
18
+ }
19
+
20
+ /**
21
+ * Constant-time verification of the X-ConfirmX-Signature header against
22
+ * HMAC-SHA256(secret, `${timestamp}.${body}`).
23
+ *
24
+ * Returns false on any mismatch, length mismatch, or non-string input.
25
+ * Never throws.
26
+ */
27
+ export function verifyPayload(opts: VerifyOpts): boolean {
28
+ const { secret, body, timestamp, signatureHeader } = opts
29
+
30
+ if (typeof secret !== 'string' || typeof body !== 'string') return false
31
+ if (typeof timestamp !== 'string' || typeof signatureHeader !== 'string') {
32
+ return false
33
+ }
34
+
35
+ const expected =
36
+ 'sha256=' + createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex')
37
+
38
+ const expectedBuf = Buffer.from(expected, 'utf8')
39
+ const providedBuf = Buffer.from(signatureHeader, 'utf8')
40
+ if (expectedBuf.length !== providedBuf.length) return false
41
+ return timingSafeEqual(expectedBuf, providedBuf)
42
+ }
43
+
44
+ /**
45
+ * Returns the absolute skew (in seconds) between the supplied timestamp
46
+ * and now. Replay window enforced by ConfirmX is 300s.
47
+ */
48
+ export function timestampSkewSeconds(timestamp: string, now: number = Date.now()): number {
49
+ const ts = parseInt(timestamp, 10)
50
+ if (!Number.isFinite(ts)) return Number.POSITIVE_INFINITY
51
+ return Math.abs(now / 1000 - ts)
52
+ }
53
+
54
+ /** Replay window in seconds — must match the receiver convention. */
55
+ export const REPLAY_WINDOW_SECONDS = 300
package/types/api.d.ts ADDED
@@ -0,0 +1,199 @@
1
+ /**
2
+ * TypeScript types mirroring the ConfirmX /api/v1/* response shapes.
3
+ *
4
+ * Kept intentionally loose (a lot of `any` on optional nested objects)
5
+ * because the runtime data is what matters — these exist for IDE help
6
+ * and downstream type-narrowing in node implementations.
7
+ *
8
+ * Verified against the ConfirmX API v1 as of 2026-06.
9
+ */
10
+ /** Webhook event types accepted by /api/v1/webhooks. */
11
+ export declare const WEBHOOK_EVENT_TYPES: readonly ["message.received", "conversation.created", "message.sent", "message.status", "template.status"];
12
+ export type WebhookEventType = (typeof WEBHOOK_EVENT_TYPES)[number];
13
+ /** Envelope POSTed to subscribers by ConfirmX. The `data` shape varies by event type. */
14
+ export interface WebhookEnvelope<T = any> {
15
+ id: string;
16
+ type: WebhookEventType;
17
+ createdAt: string;
18
+ organizationId: string;
19
+ data: T;
20
+ }
21
+ export interface Account {
22
+ id: string;
23
+ label: string;
24
+ phoneNumberId?: string;
25
+ businessAccountId?: string;
26
+ isActive: boolean;
27
+ createdAt: string;
28
+ }
29
+ export interface Template {
30
+ id: string;
31
+ name: string;
32
+ language: string;
33
+ status: 'PENDING' | 'APPROVED' | 'REJECTED';
34
+ category: string;
35
+ components: Array<Record<string, any>>;
36
+ }
37
+ export interface ConversationContact {
38
+ id: string;
39
+ phoneNumber: string;
40
+ name?: string | null;
41
+ tags?: Array<{
42
+ id: string;
43
+ name: string;
44
+ }>;
45
+ }
46
+ export interface Conversation {
47
+ id: string;
48
+ whatsappAccountId: string;
49
+ whatsappContactId: string;
50
+ status: 'open' | 'resolved' | 'archived';
51
+ unreadCount: number;
52
+ lastMessageAt: string | null;
53
+ windowExpiresAt: string | null;
54
+ assignedToId: string | null;
55
+ createdAt: string;
56
+ updatedAt: string;
57
+ contact?: ConversationContact;
58
+ account?: {
59
+ id: string;
60
+ label: string;
61
+ phoneNumberId?: string;
62
+ };
63
+ /** Latest message preview (list endpoint only embeds one). */
64
+ messages?: Array<Partial<Message>>;
65
+ assignedTo?: {
66
+ id: string;
67
+ name: string;
68
+ } | null;
69
+ }
70
+ export type MessageDirection = 'inbound' | 'outbound';
71
+ export type MessageType = 'text' | 'image' | 'video' | 'document' | 'audio' | 'voice' | 'button' | 'template';
72
+ export interface Message {
73
+ id: string;
74
+ wamid?: string | null;
75
+ whatsappConversationId: string;
76
+ direction: MessageDirection;
77
+ type: MessageType | string;
78
+ body?: string | null;
79
+ mediaUrl?: string | null;
80
+ mediaFilename?: string | null;
81
+ mediaMimeType?: string | null;
82
+ mediaCaption?: string | null;
83
+ replyToMessageId?: string | null;
84
+ status: string;
85
+ sentAt?: string | null;
86
+ deliveredAt?: string | null;
87
+ readAt?: string | null;
88
+ failedAt?: string | null;
89
+ failureReason?: string | null;
90
+ senderType: string;
91
+ sentByUserId?: string | null;
92
+ sentByApiKeyId?: string | null;
93
+ sentByApiLabel?: string | null;
94
+ sentByWorkflowId?: string | null;
95
+ sentByCampaignId?: string | null;
96
+ createdAt: string;
97
+ updatedAt: string;
98
+ replyToMessage?: Pick<Message, 'id' | 'type' | 'body' | 'direction' | 'mediaFilename' | 'mediaMimeType'> | null;
99
+ sentByUser?: {
100
+ id: string;
101
+ name: string;
102
+ } | null;
103
+ sentByWorkflow?: {
104
+ id: string;
105
+ name: string;
106
+ } | null;
107
+ sentByCampaign?: {
108
+ id: string;
109
+ name: string;
110
+ } | null;
111
+ }
112
+ export interface ListMessagesResult {
113
+ messages: Message[];
114
+ nextCursor: string | null;
115
+ }
116
+ export interface WebhookSubscription {
117
+ id: string;
118
+ url: string;
119
+ events: WebhookEventType[];
120
+ isActive: boolean;
121
+ description: string | null;
122
+ failureCount: number;
123
+ lastDeliveredAt: string | null;
124
+ lastError: string | null;
125
+ createdAt: string;
126
+ }
127
+ export interface WebhookCreateResponse {
128
+ webhook: WebhookSubscription;
129
+ /** Plaintext signing secret — returned ONLY at create time. */
130
+ secret: string;
131
+ secretNote: string;
132
+ }
133
+ export interface MatchedZoneSuccess {
134
+ success: true;
135
+ governorate: {
136
+ id: string;
137
+ name: string;
138
+ externalId: string | null;
139
+ confidence: number;
140
+ };
141
+ area: {
142
+ id: string;
143
+ name: string;
144
+ externalId: string | null;
145
+ confidence: number;
146
+ } | null;
147
+ governorateCorrected?: boolean;
148
+ availableAreas?: string[];
149
+ }
150
+ export interface MatchedZoneGovernorateMissing {
151
+ success: false;
152
+ matched: false;
153
+ error: string;
154
+ input: {
155
+ governorate: string;
156
+ area?: string;
157
+ };
158
+ suggestions: string[];
159
+ }
160
+ export interface MatchedZoneAmbiguous {
161
+ success: false;
162
+ multipleMatches: true;
163
+ areaName: string;
164
+ options: Array<{
165
+ governorateId: string;
166
+ governorateName: string;
167
+ governorateExternalId: string | null;
168
+ areaId: string;
169
+ areaName: string;
170
+ areaExternalId: string | null;
171
+ confidence: number;
172
+ }>;
173
+ message: string;
174
+ }
175
+ export interface MatchedZoneError {
176
+ success: false;
177
+ error: string;
178
+ }
179
+ export type MatchedZone = MatchedZoneSuccess | MatchedZoneGovernorateMissing | MatchedZoneAmbiguous | MatchedZoneError;
180
+ export interface GovernorateTreeNode {
181
+ id: string;
182
+ name: string;
183
+ nameEn: string | null;
184
+ aliases: string[];
185
+ externalId: string | null;
186
+ sortOrder: number;
187
+ areas: Array<{
188
+ id: string;
189
+ governorateId: string;
190
+ name: string;
191
+ nameEn: string | null;
192
+ aliases: string[];
193
+ externalId: string | null;
194
+ sortOrder: number;
195
+ }>;
196
+ }
197
+ export interface ListGovernoratesResult {
198
+ governorates: GovernorateTreeNode[];
199
+ }
package/types/api.js ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ /**
3
+ * TypeScript types mirroring the ConfirmX /api/v1/* response shapes.
4
+ *
5
+ * Kept intentionally loose (a lot of `any` on optional nested objects)
6
+ * because the runtime data is what matters — these exist for IDE help
7
+ * and downstream type-narrowing in node implementations.
8
+ *
9
+ * Verified against the ConfirmX API v1 as of 2026-06.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.WEBHOOK_EVENT_TYPES = void 0;
13
+ /** Webhook event types accepted by /api/v1/webhooks. */
14
+ exports.WEBHOOK_EVENT_TYPES = [
15
+ 'message.received',
16
+ 'conversation.created',
17
+ 'message.sent',
18
+ 'message.status',
19
+ 'template.status',
20
+ ];
21
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["api.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;AAEH,wDAAwD;AAC3C,QAAA,mBAAmB,GAAG;IACjC,kBAAkB;IAClB,sBAAsB;IACtB,cAAc;IACd,gBAAgB;IAChB,iBAAiB;CACT,CAAA"}
package/types/api.ts ADDED
@@ -0,0 +1,238 @@
1
+ /**
2
+ * TypeScript types mirroring the ConfirmX /api/v1/* response shapes.
3
+ *
4
+ * Kept intentionally loose (a lot of `any` on optional nested objects)
5
+ * because the runtime data is what matters — these exist for IDE help
6
+ * and downstream type-narrowing in node implementations.
7
+ *
8
+ * Verified against the ConfirmX API v1 as of 2026-06.
9
+ */
10
+
11
+ /** Webhook event types accepted by /api/v1/webhooks. */
12
+ export const WEBHOOK_EVENT_TYPES = [
13
+ 'message.received',
14
+ 'conversation.created',
15
+ 'message.sent',
16
+ 'message.status',
17
+ 'template.status',
18
+ ] as const
19
+
20
+ export type WebhookEventType = (typeof WEBHOOK_EVENT_TYPES)[number]
21
+
22
+ /** Envelope POSTed to subscribers by ConfirmX. The `data` shape varies by event type. */
23
+ export interface WebhookEnvelope<T = any> {
24
+ id: string
25
+ type: WebhookEventType
26
+ createdAt: string
27
+ organizationId: string
28
+ data: T
29
+ }
30
+
31
+ // ============================================
32
+ // Account
33
+ // ============================================
34
+
35
+ export interface Account {
36
+ id: string
37
+ label: string
38
+ phoneNumberId?: string
39
+ businessAccountId?: string
40
+ isActive: boolean
41
+ createdAt: string
42
+ }
43
+
44
+ // ============================================
45
+ // Template
46
+ // ============================================
47
+
48
+ export interface Template {
49
+ id: string
50
+ name: string
51
+ language: string
52
+ status: 'PENDING' | 'APPROVED' | 'REJECTED'
53
+ category: string
54
+ components: Array<Record<string, any>>
55
+ }
56
+
57
+ // ============================================
58
+ // Conversation
59
+ // ============================================
60
+
61
+ export interface ConversationContact {
62
+ id: string
63
+ phoneNumber: string
64
+ name?: string | null
65
+ tags?: Array<{ id: string; name: string }>
66
+ }
67
+
68
+ export interface Conversation {
69
+ id: string
70
+ whatsappAccountId: string
71
+ whatsappContactId: string
72
+ status: 'open' | 'resolved' | 'archived'
73
+ unreadCount: number
74
+ lastMessageAt: string | null
75
+ windowExpiresAt: string | null
76
+ assignedToId: string | null
77
+ createdAt: string
78
+ updatedAt: string
79
+ contact?: ConversationContact
80
+ account?: { id: string; label: string; phoneNumberId?: string }
81
+ /** Latest message preview (list endpoint only embeds one). */
82
+ messages?: Array<Partial<Message>>
83
+ assignedTo?: { id: string; name: string } | null
84
+ }
85
+
86
+ // ============================================
87
+ // Message
88
+ // ============================================
89
+
90
+ export type MessageDirection = 'inbound' | 'outbound'
91
+ export type MessageType =
92
+ | 'text'
93
+ | 'image'
94
+ | 'video'
95
+ | 'document'
96
+ | 'audio'
97
+ | 'voice'
98
+ | 'button'
99
+ | 'template'
100
+
101
+ export interface Message {
102
+ id: string
103
+ wamid?: string | null
104
+ whatsappConversationId: string
105
+ direction: MessageDirection
106
+ type: MessageType | string
107
+ body?: string | null
108
+ mediaUrl?: string | null
109
+ mediaFilename?: string | null
110
+ mediaMimeType?: string | null
111
+ mediaCaption?: string | null
112
+ replyToMessageId?: string | null
113
+ status: string
114
+ sentAt?: string | null
115
+ deliveredAt?: string | null
116
+ readAt?: string | null
117
+ failedAt?: string | null
118
+ failureReason?: string | null
119
+ senderType: string
120
+ sentByUserId?: string | null
121
+ sentByApiKeyId?: string | null
122
+ sentByApiLabel?: string | null
123
+ sentByWorkflowId?: string | null
124
+ sentByCampaignId?: string | null
125
+ createdAt: string
126
+ updatedAt: string
127
+ replyToMessage?: Pick<Message, 'id' | 'type' | 'body' | 'direction' | 'mediaFilename' | 'mediaMimeType'> | null
128
+ sentByUser?: { id: string; name: string } | null
129
+ sentByWorkflow?: { id: string; name: string } | null
130
+ sentByCampaign?: { id: string; name: string } | null
131
+ }
132
+
133
+ export interface ListMessagesResult {
134
+ messages: Message[]
135
+ nextCursor: string | null
136
+ }
137
+
138
+ // ============================================
139
+ // Webhook subscription
140
+ // ============================================
141
+
142
+ export interface WebhookSubscription {
143
+ id: string
144
+ url: string
145
+ events: WebhookEventType[]
146
+ isActive: boolean
147
+ description: string | null
148
+ failureCount: number
149
+ lastDeliveredAt: string | null
150
+ lastError: string | null
151
+ createdAt: string
152
+ }
153
+
154
+ export interface WebhookCreateResponse {
155
+ webhook: WebhookSubscription
156
+ /** Plaintext signing secret — returned ONLY at create time. */
157
+ secret: string
158
+ secretNote: string
159
+ }
160
+
161
+ // ============================================
162
+ // Shipping zone matcher
163
+ // ============================================
164
+
165
+ export interface MatchedZoneSuccess {
166
+ success: true
167
+ governorate: {
168
+ id: string
169
+ name: string
170
+ externalId: string | null
171
+ confidence: number
172
+ }
173
+ area: {
174
+ id: string
175
+ name: string
176
+ externalId: string | null
177
+ confidence: number
178
+ } | null
179
+ governorateCorrected?: boolean
180
+ availableAreas?: string[]
181
+ }
182
+
183
+ export interface MatchedZoneGovernorateMissing {
184
+ success: false
185
+ matched: false
186
+ error: string
187
+ input: { governorate: string; area?: string }
188
+ suggestions: string[]
189
+ }
190
+
191
+ export interface MatchedZoneAmbiguous {
192
+ success: false
193
+ multipleMatches: true
194
+ areaName: string
195
+ options: Array<{
196
+ governorateId: string
197
+ governorateName: string
198
+ governorateExternalId: string | null
199
+ areaId: string
200
+ areaName: string
201
+ areaExternalId: string | null
202
+ confidence: number
203
+ }>
204
+ message: string
205
+ }
206
+
207
+ export interface MatchedZoneError {
208
+ success: false
209
+ error: string
210
+ }
211
+
212
+ export type MatchedZone =
213
+ | MatchedZoneSuccess
214
+ | MatchedZoneGovernorateMissing
215
+ | MatchedZoneAmbiguous
216
+ | MatchedZoneError
217
+
218
+ export interface GovernorateTreeNode {
219
+ id: string
220
+ name: string
221
+ nameEn: string | null
222
+ aliases: string[]
223
+ externalId: string | null
224
+ sortOrder: number
225
+ areas: Array<{
226
+ id: string
227
+ governorateId: string
228
+ name: string
229
+ nameEn: string | null
230
+ aliases: string[]
231
+ externalId: string | null
232
+ sortOrder: number
233
+ }>
234
+ }
235
+
236
+ export interface ListGovernoratesResult {
237
+ governorates: GovernorateTreeNode[]
238
+ }