Edits history of script submission #11474 for ' HTTP route script with signature verification template (windmill)'

  • bun
    One script reply has been approved by the moderators
    Ap­pro­ved
    import * as crypto from 'crypto';
    import * as wmill from "windmill-client";
    
    const SECRET_KEY_VARIABLE_PATH = "secret_key_path";
    
    /**
     * Trigger Preprocessor
     *
     * ⚠️ This function runs BEFORE the main function.
     *
     * Windmill allows you to define a `preprocessor` for any trigger type (HTTP, WebSocket, Kafka, Email, etc.).
     * The preprocessor gives you the ability to perform custom logic such as validation, transformation, or authentication
     * before the `main()` function is executed.
     *
     * In this example:
     * - The trigger kind is `http`, which means we have access to HTTP-specific metadata.
     * - We use `wm_trigger.http.headers` to extract custom headers from the incoming HTTP request, such as:
     *   - `x-signature` for verifying the integrity and authenticity of the request.
     *   - `x-timestamp` to guard against replay attacks by validating the request time.
     * - The `raw_string` argument contains the **raw JSON body** of the request as a string — which is crucial for verifying the HMAC signature.
     *
     * ⚠️ **Important:** `raw_string` is required for signature verification in this example. 
     * Make sure the **"raw body"** option is enabled in your HTTP route configuration. 
     * If it's not enabled, `raw_string` will be undefined and the script will throw an error.
     *
     * 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.
     *
     * Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors
     */
    export async function preprocessor(
      event: {
        kind: 'http';
        body: any;
        raw_string: string | null;
        route: string;
        path: string;
        method: string;
        params: Record<string, string>;
        query: Record<string, string>;
        headers: Record<string, string>;
      },
    ) {
      if (event.kind === 'http') {
    
        if (!event.raw_string) {
          throw new Error("Missing raw body. Ensure the 'raw body' option is enabled in the HTTP route configuration.");
        }
    
        // Extract signature from headers
        const signature = event.headers['x-signature'] || event.headers['signature'];
        if (!signature) {
          throw new Error('Missing signature in request headers.');
        }
    
        // Check timestamp if present to prevent replay attacks
        const timestamp = event.headers['x-timestamp'] || event.headers['timestamp'];
        if (timestamp) {
          const timestampValue = parseInt(timestamp, 10);
          const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
          const TIME_WINDOW_SECONDS = 5 * 60; // 5 minutes
    
          if (isNaN(timestampValue)) {
            throw new Error('Invalid timestamp format.');
          }
    
          if (Math.abs(currentTime - timestampValue) > TIME_WINDOW_SECONDS) {
            throw new Error('Request timestamp is outside the acceptable time window.');
          }
        }
    
        // Verify the signature
        const isValidSignature = await verifySignature(signature, event.raw_string, timestamp);
        if (!isValidSignature) {
          throw new Error('Invalid signature.');
        }
    
        // Parse the body if it's JSON (with error handling)
        let parsedBody: any = {};
        try {
          parsedBody = JSON.parse(event.raw_string);
        } catch (error: unknown) {
          const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error';
          throw new Error(`Failed to parse request body: ${errorMessage}`);
        }
    
        // Return both HTTP details and the parsed body to main
        return {
          body: parsedBody
        };
      }
    
      throw new Error(`Expected trigger of kind 'http', but received: ${event.kind}`);
    }
    
    /**
     * Verifies the HMAC-SHA256 signature against the raw body and optional timestamp.
     *
     * @param signature - The signature from request headers
     * @param body - Raw request body as a string
     * @param timestamp - Optional timestamp from request headers
     * @returns boolean indicating if the signature is valid
     */
    async function verifySignature(signature: string, body?: string, timestamp?: string): Promise<boolean> {
      const dataToVerify = timestamp
        ? `${body || ''}${timestamp}`
        : (body || '');
    
      const secretKey = await wmill.getVariable(SECRET_KEY_VARIABLE_PATH);
    
      const expectedSignature = crypto
        .createHmac('sha256', secretKey || '')
        .update(dataToVerify)
        .digest('hex');
    
      try {
        return crypto.timingSafeEqual(
          Buffer.from(signature),
          Buffer.from(expectedSignature)
        );
      } catch (error: unknown) {
        console.error('Signature comparison error:', error);
        return false;
      }
    
      // NOTE: Modify this logic if your provider uses a different signing mechanism (e.g., Base64, RSA, etc.)
    }
    
    /**
     * Main Function - Handles processed trigger events
     *
     * ⚠️ Called AFTER `preprocessor()`, with its return values.
     *
     * @param body - Parsed request body
     */
    export async function main(body: any) {
      // At this point, the request has been authenticated (signature + timestamp) and body safely parsed
      return {
        statusCode: 200,
        body: {
          message: "Request authenticated successfully",
          receivedData: body,
        }
      };
    }

    Submitted by hugo697 361 days ago

  • bun
    import * as crypto from 'crypto';
    import * as wmill from "windmill-client";
    
    const SECRET_KEY_VARIABLE_PATH = "secret_key_path";
    
    /**
     * Trigger Preprocessor
     *
     * ⚠️ This function runs BEFORE the main function.
     *
     * Windmill allows you to define a `preprocessor` for any trigger type (HTTP, WebSocket, Kafka, Email, etc.).
     * The preprocessor gives you the ability to perform custom logic such as validation, transformation, or authentication
     * before the `main()` function is executed.
     *
     * In this example:
     * - The trigger kind is `http`, which means we have access to HTTP-specific metadata.
     * - We use `wm_trigger.http.headers` to extract custom headers from the incoming HTTP request, such as:
     *   - `x-signature` for verifying the integrity and authenticity of the request.
     *   - `x-timestamp` to guard against replay attacks by validating the request time.
     * - The `raw_string` argument contains the **raw JSON body** of the request as a string — which is crucial for verifying the HMAC signature.
     *
     * ⚠️ **Important:** `raw_string` is required for signature verification in this example. 
     * Make sure the **"raw body"** option is enabled in your HTTP route configuration. 
     * If it's not enabled, `raw_string` will be undefined and the script will throw an error.
     *
     * 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.
     *
     * Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors
     */
    export async function preprocessor(
      wm_trigger: {
        kind: 'http' | 'email' | 'webhook' | 'websocket' | 'kafka' | 'nats' | 'postgres' | 'sqs',
        http?: {
          route: string;
          path: string;
          method: string;
          params: Record<string, string>;
          query: Record<string, string>;
          headers: Record<string, string>;
        };
      },
      raw_string?: string
    ) {
      if (wm_trigger.kind === 'http' && wm_trigger.http) {
    
        if (!raw_string) {
          throw new Error("Missing raw body. Ensure the 'raw body' option is enabled in the HTTP route configuration.");
        }
    
        // Extract signature from headers
        const signature = wm_trigger.http.headers['x-signature'] || wm_trigger.http.headers['signature'];
        if (!signature) {
          throw new Error('Missing signature in request headers.');
        }
    
        // Check timestamp if present to prevent replay attacks
        const timestamp = wm_trigger.http.headers['x-timestamp'] || wm_trigger.http.headers['timestamp'];
        if (timestamp) {
          const timestampValue = parseInt(timestamp, 10);
          const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
          const TIME_WINDOW_SECONDS = 5 * 60; // 5 minutes
    
          if (isNaN(timestampValue)) {
            throw new Error('Invalid timestamp format.');
          }
    
          if (Math.abs(currentTime - timestampValue) > TIME_WINDOW_SECONDS) {
            throw new Error('Request timestamp is outside the acceptable time window.');
          }
        }
    
        // Verify the signature
        const isValidSignature = await verifySignature(signature, raw_string, timestamp);
        if (!isValidSignature) {
          throw new Error('Invalid signature.');
        }
    
        // Parse the body if it's JSON (with error handling)
        let parsedBody: any = {};
        try {
          parsedBody = JSON.parse(raw_string);
        } catch (error: unknown) {
          const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error';
          throw new Error(`Failed to parse request body: ${errorMessage}`);
        }
    
        // Return both HTTP details and the parsed body to main
        return {
          body: parsedBody
        };
      }
    
      throw new Error(`Expected trigger of kind 'http', but received: ${wm_trigger.kind}`);
    }
    
    /**
     * Verifies the HMAC-SHA256 signature against the raw body and optional timestamp.
     *
     * @param signature - The signature from request headers
     * @param body - Raw request body as a string
     * @param timestamp - Optional timestamp from request headers
     * @returns boolean indicating if the signature is valid
     */
    async function verifySignature(signature: string, body?: string, timestamp?: string): Promise<boolean> {
      const dataToVerify = timestamp
        ? `${body || ''}${timestamp}`
        : (body || '');
    
      const secretKey = await wmill.getVariable(SECRET_KEY_VARIABLE_PATH);
    
      const expectedSignature = crypto
        .createHmac('sha256', secretKey || '')
        .update(dataToVerify)
        .digest('hex');
    
      try {
        return crypto.timingSafeEqual(
          Buffer.from(signature),
          Buffer.from(expectedSignature)
        );
      } catch (error: unknown) {
        console.error('Signature comparison error:', error);
        return false;
      }
    
      // NOTE: Modify this logic if your provider uses a different signing mechanism (e.g., Base64, RSA, etc.)
    }
    
    /**
     * Main Function - Handles processed trigger events
     *
     * ⚠️ Called AFTER `preprocessor()`, with its return values.
     *
     * @param body - Parsed request body
     */
    export async function main(body: any) {
      // At this point, the request has been authenticated (signature + timestamp) and body safely parsed
      return {
        statusCode: 200,
        body: {
          message: "Request authenticated successfully",
          receivedData: body,
        }
      };
    }

    Submitted by dieriba.pro916 417 days ago

  • bun
    import * as crypto from 'crypto';
    import * as wmill from "windmill-client";
    
    const SECRET_KEY_VARIABLE_PATH = "secret_key_path";
    
    /**
     * Trigger Preprocessor
     *
     * ⚠️ This function runs BEFORE the main function.
     *
     * Windmill allows you to define a `preprocessor` for any trigger type (HTTP, WebSocket, Kafka, Email, etc.).
     * The preprocessor gives you the ability to perform custom logic such as validation, transformation, or authentication
     * before the `main()` function is executed.
     *
     * In this example:
     * - The trigger kind is `http`, which means we have access to HTTP-specific metadata.
     * - We use `wm_trigger.http.headers` to extract custom headers from the incoming HTTP request, such as:
     *   - `x-signature` for verifying the integrity and authenticity of the request.
     *   - `x-timestamp` to guard against replay attacks by validating the request time.
     * - The `raw_string` argument contains the **raw JSON body** of the request as a string — which is crucial for verifying the HMAC signature.
     *
     * ⚠️ **Important:** `raw_string` is required for signature verification in this example. 
     * Make sure the **"raw body"** option is enabled in your HTTP route configuration. 
     * If it's not enabled, `raw_string` will be undefined and the script will throw an error.
     *
     * 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.
     *
     * Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors
     */
    export async function preprocessor(
      wm_trigger: {
        kind: 'http' | 'email' | 'webhook' | 'websocket' | 'kafka' | 'nats' | 'postgres' | 'sqs',
        http?: {
          route: string;
          path: string;
          method: string;
          params: Record<string, string>;
          query: Record<string, string>;
          headers: Record<string, string>;
        };
      },
      raw_string?: string
    ) {
      if (wm_trigger.kind === 'http' && wm_trigger.http) {
    
        if (!raw_string) {
          throw new Error("Missing raw body. Ensure the 'raw body' option is enabled in the HTTP route configuration.");
        }
    
        // Extract signature from headers
        const signature = wm_trigger.http.headers['x-signature'] || wm_trigger.http.headers['signature'];
        if (!signature) {
          throw new Error('Missing signature in request headers.');
        }
    
        // Check timestamp if present to prevent replay attacks
        const timestamp = wm_trigger.http.headers['x-timestamp'] || wm_trigger.http.headers['timestamp'];
        if (timestamp) {
          const timestampValue = parseInt(timestamp, 10);
          const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
          const TIME_WINDOW_SECONDS = 5 * 60; // 5 minutes
    
          if (isNaN(timestampValue)) {
            throw new Error('Invalid timestamp format.');
          }
    
          if (Math.abs(currentTime - timestampValue) > TIME_WINDOW_SECONDS) {
            throw new Error('Request timestamp is outside the acceptable time window.');
          }
        }
    
        // Verify the signature
        const isValidSignature = await verifySignature(signature, raw_string, timestamp);
        if (!isValidSignature) {
          throw new Error('Invalid signature.');
        }
    
        // Parse the body if it's JSON (with error handling)
        let parsedBody: any = {};
        try {
          parsedBody = JSON.parse(raw_string);
        } catch (error: unknown) {
          const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error';
          throw new Error(`Failed to parse request body: ${errorMessage}`);
        }
    
        // Return both HTTP details and the parsed body to main
        return {
          body: parsedBody
        };
      }
    
      throw new Error(`Expected trigger of kind 'http', but received: ${wm_trigger.kind}`);
    }
    
    /**
     * Verifies the HMAC-SHA256 signature against the raw body and optional timestamp.
     *
     * @param signature - The signature from request headers
     * @param body - Raw request body as a string
     * @param timestamp - Optional timestamp from request headers
     * @returns boolean indicating if the signature is valid
     */
    async function verifySignature(signature: string, body?: string, timestamp?: string): Promise<boolean> {
      const dataToVerify = timestamp
        ? `${body || ''}${timestamp}`
        : (body || '');
    
      const secretKey = await wmill.getVariable(SECRET_KEY_VARIABLE_PATH);
    
      const expectedSignature = crypto
        .createHmac('sha256', secretKey || '')
        .update(dataToVerify)
        .digest('hex');
    
      try {
        return crypto.timingSafeEqual(
          Buffer.from(signature),
          Buffer.from(expectedSignature)
        );
      } catch (error: unknown) {
        console.error('Signature comparison error:', error);
        return false;
      }
    
      // NOTE: Modify this logic if your provider uses a different signing mechanism (e.g., Base64, RSA, etc.)
    }
    
    /**
     * Main Function - Handles processed trigger events
     *
     * ⚠️ Called AFTER `preprocessor()`, with its return values.
     *
     * @param http - HTTP request details
     * @param body - Parsed request body
     */
    export async function main(body: any) {
      // At this point, the request has been authenticated (signature + timestamp) and body safely parsed
      return {
        statusCode: 200,
        body: {
          message: "Request authenticated successfully",
          receivedData: body,
        }
      };
    }

    Submitted by dieriba.pro916 417 days ago

  • bun
    import * as crypto from 'crypto';
    import * as wmill from "windmill-client";
    
    const SECRET_KEY_VARIABLE_PATH = "secret_key_path";
    
    /**
     * Trigger Preprocessor
     *
     * ⚠️ This function runs BEFORE the main function.
     *
     * Windmill allows you to define a `preprocessor` for any trigger type (HTTP, WebSocket, Kafka, Email, etc.).
     * The preprocessor gives you the ability to perform custom logic such as validation, transformation, or authentication
     * before the `main()` function is executed.
     *
     * ✅ In this example:
     * - The trigger kind is `http`, which means we have access to HTTP-specific metadata.
     * - We use `wm_trigger.http.headers` to extract custom headers from the incoming HTTP request, such as:
     *   - `x-signature` for verifying the integrity and authenticity of the request.
     *   - `x-timestamp` to guard against replay attacks by validating the request time.
     * - The `raw_string` argument contains the **raw JSON body** of the request as a string — which is crucial for verifying the HMAC signature.
     *
     * ⚠️ **Important:** `raw_string` is required for signature verification in this example. 
     * Make sure the **"raw body"** option is enabled in your HTTP route configuration. 
     * If it's not enabled, `raw_string` will be undefined and the script will throw an error.
     *
     * - Once the signature and timestamp are verified, we parse the raw JSON body and return structured data to `main()`.
     *
     * ✅ The result of this preprocessor is passed directly into the `main()` function as named arguments:
     *   - `http`: contains the HTTP request metadata, including headers, path, method, etc.
     *   - `body`: the parsed JSON payload from the request body
     *
     * Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors
     */
    export async function preprocessor(
      wm_trigger: {
        kind: 'http' | 'email' | 'webhook' | 'websocket' | 'kafka' | 'nats' | 'postgres' | 'sqs',
        http?: {
          route: string;
          path: string;
          method: string;
          params: Record<string, string>;
          query: Record<string, string>;
          headers: Record<string, string>;
        };
      },
      raw_string?: string
    ) {
      if (wm_trigger.kind === 'http' && wm_trigger.http) {
    
        if (!raw_string) {
          throw new Error("Missing raw body. Ensure the 'raw body' option is enabled in the HTTP route configuration.");
        }
    
        // Extract signature from headers
        const signature = wm_trigger.http.headers['x-signature'] || wm_trigger.http.headers['signature'];
        if (!signature) {
          throw new Error('Missing signature in request headers.');
        }
    
        // Check timestamp if present to prevent replay attacks
        const timestamp = wm_trigger.http.headers['x-timestamp'] || wm_trigger.http.headers['timestamp'];
        if (timestamp) {
          const timestampValue = parseInt(timestamp, 10);
          const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
          const TIME_WINDOW_SECONDS = 5 * 60; // 5 minutes
    
          if (isNaN(timestampValue)) {
            throw new Error('Invalid timestamp format.');
          }
    
          if (Math.abs(currentTime - timestampValue) > TIME_WINDOW_SECONDS) {
            throw new Error('Request timestamp is outside the acceptable time window.');
          }
        }
    
        // Verify the signature
        const isValidSignature = await verifySignature(signature, raw_string, timestamp);
        if (!isValidSignature) {
          throw new Error('Invalid signature.');
        }
    
        // Parse the body if it's JSON (with error handling)
        let parsedBody: any = {};
        try {
          parsedBody = JSON.parse(raw_string);
        } catch (error: unknown) {
          const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error';
          throw new Error(`Failed to parse request body: ${errorMessage}`);
        }
    
        // Return both HTTP details and the parsed body to main
        return {
          http: wm_trigger.http,
          body: parsedBody
        };
      }
    
      throw new Error(`Expected trigger of kind 'http', but received: ${wm_trigger.kind}`);
    }
    
    /**
     * Verifies the HMAC-SHA256 signature against the raw body and optional timestamp.
     *
     * @param signature - The signature from request headers
     * @param body - Raw request body as a string
     * @param timestamp - Optional timestamp from request headers
     * @returns boolean indicating if the signature is valid
     */
    async function verifySignature(signature: string, body?: string, timestamp?: string): Promise<boolean> {
      const dataToVerify = timestamp
        ? `${body || ''}${timestamp}`
        : (body || '');
    
      const secretKey = await wmill.getVariable(SECRET_KEY_VARIABLE_PATH);
    
      const expectedSignature = crypto
        .createHmac('sha256', secretKey || '')
        .update(dataToVerify)
        .digest('hex');
    
      try {
        return crypto.timingSafeEqual(
          Buffer.from(signature),
          Buffer.from(expectedSignature)
        );
      } catch (error: unknown) {
        console.error('Signature comparison error:', error);
        return false;
      }
    
      // NOTE: Modify this logic if your provider uses a different signing mechanism (e.g., Base64, RSA, etc.)
    }
    
    /**
     * Main Function - Handles processed trigger events
     *
     * ⚠️ Called AFTER `preprocessor()`, with its return values.
     *
     * @param http - HTTP request details
     * @param body - Parsed request body
     */
    export async function main(
      http: {
        route: string;
        path: string;
        method: string;
        params: Record<string, string>;
        query: Record<string, string>;
        headers: Record<string, string>;
      },
      body: any
    ) {
      // At this point, the request has been authenticated (signature + timestamp) and body safely parsed
    
      return {
        statusCode: 200,
        body: {
          message: "Request authenticated successfully",
          receivedData: body,
          timestamp: http.headers['x-timestamp'] || http.headers['timestamp']
        }
      };
    }

    Submitted by dieriba.pro916 417 days ago

  • bun
    import * as crypto from 'crypto';
    import * as wmill from "windmill-client";
    
    const SECRET_KEY_VARIABLE_PATH = "secret_key_path";
    
    /**
     * Trigger Preprocessor
     *
     * ⚠️ This function runs BEFORE the main function.
     *
     * Windmill allows you to define a `preprocessor` for any trigger type (HTTP, WebSocket, Kafka, Email, etc.).
     * The preprocessor gives you the ability to perform custom logic such as validation, transformation, or authentication
     * before the `main()` function is executed.
     *
     * ✅ In this example:
     * - The trigger kind is `http`, which means we have access to HTTP-specific metadata.
     * - We use `wm_trigger.http.headers` to extract custom headers from the incoming HTTP request, such as:
     *   - `x-signature` for verifying the integrity and authenticity of the request.
     *   - `x-timestamp` to guard against replay attacks by validating the request time.
     * - The `raw_string` argument contains the **raw JSON body** of the request as a string — which is crucial for verifying the HMAC signature.
     *
     * ⚠️ **Important:** `raw_string` is required for signature verification in this example. 
     * Make sure the **"raw body"** option is enabled in your HTTP route configuration. 
     * If it's not enabled, `raw_string` will be undefined and the script will throw an error.
     *
     * - Once the signature and timestamp are verified, we parse the raw JSON body and return structured data to `main()`.
     *
     * The result of this preprocessor is passed directly into the `main()` function as named arguments.
     * This ensures that your business logic only runs if the request is authenticated and well-formed.
     *
     * Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors
     */
    export async function preprocessor(
      wm_trigger: {
        kind: 'http' | 'email' | 'webhook' | 'websocket' | 'kafka' | 'nats' | 'postgres' | 'sqs',
        http?: {
          route: string;
          path: string;
          method: string;
          params: Record<string, string>;
          query: Record<string, string>;
          headers: Record<string, string>;
        };
      },
      raw_string?: string
    ) {
      if (wm_trigger.kind === 'http' && wm_trigger.http) {
    
        if (!raw_string) {
          throw new Error("Missing raw body. Ensure the 'raw body' option is enabled in the HTTP route configuration.");
        }
    
        // Extract signature from headers
        const signature = wm_trigger.http.headers['x-signature'] || wm_trigger.http.headers['signature'];
        if (!signature) {
          throw new Error('Missing signature in request headers.');
        }
    
        // Check timestamp if present to prevent replay attacks
        const timestamp = wm_trigger.http.headers['x-timestamp'] || wm_trigger.http.headers['timestamp'];
        if (timestamp) {
          const timestampValue = parseInt(timestamp, 10);
          const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
          const TIME_WINDOW_SECONDS = 5 * 60; // 5 minutes
    
          if (isNaN(timestampValue)) {
            throw new Error('Invalid timestamp format.');
          }
    
          if (Math.abs(currentTime - timestampValue) > TIME_WINDOW_SECONDS) {
            throw new Error('Request timestamp is outside the acceptable time window.');
          }
        }
    
        // Verify the signature
        const isValidSignature = await verifySignature(signature, raw_string, timestamp);
        if (!isValidSignature) {
          throw new Error('Invalid signature.');
        }
    
        // Parse the body if it's JSON (with error handling)
        let parsedBody: any = {};
        try {
          parsedBody = JSON.parse(raw_string);
        } catch (error: unknown) {
          const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error';
          throw new Error(`Failed to parse request body: ${errorMessage}`);
        }
    
        // Return both HTTP details and the parsed body to main
        return {
          http: wm_trigger.http,
          body: parsedBody
        };
      }
    
      throw new Error(`Expected trigger of kind 'http', but received: ${wm_trigger.kind}`);
    }
    
    /**
     * Verifies the HMAC-SHA256 signature against the raw body and optional timestamp.
     *
     * @param signature - The signature from request headers
     * @param body - Raw request body as a string
     * @param timestamp - Optional timestamp from request headers
     * @returns boolean indicating if the signature is valid
     */
    async function verifySignature(signature: string, body?: string, timestamp?: string): Promise<boolean> {
      const dataToVerify = timestamp
        ? `${body || ''}${timestamp}`
        : (body || '');
    
      const secretKey = await wmill.getVariable(SECRET_KEY_VARIABLE_PATH);
    
      const expectedSignature = crypto
        .createHmac('sha256', secretKey || '')
        .update(dataToVerify)
        .digest('hex');
    
      try {
        return crypto.timingSafeEqual(
          Buffer.from(signature),
          Buffer.from(expectedSignature)
        );
      } catch (error: unknown) {
        console.error('Signature comparison error:', error);
        return false;
      }
    
      // NOTE: Modify this logic if your provider uses a different signing mechanism (e.g., Base64, RSA, etc.)
    }
    
    /**
     * Main Function - Handles processed trigger events
     *
     * ⚠️ Called AFTER `preprocessor()`, with its return values.
     *
     * @param http - HTTP request details
     * @param body - Parsed request body
     */
    export async function main(
      http: {
        route: string;
        path: string;
        method: string;
        params: Record<string, string>;
        query: Record<string, string>;
        headers: Record<string, string>;
      },
      body: any
    ) {
      // At this point, the request has been authenticated (signature + timestamp) and body safely parsed
    
      return {
        statusCode: 200,
        body: {
          message: "Request authenticated successfully",
          receivedData: body,
          timestamp: http.headers['x-timestamp'] || http.headers['timestamp']
        }
      };
    }

    Submitted by dieriba.pro916 417 days ago

  • bun
    import * as crypto from 'crypto';
    import * as wmill from "windmill-client";
    
    const SECRET_KEY_VARIABLE_PATH = "secret_key_path";
    
    /**
     * Trigger Preprocessor
     *
     * ⚠️ This function runs BEFORE the main function.
     *
     * Windmill allows you to define a `preprocessor` for any trigger type (HTTP, WebSocket, Kafka, Email, etc.).
     * The preprocessor gives you the ability to perform custom logic such as validation, transformation, or authentication
     * before the `main()` function is executed.
     *
     * ✅ In this example:
     * - The trigger kind is `http`, which means we have access to HTTP-specific metadata.
     * - We use `wm_trigger.http.headers` to extract custom headers from the incoming HTTP request, such as:
     *   - `x-signature` for verifying the integrity and authenticity of the request.
     *   - `x-timestamp` to guard against replay attacks by validating the request time.
     * - The `raw_string` argument contains the **raw JSON body** of the request as a string — which is crucial for verifying the HMAC signature.
     *   ⚠️ Note: `raw_string` is only available if the **"raw body"** option is enabled in the HTTP route configuration.
     *   If it's not enabled, `raw_string` will be empty.
     * - Once the signature and timestamp are verified, we parse the raw JSON body and return structured data to `main()`.
     *
     * The result of this preprocessor is passed directly into the `main()` function as named arguments.
     * This ensures that your business logic only runs if the request is authenticated and well-formed.
     *
     * Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors
     */
    export async function preprocessor(
      wm_trigger: {
        kind: 'http' | 'email' | 'webhook' | 'websocket' | 'kafka' | 'nats' | 'postgres' | 'sqs',
        http?: {
          route: string;
          path: string;
          method: string;
          params: Record<string, string>;
          query: Record<string, string>;
          headers: Record<string, string>;
        };
      },
      raw_string?: string
    ) {
      if (wm_trigger.kind === 'http' && wm_trigger.http) {
    
        if (!raw_string) {
          throw new Error("Missing raw body. Ensure the 'raw body' option is enabled in the HTTP route configuration.");
        }
    
        // Extract signature from headers
        const signature = wm_trigger.http.headers['x-signature'] || wm_trigger.http.headers['signature'];
        if (!signature) {
          throw new Error('Missing signature in request headers.');
        }
    
        // Check timestamp if present to prevent replay attacks
        const timestamp = wm_trigger.http.headers['x-timestamp'] || wm_trigger.http.headers['timestamp'];
        if (timestamp) {
          const timestampValue = parseInt(timestamp, 10);
          const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
          const TIME_WINDOW_SECONDS = 5 * 60; // 5 minutes
    
          if (isNaN(timestampValue)) {
            throw new Error('Invalid timestamp format.');
          }
    
          if (Math.abs(currentTime - timestampValue) > TIME_WINDOW_SECONDS) {
            throw new Error('Request timestamp is outside the acceptable time window.');
          }
        }
    
        // Verify the signature
        const isValidSignature = await verifySignature(signature, raw_string, timestamp);
        if (!isValidSignature) {
          throw new Error('Invalid signature.');
        }
    
        // Parse the body if it's JSON (with error handling)
        let parsedBody: any = {};
        try {
          parsedBody = JSON.parse(raw_string);
        } catch (error: unknown) {
          const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error';
          throw new Error(`Failed to parse request body: ${errorMessage}`);
        }
    
        // Return both HTTP details and the parsed body to main
        return {
          http: wm_trigger.http,
          body: parsedBody
        };
      }
    
      throw new Error(`Expected trigger of kind 'http', but received: ${wm_trigger.kind}`);
    }
    
    /**
     * Verifies the HMAC-SHA256 signature against the raw body and optional timestamp.
     *
     * @param signature - The signature from request headers
     * @param body - Raw request body as a string
     * @param timestamp - Optional timestamp from request headers
     * @returns boolean indicating if the signature is valid
     */
    async function verifySignature(signature: string, body?: string, timestamp?: string): Promise<boolean> {
      const dataToVerify = timestamp
        ? `${body || ''}${timestamp}`
        : (body || '');
    
      const secretKey = await wmill.getVariable(SECRET_KEY_VARIABLE_PATH);
    
      const expectedSignature = crypto
        .createHmac('sha256', secretKey || '')
        .update(dataToVerify)
        .digest('hex');
    
      try {
        return crypto.timingSafeEqual(
          Buffer.from(signature),
          Buffer.from(expectedSignature)
        );
      } catch (error: unknown) {
        console.error('Signature comparison error:', error);
        return false;
      }
    
      // NOTE: Modify this logic if your provider uses a different signing mechanism (e.g., Base64, RSA, etc.)
    }
    
    /**
     * Main Function - Handles processed trigger events
     *
     * ⚠️ Called AFTER `preprocessor()`, with its return values.
     *
     * @param http - HTTP request details
     * @param body - Parsed request body
     */
    export async function main(
      http: {
        route: string;
        path: string;
        method: string;
        params: Record<string, string>;
        query: Record<string, string>;
        headers: Record<string, string>;
      },
      body: any
    ) {
      // At this point, the request has been authenticated (signature + timestamp) and body safely parsed
    
      return {
        statusCode: 200,
        body: {
          message: "Request authenticated successfully",
          receivedData: body,
          timestamp: http.headers['x-timestamp'] || http.headers['timestamp']
        }
      };
    }

    Submitted by dieriba.pro916 417 days ago

  • bun
    import * as crypto from 'crypto';
    import * as wmill from "windmill-client"
    
    const SECRET_KEY_VARIABLE_PATH = "secret_key_path"
    
    /**
     * Trigger Preprocessor
     *
     * ⚠️ This function runs BEFORE the main function.
     *
     * Windmill allows you to define a `preprocessor` for any trigger type (HTTP, WebSocket, Kafka, Email, etc.).
     * The preprocessor gives you the ability to perform custom logic such as validation, transformation, or authentication
     * before the `main()` function is executed.
     *
     * ✅ In this example:
     * - The trigger kind is `http`, which means we have access to HTTP-specific metadata.
     * - We use `wm_trigger.http.headers` to extract custom headers from the incoming HTTP request, such as:
     *   - `x-signature` for verifying the integrity and authenticity of the request.
     *   - `x-timestamp` to guard against replay attacks by validating the request time.
     * - The `raw_string` argument contains the **raw JSON body** of the request as a string — which is crucial for verifying the HMAC signature.
     * - Once the signature and timestamp are verified, we parse the raw JSON body and return structured data to `main()`.
     *
     * The result of this preprocessor is passed directly into the `main()` function as named arguments.
     * This ensures that your business logic only runs if the request is authenticated and well-formed.
     *
     * 🛡️ Use preprocessors to separate security/validation logic from your core processing logic for better maintainability and safety.
     *
     * Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors
     */
    
    export async function preprocessor(
      wm_trigger: {
        kind: 'http' | 'email' | 'webhook' | 'websocket' | 'kafka' | 'nats' | 'postgres' | 'sqs',
        http?: {
          route: string;
          path: string;
          method: string;
          params: Record<string, string>;
          query: Record<string, string>;
          headers: Record<string, string>;
        };
      },
      raw_string: string
    ) {
      if (wm_trigger.kind === 'http' && wm_trigger.http) {
        // Extract signature from headers
        const signature = wm_trigger.http.headers['x-signature'] || wm_trigger.http.headers['signature'];
    
        if (!signature) {
          throw new Error('Missing signature in request headers');
        }
    
        // Check timestamp if present to prevent replay attacks
        const timestamp = wm_trigger.http.headers['x-timestamp'] || wm_trigger.http.headers['timestamp'];
    
        if (timestamp) {
          const timestampValue = parseInt(timestamp, 10);
          const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
    
          // Check if timestamp is within acceptable window (e.g., 5 minutes)
          const TIME_WINDOW_SECONDS = 5 * 60; // 5 minutes
    
          if (isNaN(timestampValue)) {
            throw new Error('Invalid timestamp format');
          }
    
          if (Math.abs(currentTime - timestampValue) > TIME_WINDOW_SECONDS) {
            throw new Error('Request timestamp is outside the acceptable time window');
          }
        }
    
        // Verify the signature
        if (!(await verifySignature(signature, raw_string, timestamp))) {
          throw new Error('Invalid signature');
        }
    
        // Parse the body if it's JSON (with error handling)
        let parsedBody: any = {};
        try {
          parsedBody = JSON.parse(raw_string);
        } catch (error: unknown) {
          // Properly type the error
          const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error';
          throw new Error(`Failed to parse request body: ${errorMessage}`);
        }
    
        // Return both HTTP details and the parsed body to main
        return {
          http: wm_trigger.http,
          body: parsedBody
        };
      }
    
      throw new Error(`Expected http trigger kind, got: ${wm_trigger.kind}`);
    }
    
    /**
     * Verify signature against request body and timestamp
     * 
     * @param signature - The signature from request headers
     * @param body - Raw request body as string
     * @param timestamp - Optional timestamp from request headers
     * @returns boolean indicating if signature is valid
     */
    async function verifySignature(signature: string, body?: string, timestamp?: string): Promise<boolean> {
      // IMPORTANT: This is an example implementation and should be updated
      // with your specific provider's signature verification logic
    
      // Example HMAC-SHA256 signature verification:
      const dataToVerify = timestamp
        ? `${body || ''}${timestamp}`  // Include timestamp in verification if present
        : (body || '');
    
      const secretKey = await wmill.getVariable(SECRET_KEY_VARIABLE_PATH);
    
      const expectedSignature = crypto
        .createHmac('sha256', secretKey || '')
        .update(dataToVerify)
        .digest('hex');
    
      try {
        return crypto.timingSafeEqual(
          Buffer.from(signature),
          Buffer.from(expectedSignature)
        );
      } catch (error: unknown) {
        // This can happen if the signatures are different lengths
        console.error('Signature comparison error:', error);
        return false;
      }
    
      // NOTE: Each provider may have different signature formats and verification methods.
      // For example:
      // - Some use Base64 encoding instead of hex
      // - Some require specific header ordering in the signature calculation
      // - Some use different algorithms (RSA, Ed25519, etc.)
      // Update this function according to your provider's documentation
    }
    
    /**
     * Main Function - Handles processed trigger events
     *
     * ⚠️ Called AFTER `preprocessor()`, with its return values.
     *
     * @param http - HTTP request details
     * @param body - Parsed request body
     */
    export async function main(
      http: {
        route: string;
        path: string;
        method: string;
        params: Record<string, string>;
        query: Record<string, string>;
        headers: Record<string, string>;
      },
      body: any
    ) {
      // Implement the main function logic here
      // At this point, the signature has been verified and timestamp checked in the preprocessor
    
      // Example response
      return {
        statusCode: 200,
        body: {
          message: "Request authenticated successfully",
          receivedData: body,
          timestamp: http.headers['x-timestamp'] || http.headers['timestamp']
        }
      };
    }

    Submitted by dieriba.pro916 417 days ago

  • bun
    import * as crypto from 'crypto';
    import * as wmill from "windmill-client"
    
    const SECRET_KEY_VARIABLE_PATH = "secret_key_path"
    
    /**
     * Trigger preprocessor
     *
     * ⚠️ This function runs BEFORE the main function.
     *
     * This example shows how to handle and validate an incoming HTTP request using signature-based verification.
     *
     * The logic demonstrates:
     * - Ensuring the trigger is of kind `http` and extracting relevant metadata via `wm_trigger`.
     * - Retrieving header fields like `x-signature` and `x-timestamp` for signature verification and replay attack protection.
     * - Using `raw_string`, which contains the **raw body** of the request, as a crucial component in constructing the payload for signature verification (ensuring it hasn't been tampered with).
     * - If the signature and timestamp checks pass, the request body is safely parsed and passed to the `main()` function.
     *
     * This separation of concerns (validation in preprocessor, business logic in main) helps keep the auto-generated UI clean and maintainable.
     *
     * The preprocessor receives the same data `main` would if no preprocessor was used,
     * plus trigger metadata in the `wm_trigger` object:
     * - Webhook/HTTP: `{ wm_trigger, bodyKey1, bodyKey2, ... }`
     * - Postgres: `{ transaction_type, schema_name, table_name, row, wm_trigger }`
     * - WebSocket/Kafka/NATS/SQS/MQTT: `{ msg, wm_trigger }`
     * - Email: `{ raw_email, parsed_email, wm_trigger }`
     *
     * The returned object defines the parameter values passed to `main()`.
     * e.g., { b: 1, a: 2 } → Calls `main(2, 1)`, assuming `main` is defined as `main(a: number, b: number)`.
     * Ensure that the parameter names in `main` match the keys in the returned object.
     * 
     * Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors
     */
    export async function preprocessor(
      wm_trigger: {
        kind: 'http' | 'email' | 'webhook' | 'websocket' | 'kafka' | 'nats' | 'postgres' | 'sqs',
        http?: {
          route: string;
          path: string;
          method: string;
          params: Record<string, string>;
          query: Record<string, string>;
          headers: Record<string, string>;
        };
      },
      raw_string: string
    ) {
      if (wm_trigger.kind === 'http' && wm_trigger.http) {
        // Extract signature from headers
        const signature = wm_trigger.http.headers['x-signature'] || wm_trigger.http.headers['signature'];
    
        if (!signature) {
          throw new Error('Missing signature in request headers');
        }
    
        // Check timestamp if present to prevent replay attacks
        const timestamp = wm_trigger.http.headers['x-timestamp'] || wm_trigger.http.headers['timestamp'];
    
        if (timestamp) {
          const timestampValue = parseInt(timestamp, 10);
          const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
    
          // Check if timestamp is within acceptable window (e.g., 5 minutes)
          const TIME_WINDOW_SECONDS = 5 * 60; // 5 minutes
    
          if (isNaN(timestampValue)) {
            throw new Error('Invalid timestamp format');
          }
    
          if (Math.abs(currentTime - timestampValue) > TIME_WINDOW_SECONDS) {
            throw new Error('Request timestamp is outside the acceptable time window');
          }
        }
    
        // Verify the signature
        if (!(await verifySignature(signature, raw_string, timestamp))) {
          throw new Error('Invalid signature');
        }
    
        // Parse the body if it's JSON (with error handling)
        let parsedBody: any = {};
        try {
          parsedBody = JSON.parse(raw_string);
        } catch (error: unknown) {
          // Properly type the error
          const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error';
          throw new Error(`Failed to parse request body: ${errorMessage}`);
        }
    
        // Return both HTTP details and the parsed body to main
        return {
          http: wm_trigger.http,
          body: parsedBody
        };
      }
    
      throw new Error(`Expected http trigger kind, got: ${wm_trigger.kind}`);
    }
    
    /**
     * Verify signature against request body and timestamp
     * 
     * @param signature - The signature from request headers
     * @param body - Raw request body as string
     * @param timestamp - Optional timestamp from request headers
     * @returns boolean indicating if signature is valid
     */
    async function verifySignature(signature: string, body?: string, timestamp?: string): Promise<boolean> {
      // IMPORTANT: This is an example implementation and should be updated
      // with your specific provider's signature verification logic
    
      // Example HMAC-SHA256 signature verification:
      const dataToVerify = timestamp
        ? `${body || ''}${timestamp}`  // Include timestamp in verification if present
        : (body || '');
    
      const secretKey = await wmill.getVariable(SECRET_KEY_VARIABLE_PATH);
    
      const expectedSignature = crypto
        .createHmac('sha256', secretKey || '')
        .update(dataToVerify)
        .digest('hex');
    
      try {
        return crypto.timingSafeEqual(
          Buffer.from(signature),
          Buffer.from(expectedSignature)
        );
      } catch (error: unknown) {
        // This can happen if the signatures are different lengths
        console.error('Signature comparison error:', error);
        return false;
      }
    
      // NOTE: Each provider may have different signature formats and verification methods.
      // For example:
      // - Some use Base64 encoding instead of hex
      // - Some require specific header ordering in the signature calculation
      // - Some use different algorithms (RSA, Ed25519, etc.)
      // Update this function according to your provider's documentation
    }
    
    /**
     * Main Function - Handles processed trigger events
     *
     * ⚠️ Called AFTER `preprocessor()`, with its return values.
     *
     * @param http - HTTP request details
     * @param body - Parsed request body
     */
    export async function main(
      http: {
        route: string;
        path: string;
        method: string;
        params: Record<string, string>;
        query: Record<string, string>;
        headers: Record<string, string>;
      },
      body: any
    ) {
      // Implement the main function logic here
      // At this point, the signature has been verified and timestamp checked in the preprocessor
    
      // Example response
      return {
        statusCode: 200,
        body: {
          message: "Request authenticated successfully",
          receivedData: body,
          timestamp: http.headers['x-timestamp'] || http.headers['timestamp']
        }
      };
    }

    Submitted by dieriba.pro916 417 days ago

  • bun
    import * as crypto from 'crypto';
    import * as wmill from "windmill-client"
    
    const SECRET_KEY_VARIABLE_PATH = "secret_key_path"
    
    /**
     * Trigger preprocessor
     *
     * ⚠️ This function runs BEFORE the main function.
     *
     * This function processes raw trigger data from various sources (webhook, custom HTTP route, SQS, WebSocket, Kafka, NATS, MQTT, Postgres, or email)
     * before passing it to \`main\`. This helps separate trigger logic from the main logic and keeps the auto-generated UI clean.
     *
     * In this implementation:
     *
     * This script validates incoming HTTP requests by performing signature verification and timestamp validation to prevent replay attacks.
     * It retrieves the actual HTTP request headers using \`wm_trigger.http.headers\`, which contain critical information such as the signature and timestamp.
     * The \`raw_string\` (representing the raw JSON body) is also available, which is essential for tasks like signature validation.
     * The signature is validated using HMAC-SHA256, and if a timestamp is provided, it ensures the request falls within an acceptable time window.
     * If all checks pass, the body is parsed, and both the HTTP details and the parsed body are passed to the \`main\` function for further processing.
     *
     * The returned object defines the parameters that will be passed to \`main()\`.
     * For example, \`{ b: 1, a: 2 }\` → Calls \`main(2, 1)\`, assuming \`main\` is defined as \`main(a: number, b: number)\`.
     * Ensure the parameter names in \`main\` match the keys in the returned object.
     * 
     * Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors
     */
    export async function preprocessor(
      wm_trigger: {
        kind: 'http' | 'email' | 'webhook' | 'websocket' | 'kafka' | 'nats' | 'postgres' | 'sqs',
        http?: {
          route: string;
          path: string;
          method: string;
          params: Record<string, string>;
          query: Record<string, string>;
          headers: Record<string, string>;
        };
      },
      raw_string: string
    ) {
      if (wm_trigger.kind === 'http' && wm_trigger.http) {
        // Extract signature from headers
        const signature = wm_trigger.http.headers['x-signature'] || wm_trigger.http.headers['signature'];
    
        if (!signature) {
          throw new Error('Missing signature in request headers');
        }
    
        // Check timestamp if present to prevent replay attacks
        const timestamp = wm_trigger.http.headers['x-timestamp'] || wm_trigger.http.headers['timestamp'];
    
        if (timestamp) {
          const timestampValue = parseInt(timestamp, 10);
          const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
    
          // Check if timestamp is within acceptable window (e.g., 5 minutes)
          const TIME_WINDOW_SECONDS = 5 * 60; // 5 minutes
    
          if (isNaN(timestampValue)) {
            throw new Error('Invalid timestamp format');
          }
    
          if (Math.abs(currentTime - timestampValue) > TIME_WINDOW_SECONDS) {
            throw new Error('Request timestamp is outside the acceptable time window');
          }
        }
    
        // Verify the signature
        if (!(await verifySignature(signature, raw_string, timestamp))) {
          throw new Error('Invalid signature');
        }
    
        // Parse the body if it's JSON (with error handling)
        let parsedBody: any = {};
        try {
          parsedBody = JSON.parse(raw_string);
        } catch (error: unknown) {
          // Properly type the error
          const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error';
          throw new Error(`Failed to parse request body: ${errorMessage}`);
        }
    
        // Return both HTTP details and the parsed body to main
        return {
          http: wm_trigger.http,
          body: parsedBody
        };
      }
    
      throw new Error(`Expected http trigger kind, got: ${wm_trigger.kind}`);
    }
    
    /**
     * Verify signature against request body and timestamp
     * 
     * @param signature - The signature from request headers
     * @param body - Raw request body as string
     * @param timestamp - Optional timestamp from request headers
     * @returns boolean indicating if signature is valid
     */
    async function verifySignature(signature: string, body?: string, timestamp?: string): Promise<boolean> {
      // IMPORTANT: This is an example implementation and should be updated
      // with your specific provider's signature verification logic
    
      // Example HMAC-SHA256 signature verification:
      const dataToVerify = timestamp
        ? `${body || ''}${timestamp}`  // Include timestamp in verification if present
        : (body || '');
    
      const secretKey = await wmill.getVariable(SECRET_KEY_VARIABLE_PATH);
    
      const expectedSignature = crypto
        .createHmac('sha256', secretKey || '')
        .update(dataToVerify)
        .digest('hex');
    
      try {
        return crypto.timingSafeEqual(
          Buffer.from(signature),
          Buffer.from(expectedSignature)
        );
      } catch (error: unknown) {
        // This can happen if the signatures are different lengths
        console.error('Signature comparison error:', error);
        return false;
      }
    
      // NOTE: Each provider may have different signature formats and verification methods.
      // For example:
      // - Some use Base64 encoding instead of hex
      // - Some require specific header ordering in the signature calculation
      // - Some use different algorithms (RSA, Ed25519, etc.)
      // Update this function according to your provider's documentation
    }
    
    /**
     * Main Function - Handles processed trigger events
     *
     * ⚠️ Called AFTER `preprocessor()`, with its return values.
     *
     * @param http - HTTP request details
     * @param body - Parsed request body
     */
    export async function main(
      http: {
        route: string;
        path: string;
        method: string;
        params: Record<string, string>;
        query: Record<string, string>;
        headers: Record<string, string>;
      },
      body: any
    ) {
      // Implement the main function logic here
      // At this point, the signature has been verified and timestamp checked in the preprocessor
    
      // Example response
      return {
        statusCode: 200,
        body: {
          message: "Request authenticated successfully",
          receivedData: body,
          timestamp: http.headers['x-timestamp'] || http.headers['timestamp']
        }
      };
    }

    Submitted by dieriba.pro916 417 days ago

  • bun
    import * as crypto from 'crypto';
    import * as wmill from "windmill-client"
    
    const SECRET_KEY_VARIABLE_PATH = "secret_key_path"
    
    /**
     * Trigger preprocessor
     *
     * ⚠️ This function runs BEFORE the main function.
     *
     * This function processes raw trigger data from various sources (webhook, custom HTTP route, SQS, WebSocket, Kafka, NATS, MQTT, Postgres, or email)
     * before passing it to \`main\`. This separates the trigger logic from the main logic and keeps the auto-generated runnable UI clean.
     *
     * The preprocessor receives the same data \`main\` would if no preprocessor was used,
     * plus trigger metadata in the \`wm_trigger\` object:
     * - Webhook/HTTP: \`{ wm_trigger, bodyKey1, bodyKey2, ... }\`
     * - Postgres: \`{ transaction_type, schema_name, table_name, row, wm_trigger }\`
     * - WebSocket/Kafka/NATS/SQS/MQTT: \`{ msg, wm_trigger }\`
     * - Email: \`{ raw_email, parsed_email, wm_trigger }\`
     *
     * The returned object defines the parameter values passed to \`main()\`.
     * e.g., { b: 1, a: 2 } → Calls \`main(2, 1)\`, assuming \`main\` is defined as \`main(a: number, b: number)\`.
     * Ensure that the parameter names in \`main\` match the keys in the returned object.
     * 
     * Learn more: https://www.windmill.dev/docs/core_concepts/preprocessors
     * 
     * In this implementation:
     * 
     * This script validates incoming HTTP requests by performing signature verification and timestamp validation to prevent replay attacks.
     * It retrieves the headers of the actual HTTP request using \`wm_trigger.http.headers\`, which contain critical information such as the signature and timestamp.
     * The \`raw_string\` (representing the raw JSON body) is also available, which is essential for verification processes like signature validation.
     * The signature is validated using HMAC-SHA256, and if a timestamp is provided, it ensures that the request falls within an acceptable time window.
     * If all checks pass, the body is parsed, and both the HTTP details and the parsed body are passed to the \`main\` function for further processing.
     */
    export async function preprocessor(
      wm_trigger: {
        kind: 'http' | 'email' | 'webhook' | 'websocket' | 'kafka' | 'nats' | 'postgres' | 'sqs',
        http?: {
          route: string;
          path: string;
          method: string;
          params: Record<string, string>;
          query: Record<string, string>;
          headers: Record<string, string>;
        };
      },
      raw_string: string
    ) {
      if (wm_trigger.kind === 'http' && wm_trigger.http) {
        // Extract signature from headers
        const signature = wm_trigger.http.headers['x-signature'] || wm_trigger.http.headers['signature'];
    
        if (!signature) {
          throw new Error('Missing signature in request headers');
        }
    
        // Check timestamp if present to prevent replay attacks
        const timestamp = wm_trigger.http.headers['x-timestamp'] || wm_trigger.http.headers['timestamp'];
    
        if (timestamp) {
          const timestampValue = parseInt(timestamp, 10);
          const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
    
          // Check if timestamp is within acceptable window (e.g., 5 minutes)
          const TIME_WINDOW_SECONDS = 5 * 60; // 5 minutes
    
          if (isNaN(timestampValue)) {
            throw new Error('Invalid timestamp format');
          }
    
          if (Math.abs(currentTime - timestampValue) > TIME_WINDOW_SECONDS) {
            throw new Error('Request timestamp is outside the acceptable time window');
          }
        }
    
        // Verify the signature
        if (!(await verifySignature(signature, raw_string, timestamp))) {
          throw new Error('Invalid signature');
        }
    
        // Parse the body if it's JSON (with error handling)
        let parsedBody: any = {};
        try {
          parsedBody = JSON.parse(raw_string);
        } catch (error: unknown) {
          // Properly type the error
          const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error';
          throw new Error(`Failed to parse request body: ${errorMessage}`);
        }
    
        // Return both HTTP details and the parsed body to main
        return {
          http: wm_trigger.http,
          body: parsedBody
        };
      }
    
      throw new Error(`Expected http trigger kind, got: ${wm_trigger.kind}`);
    }
    
    /**
     * Verify signature against request body and timestamp
     * 
     * @param signature - The signature from request headers
     * @param body - Raw request body as string
     * @param timestamp - Optional timestamp from request headers
     * @returns boolean indicating if signature is valid
     */
    async function verifySignature(signature: string, body?: string, timestamp?: string): Promise<boolean> {
      // IMPORTANT: This is an example implementation and should be updated
      // with your specific provider's signature verification logic
    
      // Example HMAC-SHA256 signature verification:
      const dataToVerify = timestamp
        ? `${body || ''}${timestamp}`  // Include timestamp in verification if present
        : (body || '');
    
      const secretKey = await wmill.getVariable(SECRET_KEY_VARIABLE_PATH);
    
      const expectedSignature = crypto
        .createHmac('sha256', secretKey || '')
        .update(dataToVerify)
        .digest('hex');
    
      try {
        return crypto.timingSafeEqual(
          Buffer.from(signature),
          Buffer.from(expectedSignature)
        );
      } catch (error: unknown) {
        // This can happen if the signatures are different lengths
        console.error('Signature comparison error:', error);
        return false;
      }
    
      // NOTE: Each provider may have different signature formats and verification methods.
      // For example:
      // - Some use Base64 encoding instead of hex
      // - Some require specific header ordering in the signature calculation
      // - Some use different algorithms (RSA, Ed25519, etc.)
      // Update this function according to your provider's documentation
    }
    
    /**
     * Main Function - Handles processed trigger events
     *
     * ⚠️ Called AFTER `preprocessor()`, with its return values.
     *
     * @param http - HTTP request details
     * @param body - Parsed request body
     */
    export async function main(
      http: {
        route: string;
        path: string;
        method: string;
        params: Record<string, string>;
        query: Record<string, string>;
        headers: Record<string, string>;
      },
      body: any
    ) {
      // Implement the main function logic here
      // At this point, the signature has been verified and timestamp checked in the preprocessor
    
      // Example response
      return {
        statusCode: 200,
        body: {
          message: "Request authenticated successfully",
          receivedData: body,
          timestamp: http.headers['x-timestamp'] || http.headers['timestamp']
        }
      };
    }

    Submitted by dieriba.pro916 417 days ago

  • bun
    import * as crypto from 'crypto';
    import * as wmill from "windmill-client"
    
    const SECRET_KEY_VARIABLE_PATH = "secret_key_path"
    
    /**
     * General Trigger Preprocessor
     *
     * ⚠️ This function runs BEFORE the main function.
     *
     * It processes raw trigger data (e.g., MQTT, HTTP, SQS, WebSocket, Kafka, NATS) before passing it to `main()`.
     *
     * The returned object determines `main()` parameters:
     * - `{a: 1, b: 2}` → `main(a, b)`
     * - `{http}` → `main(http)`
     *
     * @param wm_trigger - Trigger details (e.g., MQTT, HTTP, SQS, WebSocket, Kafka, NATS)
     * @param raw_string - Raw request body as string
     * @returns Processed data for `main()`
     */
    export async function preprocessor(
      wm_trigger: {
        kind: 'http' | 'email' | 'webhook' | 'websocket' | 'kafka' | 'nats' | 'postgres' | 'sqs',
        http?: {
          route: string;
          path: string;
          method: string;
          params: Record<string, string>;
          query: Record<string, string>;
          headers: Record<string, string>;
        };
      },
      raw_string: string
    ) {
      if (wm_trigger.kind === 'http' && wm_trigger.http) {
        // Extract signature from headers
        const signature = wm_trigger.http.headers['x-signature'] || wm_trigger.http.headers['signature'];
    
        if (!signature) {
          throw new Error('Missing signature in request headers');
        }
    
        // Check timestamp if present to prevent replay attacks
        const timestamp = wm_trigger.http.headers['x-timestamp'] || wm_trigger.http.headers['timestamp'];
    
        if (timestamp) {
          const timestampValue = parseInt(timestamp, 10);
          const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
    
          // Check if timestamp is within acceptable window (e.g., 5 minutes)
          const TIME_WINDOW_SECONDS = 5 * 60; // 5 minutes
    
          if (isNaN(timestampValue)) {
            throw new Error('Invalid timestamp format');
          }
    
          if (Math.abs(currentTime - timestampValue) > TIME_WINDOW_SECONDS) {
            throw new Error('Request timestamp is outside the acceptable time window');
          }
        }
    
        // Verify the signature
        if (!(await verifySignature(signature, raw_string, timestamp))) {
          throw new Error('Invalid signature');
        }
    
        // Parse the body if it's JSON (with error handling)
        let parsedBody: any = {};
        try {
          parsedBody = JSON.parse(raw_string);
        } catch (error: unknown) {
          // Properly type the error
          const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error';
          throw new Error(`Failed to parse request body: ${errorMessage}`);
        }
    
        // Return both HTTP details and the parsed body to main
        return {
          http: wm_trigger.http,
          body: parsedBody
        };
      }
    
      throw new Error(`Expected http trigger kind, got: ${wm_trigger.kind}`);
    }
    
    /**
     * Verify signature against request body and timestamp
     * 
     * @param signature - The signature from request headers
     * @param body - Raw request body as string
     * @param timestamp - Optional timestamp from request headers
     * @returns boolean indicating if signature is valid
     */
    async function verifySignature(signature: string, body?: string, timestamp?: string): Promise<boolean> {
      // IMPORTANT: This is an example implementation and should be updated
      // with your specific provider's signature verification logic
    
      // Example HMAC-SHA256 signature verification:
      const dataToVerify = timestamp
        ? `${body || ''}${timestamp}`  // Include timestamp in verification if present
        : (body || '');
    
      const secretKey = await wmill.getVariable(SECRET_KEY_VARIABLE_PATH);
    
      const expectedSignature = crypto
        .createHmac('sha256', secretKey || '')
        .update(dataToVerify)
        .digest('hex');
    
      try {
        return crypto.timingSafeEqual(
          Buffer.from(signature),
          Buffer.from(expectedSignature)
        );
      } catch (error: unknown) {
        // This can happen if the signatures are different lengths
        console.error('Signature comparison error:', error);
        return false;
      }
    
      // NOTE: Each provider may have different signature formats and verification methods.
      // For example:
      // - Some use Base64 encoding instead of hex
      // - Some require specific header ordering in the signature calculation
      // - Some use different algorithms (RSA, Ed25519, etc.)
      // Update this function according to your provider's documentation
    }
    
    /**
     * Main Function - Handles processed trigger events
     *
     * ⚠️ Called AFTER `preprocessor()`, with its return values.
     *
     * @param http - HTTP request details
     * @param body - Parsed request body
     */
    export async function main(
      http: {
        route: string;
        path: string;
        method: string;
        params: Record<string, string>;
        query: Record<string, string>;
        headers: Record<string, string>;
      },
      body: any
    ) {
      // Implement the main function logic here
      // At this point, the signature has been verified and timestamp checked in the preprocessor
    
      // Example response
      return {
        statusCode: 200,
        body: {
          message: "Request authenticated successfully",
          receivedData: body,
          timestamp: http.headers['x-timestamp'] || http.headers['timestamp']
        }
      };
    }

    Submitted by dieriba.pro916 417 days ago

  • bun
    import * as crypto from 'crypto';
    import * as wmill from "windmill-client"
    
    /**
     * General Trigger Preprocessor
     *
     * ⚠️ This function runs BEFORE the main function.
     *
     * It processes raw trigger data (e.g., MQTT, HTTP, SQS, WebSocket, Kafka, NATS) before passing it to `main()`.
     * Common tasks:
     * - Convert binary payloads to string/JSON
     * - Extract metadata
     * - Filter messages
     * - Add timestamps/context
     * - Verify signatures and authenticate requests
     *
     * The returned object determines `main()` parameters:
     * - `{a: 1, b: 2}` → `main(a, b)`
     * - `{http}` → `main(http)`
     *
     * @param wm_trigger - Trigger details (e.g., MQTT, HTTP, SQS, WebSocket, Kafka, NATS)
     * @param raw_string - Raw request body as string
     * @returns Processed data for `main()`
     */
    export async function preprocessor(
      wm_trigger: {
        kind: 'http' | 'email' | 'webhook' | 'websocket' | 'kafka' | 'nats' | 'postgres' | 'sqs',
        http?: {
          route: string;
          path: string;
          method: string;
          params: Record<string, string>;
          query: Record<string, string>;
          headers: Record<string, string>;
        };
      },
      raw_string: string
    ) {
      if (wm_trigger.kind === 'http' && wm_trigger.http) {
        // Extract signature from headers
        const signature = wm_trigger.http.headers['x-signature'] || wm_trigger.http.headers['signature'];
    
        if (!signature) {
          throw new Error('Missing signature in request headers');
        }
    
        // Check timestamp if present to prevent replay attacks
        const timestamp = wm_trigger.http.headers['x-timestamp'] || wm_trigger.http.headers['timestamp'];
    
        if (timestamp) {
          const timestampValue = parseInt(timestamp, 10);
          const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
    
          // Check if timestamp is within acceptable window (e.g., 5 minutes)
          const TIME_WINDOW_SECONDS = 5 * 60; // 5 minutes
    
          if (isNaN(timestampValue)) {
            throw new Error('Invalid timestamp format');
          }
    
          if (Math.abs(currentTime - timestampValue) > TIME_WINDOW_SECONDS) {
            throw new Error('Request timestamp is outside the acceptable time window');
          }
        }
    
        // Verify the signature
        if (!(await verifySignature(signature, raw_string, timestamp))) {
          throw new Error('Invalid signature');
        }
    
        // Parse the body if it's JSON (with error handling)
        let parsedBody: any = {};
        try {
          parsedBody = JSON.parse(raw_string);
        } catch (error: unknown) {
          // Properly type the error
          const errorMessage = error instanceof Error ? error.message : 'Unknown parsing error';
          throw new Error(`Failed to parse request body: ${errorMessage}`);
        }
    
        // Return both HTTP details and the parsed body to main
        return {
          http: wm_trigger.http,
          body: parsedBody
        };
      }
    
      throw new Error(`Expected http trigger kind, got: ${wm_trigger.kind}`);
    }
    
    /**
     * Verify signature against request body and timestamp
     * 
     * @param signature - The signature from request headers
     * @param body - Raw request body as string
     * @param timestamp - Optional timestamp from request headers
     * @returns boolean indicating if signature is valid
     */
    async function verifySignature(signature: string, body?: string, timestamp?: string): Promise<boolean> {
      // IMPORTANT: This is an example implementation and should be updated
      // with your specific provider's signature verification logic
    
      // Example HMAC-SHA256 signature verification:
      const dataToVerify = timestamp
        ? `${body || ''}${timestamp}`  // Include timestamp in verification if present
        : (body || '');
    
      const secretKey = await wmill.getVariable('u/production/stripe_secret_key');
    
      const expectedSignature = crypto
        .createHmac('sha256', secretKey || '')
        .update(dataToVerify)
        .digest('hex');
    
      try {
        return crypto.timingSafeEqual(
          Buffer.from(signature),
          Buffer.from(expectedSignature)
        );
      } catch (error: unknown) {
        // This can happen if the signatures are different lengths
        console.error('Signature comparison error:', error);
        return false;
      }
    
      // NOTE: Each provider may have different signature formats and verification methods.
      // For example:
      // - Some use Base64 encoding instead of hex
      // - Some require specific header ordering in the signature calculation
      // - Some use different algorithms (RSA, Ed25519, etc.)
      // Update this function according to your provider's documentation
    }
    
    /**
     * Main Function - Handles processed trigger events
     *
     * ⚠️ Called AFTER `preprocessor()`, with its return values.
     *
     * @param http - HTTP request details
     * @param body - Parsed request body
     */
    export async function main(
      http: {
        route: string;
        path: string;
        method: string;
        params: Record<string, string>;
        query: Record<string, string>;
        headers: Record<string, string>;
      },
      body: any
    ) {
      // Implement the main function logic here
      // At this point, the signature has been verified and timestamp checked in the preprocessor
    
      // Example response
      return {
        statusCode: 200,
        body: {
          message: "Request authenticated successfully",
          receivedData: body,
          timestamp: http.headers['x-timestamp'] || http.headers['timestamp']
        }
      };
    }

    Submitted by dieriba.pro916 417 days ago