@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.
- package/.eslintrc.json +33 -0
- package/README.md +5 -0
- package/assets/logo.png +0 -0
- package/package.json +26 -0
- package/src/i18n/ar.json +28 -0
- package/src/i18n/bg.json +28 -0
- package/src/i18n/ca.json +28 -0
- package/src/i18n/de.json +27 -0
- package/src/i18n/es.json +27 -0
- package/src/i18n/fr.json +27 -0
- package/src/i18n/hi.json +28 -0
- package/src/i18n/hu.json +28 -0
- package/src/i18n/hy.json +28 -0
- package/src/i18n/id.json +28 -0
- package/src/i18n/it.json +28 -0
- package/src/i18n/ja.json +27 -0
- package/src/i18n/ko.json +28 -0
- package/src/i18n/nl.json +27 -0
- package/src/i18n/pl.json +28 -0
- package/src/i18n/pt.json +27 -0
- package/src/i18n/ru.json +28 -0
- package/src/i18n/sv.json +28 -0
- package/src/i18n/translation.json +27 -0
- package/src/i18n/uk.json +28 -0
- package/src/i18n/vi.json +28 -0
- package/src/i18n/zh.json +27 -0
- package/src/index.ts +17 -0
- package/src/lib/actions/return-response-and-wait-for-next-webhook.ts +163 -0
- package/src/lib/actions/return-response.ts +171 -0
- package/src/lib/triggers/catch-hook.ts +319 -0
- package/test/hmac-verification.test.ts +476 -0
- package/tsconfig.json +19 -0
- package/tsconfig.lib.json +15 -0
- package/tsconfig.spec.json +17 -0
- package/vitest.config.ts +18 -0
|
@@ -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
|
+
}
|