1 | import * as crypto from 'crypto'; |
2 | import * as wmill from "windmill-client"; |
3 |
|
4 | const SECRET_KEY_VARIABLE_PATH = "secret_key_path"; |
5 |
|
6 | |
7 | * Trigger Preprocessor |
8 | * |
9 | * ⚠️ This function runs BEFORE the main function. |
10 | * |
11 | * Windmill allows you to define a `preprocessor` for any trigger type (HTTP, WebSocket, Kafka, Email, etc.). |
12 | * The preprocessor gives you the ability to perform custom logic such as validation, transformation, or authentication |
13 | * before the `main()` function is executed. |
14 | * |
15 | * In this example: |
16 | * - The trigger kind is `http`, which means we have access to HTTP-specific metadata. |
17 | * - We use `wm_trigger.http.headers` to extract custom headers from the incoming HTTP request, such as: |
18 | * - `x-signature` for verifying the integrity and authenticity of the request. |
19 | * - `x-timestamp` to guard against replay attacks by validating the request time. |
20 | * - The `raw_string` argument contains the **raw JSON body** of the request as a string — which is crucial for verifying the HMAC signature. |
21 | * |
22 | * ⚠️ **Important:** `raw_string` is required for signature verification in this example. |
23 | * Make sure the **"raw body"** option is enabled in your HTTP route configuration. |
24 | * If it's not enabled, `raw_string` will be undefined and the script will throw an error. |
25 | * |
26 | * Once the signature and timestamp are verified, we parse the raw JSON body and return the parsed payload as `body`, which is passed directly into the `main()` function as a named argument. |
27 | * |
28 | * Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors |
29 | */ |
30 | export async function preprocessor( |
31 | event: { |
32 | kind: 'http'; |
33 | body: any; |
34 | raw_string: string | null; |
35 | route: string; |
36 | path: string; |
37 | method: string; |
38 | params: Record<string, string>; |
39 | query: Record<string, string>; |
40 | headers: Record<string, string>; |
41 | }, |
42 | ) { |
43 | if (event.kind === 'http') { |
44 |
|
45 | if (!event.raw_string) { |
46 | throw new Error("Missing raw body. Ensure the 'raw body' option is enabled in the HTTP route configuration."); |
47 | } |
48 |
|
49 | |
50 | const signature = event.headers['x-signature'] || event.headers['signature']; |
51 | if (!signature) { |
52 | throw new Error('Missing signature in request headers.'); |
53 | } |
54 |
|
55 | |
56 | const timestamp = event.headers['x-timestamp'] || event.headers['timestamp']; |
57 | if (timestamp) { |
58 | const timestampValue = parseInt(timestamp, 10); |
59 | const currentTime = Math.floor(Date.now() / 1000); |
60 | const TIME_WINDOW_SECONDS = 5 * 60; |
61 |
|
62 | if (isNaN(timestampValue)) { |
63 | throw new Error('Invalid timestamp format.'); |
64 | } |
65 |
|
66 | if (Math.abs(currentTime - timestampValue) > TIME_WINDOW_SECONDS) { |
67 | throw new Error('Request timestamp is outside the acceptable time window.'); |
68 | } |
69 | } |
70 |
|
71 | |
72 | const isValidSignature = await verifySignature(signature, event.raw_string, timestamp); |
73 | if (!isValidSignature) { |
74 | throw new Error('Invalid signature.'); |
75 | } |
76 |
|
77 | |
78 | let parsedBody: any = {}; |
79 | try { |
80 | parsedBody = JSON.parse(event.raw_string); |
81 | } catch (error: unknown) { |
82 | const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error'; |
83 | throw new Error(`Failed to parse request body: ${errorMessage}`); |
84 | } |
85 |
|
86 | |
87 | return { |
88 | body: parsedBody |
89 | }; |
90 | } |
91 |
|
92 | throw new Error(`Expected trigger of kind 'http', but received: ${event.kind}`); |
93 | } |
94 |
|
95 | |
96 | * Verifies the HMAC-SHA256 signature against the raw body and optional timestamp. |
97 | * |
98 | * @param signature - The signature from request headers |
99 | * @param body - Raw request body as a string |
100 | * @param timestamp - Optional timestamp from request headers |
101 | * @returns boolean indicating if the signature is valid |
102 | */ |
103 | async function verifySignature(signature: string, body?: string, timestamp?: string): Promise<boolean> { |
104 | const dataToVerify = timestamp |
105 | ? `${body || ''}${timestamp}` |
106 | : (body || ''); |
107 |
|
108 | const secretKey = await wmill.getVariable(SECRET_KEY_VARIABLE_PATH); |
109 |
|
110 | const expectedSignature = crypto |
111 | .createHmac('sha256', secretKey || '') |
112 | .update(dataToVerify) |
113 | .digest('hex'); |
114 |
|
115 | try { |
116 | return crypto.timingSafeEqual( |
117 | Buffer.from(signature), |
118 | Buffer.from(expectedSignature) |
119 | ); |
120 | } catch (error: unknown) { |
121 | console.error('Signature comparison error:', error); |
122 | return false; |
123 | } |
124 |
|
125 | |
126 | } |
127 |
|
128 | |
129 | * Main Function - Handles processed trigger events |
130 | * |
131 | * ⚠️ Called AFTER `preprocessor()`, with its return values. |
132 | * |
133 | * @param body - Parsed request body |
134 | */ |
135 | export async function main(body: any) { |
136 | |
137 | return { |
138 | statusCode: 200, |
139 | body: { |
140 | message: "Request authenticated successfully", |
141 | receivedData: body, |
142 | } |
143 | }; |
144 | } |