@guayaba/workflow-piece-webhook 0.1.33

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.
@@ -0,0 +1,319 @@
1
+ import {
2
+ createTrigger,
3
+ DynamicPropsValue,
4
+ PieceAuth,
5
+ Property,
6
+ TriggerStrategy,
7
+ } from '@guayaba/workflows-framework';
8
+ import {
9
+ assertNotNullOrUndefined,
10
+ MarkdownVariant,
11
+ } from '@guayaba/workflows-shared';
12
+ import { createHmac, timingSafeEqual } from 'crypto';
13
+
14
+ const liveMarkdown = `**Live URL:**
15
+ \`\`\`text
16
+ {{webhookUrl}}
17
+ \`\`\`
18
+ generate sample data & triggers published flow.
19
+
20
+ `;
21
+
22
+ const testMarkdown = `
23
+ **Test URL:**
24
+
25
+ if you want to generate sample data without triggering the flow, append \`/test\` to your webhook URL.
26
+
27
+ `;
28
+
29
+ const syncMarkdown = `**Synchronous Requests:**
30
+
31
+ If you expect a response from this webhook, add \`/sync\` to the end of the URL.
32
+ If it takes more than {{webhookTimeoutSeconds}} seconds, it will return a 408 Request Timeout response.
33
+
34
+ To return data, add an Webhook step to your flow with the Return Response action.
35
+ `;
36
+
37
+ enum AuthType {
38
+ NONE = 'none',
39
+ BASIC = 'basic',
40
+ HEADER = 'header',
41
+ HMAC = 'hmac',
42
+ }
43
+ export const catchWebhook = createTrigger({
44
+ name: 'catch_webhook',
45
+ displayName: 'Catch Webhook',
46
+ description:
47
+ 'Receive incoming HTTP/webhooks using any HTTP method such as GET, POST, PUT, DELETE, etc.',
48
+ props: {
49
+ liveMarkdown: Property.MarkDown({
50
+ value: liveMarkdown,
51
+ variant: MarkdownVariant.BORDERLESS,
52
+ }),
53
+ syncMarkdown: Property.MarkDown({
54
+ value: syncMarkdown,
55
+ variant: MarkdownVariant.INFO,
56
+ }),
57
+ testMarkdown: Property.MarkDown({
58
+ value: testMarkdown,
59
+ variant: MarkdownVariant.INFO,
60
+ }),
61
+ authType: Property.StaticDropdown<AuthType>({
62
+ displayName: 'Authentication',
63
+ required: true,
64
+ defaultValue: 'none',
65
+ options: {
66
+ disabled: false,
67
+ options: [
68
+ { label: 'None', value: AuthType.NONE },
69
+ { label: 'Basic Auth', value: AuthType.BASIC },
70
+ { label: 'Header Auth', value: AuthType.HEADER },
71
+ { label: 'HMAC Signature', value: AuthType.HMAC },
72
+ ],
73
+ },
74
+ }),
75
+ authFields: Property.DynamicProperties({
76
+ auth: PieceAuth.None(),
77
+ displayName: 'Authentication Fields',
78
+ required: false,
79
+ refreshers: ['authType'],
80
+ props: async ({ authType }) => {
81
+ if (!authType) {
82
+ return {};
83
+ }
84
+ const authTypeEnum = authType.toString() as AuthType;
85
+ let fields: DynamicPropsValue = {};
86
+ switch (authTypeEnum) {
87
+ case AuthType.NONE:
88
+ fields = {};
89
+ break;
90
+ case AuthType.BASIC:
91
+ fields = {
92
+ username: Property.ShortText({
93
+ displayName: 'Username',
94
+ description: 'The username to use for authentication.',
95
+ required: true,
96
+ }),
97
+ password: Property.ShortText({
98
+ displayName: 'Password',
99
+ description: 'The password to use for authentication.',
100
+ required: true,
101
+ }),
102
+ };
103
+ break;
104
+ case AuthType.HEADER:
105
+ fields = {
106
+ headerName: Property.ShortText({
107
+ displayName: 'Header Name',
108
+ description:
109
+ 'The name of the header to use for authentication.',
110
+ required: true,
111
+ }),
112
+ headerValue: Property.ShortText({
113
+ displayName: 'Header Value',
114
+ description: 'The value to check against the header.',
115
+ required: true,
116
+ }),
117
+ };
118
+ break;
119
+ case AuthType.HMAC:
120
+ fields = {
121
+ hmacHeaderName: Property.ShortText({
122
+ displayName: 'Signature Header Name',
123
+ description:
124
+ 'The HTTP header containing the HMAC signature (e.g., X-Signature, X-Hub-Signature-256)',
125
+ required: true,
126
+ defaultValue: 'x-signature',
127
+ }),
128
+ hmacSecret: Property.ShortText({
129
+ displayName: 'Secret',
130
+ description:
131
+ 'The shared secret used for HMAC signature verification',
132
+ required: true,
133
+ }),
134
+ hmacAlgorithm: Property.StaticDropdown({
135
+ displayName: 'Algorithm',
136
+ description: 'The hash algorithm used for HMAC computation',
137
+ required: true,
138
+ defaultValue: 'sha256',
139
+ options: {
140
+ disabled: false,
141
+ options: [
142
+ { label: 'SHA-256 (Recommended)', value: 'sha256' },
143
+ { label: 'SHA-1', value: 'sha1' },
144
+ { label: 'SHA-512', value: 'sha512' },
145
+ ],
146
+ },
147
+ }),
148
+ hmacEncoding: Property.StaticDropdown({
149
+ displayName: 'Signature Encoding',
150
+ description: 'How the signature is encoded in the header',
151
+ required: true,
152
+ defaultValue: 'hex',
153
+ options: {
154
+ disabled: false,
155
+ options: [
156
+ { label: 'Hexadecimal', value: 'hex' },
157
+ { label: 'Base64', value: 'base64' },
158
+ ],
159
+ },
160
+ }),
161
+ hmacSignaturePrefix: Property.ShortText({
162
+ displayName: 'Signature Prefix',
163
+ description:
164
+ 'Optional prefix to strip from signature (e.g., "sha256=" for GitHub webhooks). Leave empty if no prefix.',
165
+ required: false,
166
+ defaultValue: '',
167
+ }),
168
+ };
169
+ break;
170
+ default:
171
+ throw new Error('Invalid authentication type');
172
+ }
173
+ return fields;
174
+ },
175
+ }),
176
+ },
177
+ sampleData: null,
178
+ type: TriggerStrategy.WEBHOOK,
179
+ async onEnable() {
180
+ // ignore
181
+ },
182
+ async onDisable() {
183
+ // ignore
184
+ },
185
+ async run(context) {
186
+ const authenticationType = context.propsValue.authType;
187
+ assertNotNullOrUndefined(
188
+ authenticationType,
189
+ 'Authentication type is required'
190
+ );
191
+ const verified = verifyAuth(
192
+ authenticationType,
193
+ context.propsValue.authFields ?? {},
194
+ context.payload.headers,
195
+ context.payload.rawBody
196
+ );
197
+ if (!verified) {
198
+ return [];
199
+ }
200
+ return [context.payload];
201
+ },
202
+ });
203
+
204
+ function verifyAuth(
205
+ authenticationType: AuthType,
206
+ authFields: DynamicPropsValue,
207
+ headers: Record<string, string>,
208
+ rawBody?: unknown
209
+ ): boolean {
210
+ switch (authenticationType) {
211
+ case AuthType.NONE:
212
+ return true;
213
+ case AuthType.BASIC:
214
+ return verifyBasicAuth(
215
+ headers['authorization'],
216
+ authFields['username'],
217
+ authFields['password']
218
+ );
219
+ case AuthType.HEADER:
220
+ return verifyHeaderAuth(
221
+ headers,
222
+ authFields['headerName'],
223
+ authFields['headerValue']
224
+ );
225
+ case AuthType.HMAC:
226
+ return verifyHmacAuth(
227
+ headers,
228
+ rawBody,
229
+ authFields['hmacHeaderName'],
230
+ authFields['hmacSecret'],
231
+ authFields['hmacAlgorithm'] ?? 'sha256',
232
+ authFields['hmacEncoding'] ?? 'hex',
233
+ authFields['hmacSignaturePrefix'] ?? ''
234
+ );
235
+ default:
236
+ throw new Error('Invalid authentication type');
237
+ }
238
+ }
239
+
240
+ function verifyHeaderAuth(
241
+ headers: Record<string, string>,
242
+ headerName: string,
243
+ headerSecret: string
244
+ ) {
245
+ const headerValue = headers[headerName.toLocaleLowerCase()];
246
+ return headerValue === headerSecret;
247
+ }
248
+
249
+ function verifyBasicAuth(
250
+ headerValue: string,
251
+ username: string,
252
+ password: string
253
+ ) {
254
+ if (!headerValue.toLocaleLowerCase().startsWith('basic ')) {
255
+ return false;
256
+ }
257
+ const auth = headerValue.substring(6);
258
+ const decodedAuth = Buffer.from(auth, 'base64').toString();
259
+ const [receivedUsername, receivedPassword] = decodedAuth.split(':');
260
+ return receivedUsername === username && receivedPassword === password;
261
+ }
262
+
263
+ export function verifyHmacAuth(
264
+ headers: Record<string, string>,
265
+ rawBody: unknown,
266
+ headerName: string,
267
+ secret: string,
268
+ algorithm: string,
269
+ encoding: 'hex' | 'base64',
270
+ signaturePrefix: string
271
+ ): boolean {
272
+ // Get signature from header
273
+ const headerValue = headers[headerName.toLowerCase()];
274
+ if (!headerValue) {
275
+ return false;
276
+ }
277
+
278
+ // Strip prefix if specified, or reject if prefix is configured but missing
279
+ let receivedSignature = headerValue;
280
+ if (signaturePrefix) {
281
+ if (!headerValue.startsWith(signaturePrefix)) {
282
+ return false;
283
+ }
284
+ receivedSignature = headerValue.substring(signaturePrefix.length);
285
+ }
286
+
287
+ // Convert rawBody to string for HMAC computation
288
+ // rawBody should ideally be the raw unparsed request body (string or Buffer)
289
+ // to ensure the signature matches the exact bytes sent by the webhook provider.
290
+ let bodyString: string;
291
+ if (rawBody instanceof Buffer) {
292
+ bodyString = rawBody.toString('utf8');
293
+ } else if (typeof rawBody === 'string') {
294
+ bodyString = rawBody;
295
+ } else if (rawBody === undefined || rawBody === null) {
296
+ bodyString = '';
297
+ } else {
298
+ bodyString = JSON.stringify(rawBody);
299
+ }
300
+
301
+ // Compute HMAC
302
+ const hmac = createHmac(algorithm, secret);
303
+ hmac.update(bodyString);
304
+ const expectedSignature = hmac.digest(encoding);
305
+
306
+ // Use timing-safe comparison to prevent timing attacks
307
+ try {
308
+ const expectedBuffer = Buffer.from(expectedSignature);
309
+ const receivedBuffer = Buffer.from(receivedSignature);
310
+
311
+ if (expectedBuffer.length !== receivedBuffer.length) {
312
+ return false;
313
+ }
314
+
315
+ return timingSafeEqual(expectedBuffer, receivedBuffer);
316
+ } catch {
317
+ return false;
318
+ }
319
+ }