diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..12f0cf4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,675 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +max_line_length = 100 +tab_width = 2 +trim_trailing_whitespace = true +ij_continuation_indent_size = 2 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_visual_guides = 80 +ij_wrap_on_typing = false + +[*.css] +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_brace_placement = end_of_line +ij_css_enforce_quotes_on_format = false +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = false +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = false +ij_css_keep_single_line_blocks = false +ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_use_double_quotes = true +ij_css_value_alignment = do_not_align + +[*.feature] +indent_size = 2 +ij_gherkin_keep_indents_on_empty_lines = false + +[*.haml] +indent_size = 2 +ij_haml_keep_indents_on_empty_lines = false + +[*.less] +indent_size = 2 +ij_less_align_closing_brace_with_properties = false +ij_less_blank_lines_around_nested_selector = 1 +ij_less_blank_lines_between_blocks = 1 +ij_less_brace_placement = 0 +ij_less_enforce_quotes_on_format = false +ij_less_hex_color_long_format = false +ij_less_hex_color_lower_case = false +ij_less_hex_color_short_format = false +ij_less_hex_color_upper_case = false +ij_less_keep_blank_lines_in_code = 2 +ij_less_keep_indents_on_empty_lines = false +ij_less_keep_single_line_blocks = false +ij_less_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_less_space_after_colon = true +ij_less_space_before_opening_brace = true +ij_less_use_double_quotes = true +ij_less_value_alignment = 0 + +[*.sass] +indent_size = 2 +ij_sass_align_closing_brace_with_properties = false +ij_sass_blank_lines_around_nested_selector = 1 +ij_sass_blank_lines_between_blocks = 1 +ij_sass_brace_placement = 0 +ij_sass_enforce_quotes_on_format = false +ij_sass_hex_color_long_format = false +ij_sass_hex_color_lower_case = false +ij_sass_hex_color_short_format = false +ij_sass_hex_color_upper_case = false +ij_sass_keep_blank_lines_in_code = 2 +ij_sass_keep_indents_on_empty_lines = false +ij_sass_keep_single_line_blocks = false +ij_sass_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_sass_space_after_colon = true +ij_sass_space_before_opening_brace = true +ij_sass_use_double_quotes = true +ij_sass_value_alignment = 0 + +[*.scss] +indent_size = 2 +ij_scss_align_closing_brace_with_properties = false +ij_scss_blank_lines_around_nested_selector = 1 +ij_scss_blank_lines_between_blocks = 1 +ij_scss_brace_placement = 0 +ij_scss_enforce_quotes_on_format = false +ij_scss_hex_color_long_format = false +ij_scss_hex_color_lower_case = false +ij_scss_hex_color_short_format = false +ij_scss_hex_color_upper_case = false +ij_scss_keep_blank_lines_in_code = 2 +ij_scss_keep_indents_on_empty_lines = false +ij_scss_keep_single_line_blocks = false +ij_scss_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_scss_space_after_colon = true +ij_scss_space_before_opening_brace = true +ij_scss_use_double_quotes = true +ij_scss_value_alignment = 0 + +[*.styl] +indent_size = 2 +ij_stylus_align_closing_brace_with_properties = false +ij_stylus_blank_lines_around_nested_selector = 1 +ij_stylus_blank_lines_between_blocks = 1 +ij_stylus_brace_placement = 0 +ij_stylus_enforce_quotes_on_format = false +ij_stylus_hex_color_long_format = false +ij_stylus_hex_color_lower_case = false +ij_stylus_hex_color_short_format = false +ij_stylus_hex_color_upper_case = false +ij_stylus_keep_blank_lines_in_code = 2 +ij_stylus_keep_indents_on_empty_lines = false +ij_stylus_keep_single_line_blocks = false +ij_stylus_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_stylus_space_after_colon = true +ij_stylus_space_before_opening_brace = true +ij_stylus_use_double_quotes = true +ij_stylus_value_alignment = 0 + +[*.vue] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_visual_guides = 80 +ij_vue_indent_children_of_top_level = template +ij_vue_interpolation_new_line_after_start_delimiter = false +ij_vue_interpolation_new_line_before_end_delimiter = false +ij_vue_interpolation_wrap = off +ij_vue_keep_indents_on_empty_lines = false +ij_vue_spaces_within_interpolation_expressions = true + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] +ij_xml_align_attributes = true +ij_xml_align_text = false +ij_xml_attribute_wrap = normal +ij_xml_block_comment_at_first_column = true +ij_xml_keep_blank_lines = 2 +ij_xml_keep_indents_on_empty_lines = false +ij_xml_keep_line_breaks = true +ij_xml_keep_line_breaks_in_text = true +ij_xml_keep_whitespaces = false +ij_xml_keep_whitespaces_around_cdata = preserve +ij_xml_keep_whitespaces_inside_cdata = false +ij_xml_line_comment_at_first_column = true +ij_xml_space_after_tag_name = false +ij_xml_space_around_equals_in_attribute = false +ij_xml_space_inside_empty_tag = false +ij_xml_text_wrap = normal + +[{*.ats,*.ts}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_visual_guides = 80 +ij_typescript_align_imports = false +ij_typescript_align_multiline_array_initializer_expression = false +ij_typescript_align_multiline_binary_operation = false +ij_typescript_align_multiline_chained_methods = false +ij_typescript_align_multiline_extends_list = false +ij_typescript_align_multiline_for = true +ij_typescript_align_multiline_parameters = true +ij_typescript_align_multiline_parameters_in_calls = false +ij_typescript_align_multiline_ternary_operation = false +ij_typescript_align_object_properties = 0 +ij_typescript_align_union_types = false +ij_typescript_align_var_statements = 0 +ij_typescript_array_initializer_new_line_after_left_brace = false +ij_typescript_array_initializer_right_brace_on_new_line = false +ij_typescript_array_initializer_wrap = off +ij_typescript_assignment_wrap = off +ij_typescript_binary_operation_sign_on_next_line = false +ij_typescript_binary_operation_wrap = off +ij_typescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_typescript_blank_lines_after_imports = 1 +ij_typescript_blank_lines_around_class = 1 +ij_typescript_blank_lines_around_field = 0 +ij_typescript_blank_lines_around_field_in_interface = 0 +ij_typescript_blank_lines_around_function = 1 +ij_typescript_blank_lines_around_method = 1 +ij_typescript_blank_lines_around_method_in_interface = 1 +ij_typescript_block_brace_style = end_of_line +ij_typescript_call_parameters_new_line_after_left_paren = false +ij_typescript_call_parameters_right_paren_on_new_line = false +ij_typescript_call_parameters_wrap = off +ij_typescript_catch_on_new_line = false +ij_typescript_chained_call_dot_on_new_line = true +ij_typescript_class_brace_style = end_of_line +ij_typescript_comma_on_new_line = false +ij_typescript_do_while_brace_force = never +ij_typescript_else_on_new_line = false +ij_typescript_enforce_trailing_comma = remove +ij_typescript_extends_keyword_wrap = off +ij_typescript_extends_list_wrap = off +ij_typescript_field_prefix = _ +ij_typescript_file_name_style = relaxed +ij_typescript_finally_on_new_line = false +ij_typescript_for_brace_force = always +ij_typescript_for_statement_new_line_after_left_paren = false +ij_typescript_for_statement_right_paren_on_new_line = false +ij_typescript_for_statement_wrap = off +ij_typescript_force_quote_style = true +ij_typescript_force_semicolon_style = true +ij_typescript_function_expression_brace_style = end_of_line +ij_typescript_if_brace_force = always +ij_typescript_import_merge_members = global +ij_typescript_import_prefer_absolute_path = global +ij_typescript_import_sort_members = true +ij_typescript_import_sort_module_name = false +ij_typescript_import_use_node_resolution = true +ij_typescript_imports_wrap = on_every_item +ij_typescript_indent_case_from_switch = true +ij_typescript_indent_chained_calls = true +ij_typescript_indent_package_children = 0 +ij_typescript_jsdoc_include_types = false +ij_typescript_jsx_attribute_value = braces +ij_typescript_keep_blank_lines_in_code = 1 +ij_typescript_keep_first_column_comment = true +ij_typescript_keep_indents_on_empty_lines = false +ij_typescript_keep_line_breaks = true +ij_typescript_keep_simple_blocks_in_one_line = true +ij_typescript_keep_simple_methods_in_one_line = true +ij_typescript_line_comment_add_space = true +ij_typescript_line_comment_at_first_column = false +ij_typescript_method_brace_style = end_of_line +ij_typescript_method_call_chain_wrap = on_every_item +ij_typescript_method_parameters_new_line_after_left_paren = false +ij_typescript_method_parameters_right_paren_on_new_line = false +ij_typescript_method_parameters_wrap = on_every_item +ij_typescript_object_literal_wrap = on_every_item +ij_typescript_parentheses_expression_new_line_after_left_paren = false +ij_typescript_parentheses_expression_right_paren_on_new_line = false +ij_typescript_place_assignment_sign_on_next_line = false +ij_typescript_prefer_as_type_cast = false +ij_typescript_prefer_explicit_types_function_expression_returns = false +ij_typescript_prefer_explicit_types_function_returns = false +ij_typescript_prefer_explicit_types_vars_fields = false +ij_typescript_prefer_parameters_wrap = false +ij_typescript_reformat_c_style_comments = true +ij_typescript_space_after_colon = true +ij_typescript_space_after_comma = true +ij_typescript_space_after_dots_in_rest_parameter = false +ij_typescript_space_after_generator_mult = true +ij_typescript_space_after_property_colon = true +ij_typescript_space_after_quest = true +ij_typescript_space_after_type_colon = true +ij_typescript_space_after_unary_not = false +ij_typescript_space_before_async_arrow_lparen = true +ij_typescript_space_before_catch_keyword = true +ij_typescript_space_before_catch_left_brace = true +ij_typescript_space_before_catch_parentheses = true +ij_typescript_space_before_class_lbrace = true +ij_typescript_space_before_class_left_brace = true +ij_typescript_space_before_colon = true +ij_typescript_space_before_comma = false +ij_typescript_space_before_do_left_brace = true +ij_typescript_space_before_else_keyword = true +ij_typescript_space_before_else_left_brace = true +ij_typescript_space_before_finally_keyword = true +ij_typescript_space_before_finally_left_brace = true +ij_typescript_space_before_for_left_brace = true +ij_typescript_space_before_for_parentheses = true +ij_typescript_space_before_for_semicolon = false +ij_typescript_space_before_function_left_parenth = true +ij_typescript_space_before_generator_mult = false +ij_typescript_space_before_if_left_brace = true +ij_typescript_space_before_if_parentheses = true +ij_typescript_space_before_method_call_parentheses = false +ij_typescript_space_before_method_left_brace = true +ij_typescript_space_before_method_parentheses = false +ij_typescript_space_before_property_colon = false +ij_typescript_space_before_quest = true +ij_typescript_space_before_switch_left_brace = true +ij_typescript_space_before_switch_parentheses = true +ij_typescript_space_before_try_left_brace = true +ij_typescript_space_before_type_colon = false +ij_typescript_space_before_unary_not = false +ij_typescript_space_before_while_keyword = true +ij_typescript_space_before_while_left_brace = true +ij_typescript_space_before_while_parentheses = true +ij_typescript_spaces_around_additive_operators = true +ij_typescript_spaces_around_arrow_function_operator = true +ij_typescript_spaces_around_assignment_operators = true +ij_typescript_spaces_around_bitwise_operators = true +ij_typescript_spaces_around_equality_operators = true +ij_typescript_spaces_around_logical_operators = true +ij_typescript_spaces_around_multiplicative_operators = true +ij_typescript_spaces_around_relational_operators = true +ij_typescript_spaces_around_shift_operators = true +ij_typescript_spaces_around_unary_operator = false +ij_typescript_spaces_within_array_initializer_brackets = false +ij_typescript_spaces_within_brackets = false +ij_typescript_spaces_within_catch_parentheses = false +ij_typescript_spaces_within_for_parentheses = false +ij_typescript_spaces_within_if_parentheses = false +ij_typescript_spaces_within_imports = true +ij_typescript_spaces_within_interpolation_expressions = false +ij_typescript_spaces_within_method_call_parentheses = false +ij_typescript_spaces_within_method_parentheses = false +ij_typescript_spaces_within_object_literal_braces = true +ij_typescript_spaces_within_object_type_braces = true +ij_typescript_spaces_within_parentheses = false +ij_typescript_spaces_within_switch_parentheses = false +ij_typescript_spaces_within_type_assertion = false +ij_typescript_spaces_within_union_types = true +ij_typescript_spaces_within_while_parentheses = false +ij_typescript_special_else_if_treatment = true +ij_typescript_ternary_operation_signs_on_next_line = true +ij_typescript_ternary_operation_wrap = on_every_item +ij_typescript_union_types_wrap = on_every_item +ij_typescript_use_chained_calls_group_indents = false +ij_typescript_use_double_quotes = false +ij_typescript_use_explicit_js_extension = global +ij_typescript_use_path_mapping = always +ij_typescript_use_public_modifier = false +ij_typescript_use_semicolon_after_statement = true +ij_typescript_var_declaration_wrap = on_every_item +ij_typescript_while_brace_force = always +ij_typescript_while_on_new_line = false +ij_typescript_wrap_comments = false + +[{*.bash,*.sh,*.zsh}] +indent_size = 2 +tab_width = 2 +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false +ij_shell_use_unix_line_separator = true + +[{*.cjs,*.js,*.mjs}] +ij_continuation_indent_size = 2 +ij_visual_guides = none +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = true +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = false +ij_javascript_align_multiline_parameters = false +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = true +ij_javascript_array_initializer_right_brace_on_new_line = true +ij_javascript_array_initializer_wrap = on_every_item +ij_javascript_assignment_wrap = on_every_item +ij_javascript_binary_operation_sign_on_next_line = true +ij_javascript_binary_operation_wrap = on_every_item +ij_javascript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = end_of_line +ij_javascript_call_parameters_new_line_after_left_paren = false +ij_javascript_call_parameters_right_paren_on_new_line = false +ij_javascript_call_parameters_wrap = on_every_item +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = true +ij_javascript_class_brace_style = end_of_line +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = always +ij_javascript_else_on_new_line = false +ij_javascript_enforce_trailing_comma = remove +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = off +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = false +ij_javascript_for_brace_force = always +ij_javascript_for_statement_new_line_after_left_paren = false +ij_javascript_for_statement_right_paren_on_new_line = false +ij_javascript_for_statement_wrap = off +ij_javascript_force_quote_style = true +ij_javascript_force_semicolon_style = true +ij_javascript_function_expression_brace_style = end_of_line +ij_javascript_if_brace_force = always +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = global +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = on_every_item +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = true +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 1 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = false +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = true +ij_javascript_keep_simple_methods_in_one_line = true +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = end_of_line +ij_javascript_method_call_chain_wrap = on_every_item +ij_javascript_method_parameters_new_line_after_left_paren = false +ij_javascript_method_parameters_right_paren_on_new_line = false +ij_javascript_method_parameters_wrap = on_every_item +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_explicit_types_function_expression_returns = false +ij_javascript_prefer_explicit_types_function_returns = false +ij_javascript_prefer_explicit_types_vars_fields = false +ij_javascript_prefer_parameters_wrap = true +ij_javascript_reformat_c_style_comments = false +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = false +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = true +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = true +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = true +ij_javascript_ternary_operation_wrap = on_every_item +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = false +ij_javascript_use_explicit_js_extension = global +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = true +ij_javascript_var_declaration_wrap = on_every_item +ij_javascript_while_brace_force = always +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + +[{*.cjsx,*.coffee}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_coffeescript_align_function_body = false +ij_coffeescript_align_imports = false +ij_coffeescript_align_multiline_array_initializer_expression = true +ij_coffeescript_align_multiline_parameters = true +ij_coffeescript_align_multiline_parameters_in_calls = false +ij_coffeescript_align_object_properties = 0 +ij_coffeescript_align_union_types = false +ij_coffeescript_align_var_statements = 0 +ij_coffeescript_array_initializer_new_line_after_left_brace = false +ij_coffeescript_array_initializer_right_brace_on_new_line = false +ij_coffeescript_array_initializer_wrap = normal +ij_coffeescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_coffeescript_blank_lines_around_function = 1 +ij_coffeescript_call_parameters_new_line_after_left_paren = false +ij_coffeescript_call_parameters_right_paren_on_new_line = false +ij_coffeescript_call_parameters_wrap = normal +ij_coffeescript_chained_call_dot_on_new_line = true +ij_coffeescript_comma_on_new_line = false +ij_coffeescript_enforce_trailing_comma = keep +ij_coffeescript_field_prefix = _ +ij_coffeescript_file_name_style = relaxed +ij_coffeescript_force_quote_style = false +ij_coffeescript_force_semicolon_style = false +ij_coffeescript_function_expression_brace_style = end_of_line +ij_coffeescript_import_merge_members = global +ij_coffeescript_import_prefer_absolute_path = global +ij_coffeescript_import_sort_members = true +ij_coffeescript_import_sort_module_name = false +ij_coffeescript_import_use_node_resolution = true +ij_coffeescript_imports_wrap = on_every_item +ij_coffeescript_indent_chained_calls = true +ij_coffeescript_indent_package_children = 0 +ij_coffeescript_jsx_attribute_value = braces +ij_coffeescript_keep_blank_lines_in_code = 2 +ij_coffeescript_keep_first_column_comment = true +ij_coffeescript_keep_indents_on_empty_lines = false +ij_coffeescript_keep_line_breaks = true +ij_coffeescript_keep_simple_methods_in_one_line = false +ij_coffeescript_method_parameters_new_line_after_left_paren = false +ij_coffeescript_method_parameters_right_paren_on_new_line = false +ij_coffeescript_method_parameters_wrap = off +ij_coffeescript_object_literal_wrap = on_every_item +ij_coffeescript_prefer_as_type_cast = false +ij_coffeescript_prefer_explicit_types_function_expression_returns = false +ij_coffeescript_prefer_explicit_types_function_returns = false +ij_coffeescript_prefer_explicit_types_vars_fields = false +ij_coffeescript_reformat_c_style_comments = false +ij_coffeescript_space_after_comma = true +ij_coffeescript_space_after_dots_in_rest_parameter = false +ij_coffeescript_space_after_generator_mult = true +ij_coffeescript_space_after_property_colon = true +ij_coffeescript_space_after_type_colon = true +ij_coffeescript_space_after_unary_not = false +ij_coffeescript_space_before_async_arrow_lparen = true +ij_coffeescript_space_before_class_lbrace = true +ij_coffeescript_space_before_comma = false +ij_coffeescript_space_before_function_left_parenth = true +ij_coffeescript_space_before_generator_mult = false +ij_coffeescript_space_before_property_colon = false +ij_coffeescript_space_before_type_colon = false +ij_coffeescript_space_before_unary_not = false +ij_coffeescript_spaces_around_additive_operators = true +ij_coffeescript_spaces_around_arrow_function_operator = true +ij_coffeescript_spaces_around_assignment_operators = true +ij_coffeescript_spaces_around_bitwise_operators = true +ij_coffeescript_spaces_around_equality_operators = true +ij_coffeescript_spaces_around_logical_operators = true +ij_coffeescript_spaces_around_multiplicative_operators = true +ij_coffeescript_spaces_around_relational_operators = true +ij_coffeescript_spaces_around_shift_operators = true +ij_coffeescript_spaces_around_unary_operator = false +ij_coffeescript_spaces_within_array_initializer_braces = false +ij_coffeescript_spaces_within_array_initializer_brackets = false +ij_coffeescript_spaces_within_imports = false +ij_coffeescript_spaces_within_index_brackets = false +ij_coffeescript_spaces_within_interpolation_expressions = false +ij_coffeescript_spaces_within_method_call_parentheses = false +ij_coffeescript_spaces_within_method_parentheses = false +ij_coffeescript_spaces_within_object_braces = false +ij_coffeescript_spaces_within_object_literal_braces = false +ij_coffeescript_spaces_within_object_type_braces = true +ij_coffeescript_spaces_within_range_brackets = false +ij_coffeescript_spaces_within_type_assertion = false +ij_coffeescript_spaces_within_union_types = true +ij_coffeescript_union_types_wrap = on_every_item +ij_coffeescript_use_chained_calls_group_indents = false +ij_coffeescript_use_double_quotes = true +ij_coffeescript_use_explicit_js_extension = global +ij_coffeescript_use_path_mapping = always +ij_coffeescript_use_public_modifier = false +ij_coffeescript_use_semicolon_after_statement = false +ij_coffeescript_var_declaration_wrap = normal + +[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config}] +indent_size = 2 +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = true +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.htm,*.html,*.ng,*.sht,*.shtm,*.shtml}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_visual_guides = 80 +ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = on_every_item +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p +ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot +ij_html_enforce_quotes = true +ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var +ij_html_keep_blank_lines = 1 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span,pre,textarea +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = when multiline +ij_html_new_line_before_first_attribute = when multiline +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = true +ij_html_text_wrap = off + +[{*.markdown,*.md,migrate-to-nextjs.md}] +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.eslintrc.js b/.eslintrc.js index 661f901..c996278 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,19 +6,14 @@ module.exports = { node: true, 'jest/globals': true }, - extends: [ - 'eslint:recommended', - 'plugin:react/recommended', - 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:@typescript-eslint/recommended', - 'prettier' - ], - parser: '@typescript-eslint/parser', + extends: ['eslint:recommended', 'prettier'], overrides: [], parserOptions: { - ecmaVersion: 'latest' + ecmaVersion: 'latest', + sourceType: 'module', + allowImportExportEverywhere: true }, - plugins: ['react', 'prettier', 'jest', '@typescript-eslint'], + plugins: ['prettier', 'jest'], rules: { // 'indent': ['error', 2], // 'quotes': ['error', 'single'], @@ -28,5 +23,11 @@ module.exports = { // 'no-multi-spaces': ['error'], // 'max-len': ['error', 80], 'prettier/prettier': 2 + // 'react/jsx-max-props-per-line': [ + // 1, + // { + // maximum: 1 + // } + // ] } }; diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..06aeff5 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + "configurations": [ + { + "command": "yarn worker:data", + "name": "Run Data Worker", + "request": "launch", + "type": "node-terminal" + }, + { + "command": "yarn worker:cron", + "name": "Run Cron Worker", + "request": "launch", + "type": "node-terminal" + }, + { + "command": "yarn server", + "name": "Run Server", + "request": "launch", + "type": "node-terminal" + }, + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 1311aaf..dd0b2a2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,8 +3,9 @@ "source.fixAll.eslint": true }, "eslint.validate": [ - "javascript", - "typescript", + "javascript" ], - "editor.tabSize": 2 + "editor.tabSize": 2, + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true } diff --git a/package.json b/package.json index f39f089..afda973 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "@types/lodash.set": "^4.3.7", "@types/signal-exit": "^3.0.1", "@types/superagent": "^4.1.16", - "@typescript-eslint/eslint-plugin": "^5.49.0", "@typescript-eslint/parser": "^5.49.0", "axios": "^1.4.0", "dotenv": "^16.0.3", diff --git a/src/api.ts b/src/api.ts index 91d7910..5a4eb0f 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,5 +1,5 @@ import { HeaderOptionType, EventRequestType, ErrorPayloadType } from './types'; -import { post } from './utils'; +import { post, get } from './utils'; const postError = async ( errorSinkUrl: string, @@ -29,8 +29,12 @@ const postEvents = async ( data, options.headers.Authorization ); - return response; }; -export { postError, postEvents }; +const fetchRemoteConfig = async (configUrl: string, options: HeaderOptionType) => { + const response = await get(configUrl, options.headers.Authorization); + return JSON.parse(response); +} + +export { postError, postEvents, fetchRemoteConfig }; diff --git a/src/constants.ts b/src/constants.ts index f99da7d..a711360 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,14 +1,15 @@ const defaultConfig = { flushInterval: 1000, + remoteConfigFetchInterval: 10000, eventSinkEndpoint: '/events', errorSinkEndpoint: '/errors', + remoteConfigFetchEndpoint: '/config', allowLocalUrls: false, - keysToHash: [], ignoredDomains: [], // After the close command is sent, wait for this many milliseconds before // exiting. This gives any hanging responses a chance to return. - waitAfterClose: 1000 + waitAfterClose: 1000, }; const errors = { diff --git a/src/index.ts b/src/index.ts index f86cdb8..705e74d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,10 +5,12 @@ import { logger, safeParseJson, prepareData, - sleep + sleep, + processRemoteConfig, + getEndpointConfigForRequest } from './utils'; -import { postEvents } from './api'; - +import { postEvents, fetchRemoteConfig } from './api'; +import v8 from 'v8'; import { HeaderOptionType, EventRequestType, @@ -34,6 +36,7 @@ import { FetchInterceptor } from './interceptor/FetchInterceptor'; const Supergood = () => { let eventSinkUrl: string; let errorSinkUrl: string; + let remoteConfigFetchUrl: string; let headerOptions: HeaderOptionType; let supergoodConfig: ConfigType; @@ -43,7 +46,8 @@ const Supergood = () => { let responseCache: NodeCache; let log: LoggerType; - let interval: NodeJS.Timeout; + let flushInterval: NodeJS.Timeout; + let remoteConfigFetchInterval: NodeJS.Timeout; let localOnly = false; @@ -61,11 +65,11 @@ const Supergood = () => { config?: Partial; metadata?: Partial; } = { - clientId: process.env.SUPERGOOD_CLIENT_ID as string, - clientSecret: process.env.SUPERGOOD_CLIENT_SECRET as string, - config: {} as Partial, - metadata: {} as Partial - }, + clientId: process.env.SUPERGOOD_CLIENT_ID as string, + clientSecret: process.env.SUPERGOOD_CLIENT_SECRET as string, + config: {} as Partial, + metadata: {} as Partial + }, baseUrl = process.env.SUPERGOOD_BASE_URL || 'https://api.supergood.ai' ) => { if (!clientId) throw new Error(errors.NO_CLIENT_ID); @@ -102,100 +106,135 @@ const Supergood = () => { errorSinkUrl = `${baseUrl}${supergoodConfig.errorSinkEndpoint}`; eventSinkUrl = `${baseUrl}${supergoodConfig.eventSinkEndpoint}`; + remoteConfigFetchUrl = `${baseUrl}${supergoodConfig.remoteConfigFetchEndpoint}`; headerOptions = getHeaderOptions(clientId, clientSecret); log = logger({ errorSinkUrl, headerOptions }); - interceptor.setup(); - - interceptor.on( - 'request', - async (request: IsomorphicRequest, requestId: string) => { - try { - const url = new URL(request.url); - // Meant for debug and testing purposes + const fetchAndProcessRemoteConfig = async () => { + try { + const remoteConfigPayload = await fetchRemoteConfig(remoteConfigFetchUrl, headerOptions); + supergoodConfig = { + ...supergoodConfig, + remoteConfig: processRemoteConfig(remoteConfigPayload) + }; + } catch (e) { + log.error(errors.FETCHING_CONFIG, { config: supergoodConfig }, e as Error) + } + }; - if (url.pathname === TestErrorPath) { - throw new Error(errors.TEST_ERROR); - } + const initializeInterceptors = () => { + interceptor.setup(); + interceptor.on( + 'request', + async (request: IsomorphicRequest, requestId: string) => { + // Don't intercept if there's no remote config set + // to avoid sensitive keys being sent to the SG server. + if (!supergoodConfig.remoteConfig) return; + + try { + const url = new URL(request.url); + // Meant for debug and testing purposes + if (url.pathname === TestErrorPath) { + throw new Error(errors.TEST_ERROR); + } - const body = await request.clone().text(); - const requestData = { - id: requestId, - headers: Object.fromEntries(request.headers.entries()), - method: request.method, - url: url.href, - path: url.pathname, - search: url.search, - body: safeParseJson(body), - requestedAt: new Date() - } as RequestType; - - cacheRequest(requestData, baseUrl); - } catch (e) { - log.error( - errors.CACHING_REQUEST, - { - config: supergoodConfig, - metadata: { - requestUrl: request.url.toString(), - payloadSize: serialize(request).length, - ...supergoodMetadata + const body = await request.clone().text(); + const requestData = { + id: requestId, + headers: Object.fromEntries(request.headers.entries()), + method: request.method, + url: url.href, + path: url.pathname, + search: url.search, + body: safeParseJson(body), + requestedAt: new Date() + } as RequestType; + + const endpointConfig = getEndpointConfigForRequest(requestData, supergoodConfig.remoteConfig); + if (endpointConfig?.ignored) return; + + cacheRequest(requestData, baseUrl); + } catch (e) { + log.error( + errors.CACHING_REQUEST, + { + config: supergoodConfig, + metadata: { + requestUrl: request.url.toString(), + payloadSize: serialize(request).length, + ...supergoodMetadata + } + }, + e as Error, + { + reportOut: !localOnly } - }, - e as Error, - { - reportOut: !localOnly - } - ); + ); + } } - } - ); - - interceptor.on( - 'response', - async (response: IsomorphicResponse, requestId: string) => { - let requestData = { url: '' }; - let responseData = {}; - - try { - const requestData = requestCache.get(requestId) as { - request: RequestType; - }; - - if (requestData) { - const responseData = { - response: { - headers: Object.fromEntries(response.headers.entries()), - status: response.status, - statusText: response.statusText, - body: response.body && safeParseJson(response.body), - respondedAt: new Date() + ); + + interceptor.on( + 'response', + async (response: IsomorphicResponse, requestId: string) => { + let requestData = { url: '' }; + let responseData = {}; + + if (!supergoodConfig.remoteConfig) return; + + try { + const requestData = requestCache.get(requestId) as { + request: RequestType; + }; + + if (requestData) { + + const endpointConfig = getEndpointConfigForRequest(requestData.request, supergoodConfig.remoteConfig); + if (endpointConfig?.ignored) return; + + const responseData = { + response: { + headers: Object.fromEntries(response.headers.entries()), + status: response.status, + statusText: response.statusText, + body: response.body && safeParseJson(response.body), + respondedAt: new Date() + }, + ...requestData + } as EventRequestType; + cacheResponse(responseData, baseUrl); + } + } catch (e) { + log.error( + errors.CACHING_RESPONSE, + { + config: supergoodConfig, + metadata: { + ...supergoodMetadata, + requestUrl: requestData.url, + payloadSize: responseData ? serialize(responseData).length : 0 + } }, - ...requestData - } as EventRequestType; - cacheResponse(responseData, baseUrl); + e as Error + ); } - } catch (e) { - log.error( - errors.CACHING_RESPONSE, - { - config: supergoodConfig, - metadata: { - ...supergoodMetadata, - requestUrl: requestData.url, - payloadSize: responseData ? serialize(responseData).length : 0 - } - }, - e as Error - ); } - } - ); + ); + }; + + // Fetch the initial config and process it + await fetchAndProcessRemoteConfig(); + initializeInterceptors(); + + // Fetch the config ongoing every milliseconds + remoteConfigFetchInterval = setInterval(fetchAndProcessRemoteConfig, supergoodConfig.remoteConfigFetchInterval); + remoteConfigFetchInterval.unref(); // Flushes the cache every milliseconds - interval = setInterval(flushCache, supergoodConfig.flushInterval); - interval.unref(); + flushInterval = setInterval(flushCache, supergoodConfig.flushInterval); + // https://httptoolkit.com/blog/unblocking-node-with-unref/ + flushInterval.unref(); }; const cacheRequest = async (request: RequestType, baseUrl: string) => { @@ -217,13 +256,13 @@ const Supergood = () => { // Force flush cache means don't wait for responses const flushCache = async ({ force } = { force: false }) => { - log.debug('Flushing Cache ...', { force }); + // log.debug('Flushing Cache ...', { force }); const responseCacheKeys = responseCache.keys(); const requestCacheKeys = requestCache.keys(); const responseArray = prepareData( Object.values(responseCache.mget(responseCacheKeys)), - supergoodConfig.keysToHash + supergoodConfig.remoteConfig ) as Array; let data = [...responseArray]; @@ -232,40 +271,38 @@ const Supergood = () => { if (force) { const requestArray = prepareData( Object.values(requestCache.mget(requestCacheKeys)), - supergoodConfig.keysToHash + supergoodConfig.remoteConfig ) as Array; data = [...requestArray, ...responseArray]; } if (data.length === 0) { - log.debug('Nothing to flush', { force }); + // log.debug('Nothing to flush', { force }); return; } try { if (localOnly) { - log.debug(JSON.stringify(data, null, 2)); + log.debug(JSON.stringify(data, null, 2), { force }); } else { await postEvents(eventSinkUrl, data, headerOptions); } - log.debug(`Flushed ${data.length} events`, { force }); + if (data.length) { + log.debug(`Flushed ${data.length} events`, { force }); + } } catch (e) { const error = e as Error; if (error.message === errors.UNAUTHORIZED) { log.error( errors.UNAUTHORIZED, - { - config: supergoodConfig, - metadata: { - ...supergoodMetadata - } - }, + { config: supergoodConfig, metadata: { ...supergoodMetadata } }, error, { reportOut: false } ); - clearInterval(interval); + clearInterval(flushInterval); + clearInterval(remoteConfigFetchInterval); interceptor.teardown(); } else { log.error( @@ -297,7 +334,8 @@ const Supergood = () => { // Stops the interval and disposes of the interceptor const close = async (force = true) => { - clearInterval(interval); + clearInterval(flushInterval); + clearInterval(remoteConfigFetchInterval); // If there are hanging requests, wait a second if (requestCache.keys().length > 0) { diff --git a/src/interceptor/NodeClientRequest.ts b/src/interceptor/NodeClientRequest.ts index ce6fb7b..935c36d 100644 --- a/src/interceptor/NodeClientRequest.ts +++ b/src/interceptor/NodeClientRequest.ts @@ -121,6 +121,10 @@ export class NodeClientRequest extends ClientRequest { if (this.isInterceptable) { emitResponse(this.requestId as string, args[0], this.emitter); } + + if (this.isInterceptable) { + emitResponse(this.requestId as string, args[0], this.emitter); + } } return super.emit(event as string, ...args); diff --git a/src/types.ts b/src/types.ts index 7e10e45..badd8aa 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,7 +5,14 @@ interface HeaderOptionType { }; } -type BodyType = Record; +type JSONValue = string | number | boolean | null | JSONArray | JSONObject; + +interface JSONArray extends Array {} +interface JSONObject { + [key: string]: JSONValue; +} + +type BodyType = JSONObject interface RequestType { id: string; @@ -29,15 +36,31 @@ interface ResponseType { interface ConfigType { flushInterval: number; + remoteConfigFetchInterval: number; ignoredDomains: string[]; allowLocalUrls: boolean; cacheTtl: number; keysToHash: string[]; + remoteConfigFetchEndpoint: string; // Defaults to {baseUrl}/config if not provided eventSinkEndpoint: string; // Defaults to {baseUrl}/events if not provided errorSinkEndpoint: string; // Defaults to {baseUrl}/errors if not provided waitAfterClose: number; + remoteConfig: RemoteConfigType; } +interface EndpointConfigType { + location: string; + regex: string; + ignored: boolean; + sensitiveKeys: Array; +} + +interface RemoteConfigType { + [domain: string]: { + [endpointName: string]: EndpointConfigType; + }; +}; + interface MetadataType { numberOfEvents?: number; payloadSize?: number; @@ -49,6 +72,9 @@ interface MetadataType { interface EventRequestType { request: RequestType; response: ResponseType; + metadata?: { + sensitiveKeys: Array; + }; } // interface EventResponseType {} @@ -85,6 +111,30 @@ interface LoggerType { debug: (message: string, payload?: any) => void; } +type RemoteConfigPayloadType = Array<{ + domain: string; + endpoints: Array<{ + name: string; + matchingRegex: { + regex: string; + location: string; + }; + endpointConfiguration: { + action: string; + sensitiveKeys: Array< + { + keyPath: string; + }>; + } + }>; +}>; + +type SensitiveKeyMetadata = { + keyPath?: string; + length?: number; + type?: string; +}; + export type { HeaderOptionType, RequestType, @@ -95,5 +145,9 @@ export type { ConfigType, ErrorPayloadType, BodyType, + SensitiveKeyMetadata, + RemoteConfigType, + EndpointConfigType, + RemoteConfigPayloadType, MetadataType }; diff --git a/src/utils.test.ts b/src/utils.test.ts new file mode 100644 index 0000000..3f20074 --- /dev/null +++ b/src/utils.test.ts @@ -0,0 +1,406 @@ +import { RequestType, ResponseType } from './types'; +import { prepareData, expandSensitiveKeySetForArrays, redactValuesFromKeys } from './utils'; +import _get from 'lodash.get'; + +it('generates multiple sensitive key paths for an array', () => { + const obj = { + blog: { + name: 'My Blog', + posts: [ + { + id: 1, + title: 'json-server', + author: 'typicode' + }, + { + id: 2, + title: 'nodejs', + author: 'alex' + }, + { + id: 3, + title: 'typescript', + author: 'zack' + }, + { + id: 4, + title: 'python', + author: 'steve' + } + ] + } + }; + const sensitiveKeys = ['blog.posts[].title']; + expect(expandSensitiveKeySetForArrays(obj, sensitiveKeys)).toEqual([ + 'blog.posts[0].title', + 'blog.posts[1].title', + 'blog.posts[2].title', + 'blog.posts[3].title' + ]); +}); + +it('generates multiple sensitive key paths for an object with nested arrays', () => { + const obj = { + blog: { + name: 'My Blog', + posts: [ + { + id: 1, + title: 'json-server', + author: 'typicode', + comments: [ + { + id: 1, + body: 'some comment', + postId: 1 + }, + { + id: 2, + body: 'some comment', + postId: 1 + }, + { + id: 3, + body: 'some comment', + postId: 1 + }, + { + id: 4, + body: 'some comment', + postId: 1 + } + ] + }, + { + id: 2, + title: 'nodejs', + author: 'alex', + comments: [ + { + id: 1, + body: 'some comment', + postId: 1 + }, + { + id: 2, + body: 'some comment', + postId: 1 + }, + { + id: 3, + body: 'some comment', + postId: 1 + } + ] + }, + { + id: 3, + title: 'typescript', + author: 'zack', + comments: [ + { + id: 1, + body: 'some comment', + postId: 1 + }, + { + id: 2, + body: 'some comment', + postId: 1 + } + ] + }, + { + id: 4, + title: 'python', + author: 'steve', + comments: [ + { + id: 1, + body: 'some comment', + postId: 1 + } + ] + } + ] + } + }; + const sensitiveKeys = ['blog.posts[].comments[].body']; + expect(expandSensitiveKeySetForArrays(obj, sensitiveKeys)).toEqual([ + 'blog.posts[0].comments[0].body', + 'blog.posts[0].comments[1].body', + 'blog.posts[0].comments[2].body', + 'blog.posts[0].comments[3].body', + 'blog.posts[1].comments[0].body', + 'blog.posts[1].comments[1].body', + 'blog.posts[1].comments[2].body', + 'blog.posts[2].comments[0].body', + 'blog.posts[2].comments[1].body', + 'blog.posts[3].comments[0].body' + ]); +}); + +it('redacts values from keys with proper marshalling', () => { + const MOCK_DATA_SERVER = 'http://localhost:3001'; + const obj = { + request: { + id: '', + headers: {}, + method: 'GET', + url: `${MOCK_DATA_SERVER}/posts`, + path: '/posts', + search: '', + requestedAt: new Date(), + body: { + name: 'My Blog', + posts: [ + { + id: 1, + title: 'json-server', + author: 'typicode' + }, + { + id: 2, + title: 'nodejs', + author: 'alex' + }, + { + id: 3, + title: 'typescript', + author: 'zack' + }, + { + id: 4, + title: 'python', + author: 'steve' + } + ] + } + }, + }; + + const remoteConfig = { + [new URL(MOCK_DATA_SERVER).hostname]: { + '/posts': { + location: 'path', + regex: '/posts', + ignored: false, + sensitiveKeys: ['requestBody.posts[].title'] + } + } + }; + + const redactedObj = redactValuesFromKeys(obj, remoteConfig); + expect(_get(redactedObj, 'event.request.body.posts[0].title')).toBeNull(); + expect(redactedObj.sensitiveKeyMetadata[0]).toEqual({ + keyPath: "requestBody.posts[0].title", + type: "string", + length: 11, + }) +}); + +it('redacts values from keys of nested array', () => { + const MOCK_DATA_SERVER = 'http://localhost:3001'; + const obj = { + request: { + id: '', + headers: {}, + method: 'GET', + url: `${MOCK_DATA_SERVER}/posts`, + path: '/posts', + search: '', + requestedAt: new Date(), + body: { + name: 'My Blog', + posts: [ + { + id: 1, + title: 'json-server', + author: 'typicode', + comments: [ + { + id: 1, + body: 'some comment', + postId: 1 + }, + { + id: 2, + body: 'some comment', + postId: 1 + }, + { + id: 3, + body: 'some comment', + postId: 1 + }, + { + id: 4, + body: 'some comment', + postId: 1 + } + ] + }, + { + id: 2, + title: 'nodejs', + author: 'alex', + comments: [ + { + id: 1, + body: 'some comment', + postId: 1 + }, + { + id: 2, + body: 'some comment', + postId: 1 + }, + { + id: 3, + body: 'some comment', + postId: 1 + } + ] + }, + { + id: 3, + title: 'typescript', + author: 'zack', + comments: [ + { + id: 1, + body: 'some comment', + postId: 1 + }, + { + id: 2, + body: 'some comment', + postId: 1 + } + ] + }, + { + id: 4, + title: 'python', + author: 'steve', + comments: [ + { + id: 1, + body: 'some comment', + postId: 1 + } + ] + } + ] + } + }, + }; + + const remoteConfig = { + [new URL(MOCK_DATA_SERVER).hostname]: { + '/posts': { + location: 'path', + regex: '/posts', + ignored: false, + sensitiveKeys: ['requestBody.posts[].comments[].body'] + } + } + }; + + const redactedObj = redactValuesFromKeys(obj, remoteConfig); + expect(_get(redactedObj, 'event.request.body.posts[0].comments[0].body')).toBeNull(); + expect(redactedObj.sensitiveKeyMetadata[0]).toEqual({ + keyPath: "requestBody.posts[0].comments[0].body", + type: "string", + length: 12, + }) +}); + +it('will not blow up or redact anything if the sensitive key is bad', () => { + const MOCK_DATA_SERVER = 'http://localhost:3001'; + const obj = { + request: { + id: '', + headers: {}, + method: 'GET', + url: `${MOCK_DATA_SERVER}/posts`, + path: '/posts', + search: '', + requestedAt: new Date(), + body: { + name: 'My Blog', + comments: [ + 1,2,3,4 + ] + } + }, + }; + + const remoteConfig = { + [new URL(MOCK_DATA_SERVER).hostname]: { + '/posts': { + location: 'path', + regex: '/posts', + ignored: false, + sensitiveKeys: ['request_body.posts[].title[]'] + } + } + }; + + const redactedObj = redactValuesFromKeys(obj, remoteConfig); + expect(_get(redactedObj, 'event.request.body.name')).toBeTruthy(); + expect(redactedObj.sensitiveKeyMetadata.length).toEqual(0) +}); + + +it('will prepare the data appropriately for posting to the server', () => { + const MOCK_DATA_SERVER = 'http://localhost:3001'; + const obj = { + request: { + id: '', + headers: {}, + method: 'GET', + url: `${MOCK_DATA_SERVER}/posts`, + path: '/posts', + search: '', + requestedAt: new Date(), + body: { + blogType: { + name: 'My Blog', + } + }, + }, + response: { + headers: {}, + status: 200, + statusText: 'OK', + respondedAt: new Date(), + body: { + name: 'My Blog', + user: { + name: 'John Doe', + email: 'john@doe.com', + }, + comments: [ + 1,2,3,4 + ] + } + } + }; + + const remoteConfig = { + [new URL(MOCK_DATA_SERVER).hostname]: { + '/posts': { + location: 'path', + regex: '/posts', + ignored: false, + sensitiveKeys: ['responseBody.user.email', 'requestBody.blogType.name'] + } + } + }; + + const events = prepareData([obj], remoteConfig); + expect(_get(events[0], 'response.body.user.email')).toBeFalsy(); + expect(_get(events[0], 'request.body.blogType.name')).toBeFalsy(); + expect(events[0].metadata.sensitiveKeys.length).toEqual(2) +}); diff --git a/src/utils.ts b/src/utils.ts index d5f3c7d..a6e0e79 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,18 +4,20 @@ import { RequestType, ResponseType, EventRequestType, - ConfigType, - ErrorPayloadType + ErrorPayloadType, + RemoteConfigPayloadType, + RemoteConfigType, + EndpointConfigType, + SensitiveKeyMetadata } from './types'; -import crypto from 'node:crypto'; import { postError } from './api'; import { name, version } from '../package.json'; import https from 'https'; import http from 'http'; import { errors } from './constants'; -import set from 'lodash.set'; -import get from 'lodash.get'; +import _set from 'lodash.set'; +import _get from 'lodash.get'; const logger = ({ errorSinkUrl, @@ -39,7 +41,6 @@ const logger = ({ JSON.stringify(payload, null, 2), error ); - console.log({ reportOut, errorSinkUrl }); if (reportOut && errorSinkUrl) { postError( errorSinkUrl, @@ -85,19 +86,91 @@ const getHeaderOptions = ( }; }; -const hashValuesFromKeys = ( - obj: { request?: RequestType; response?: ResponseType }, - keysToHash: Array -) => { - let objCopy = { ...obj }; - for (let i = 0; i < keysToHash.length; i++) { - const keyString = keysToHash[i]; - const value = get(objCopy, keyString); - if (value) { - objCopy = set(objCopy, keyString, hashValue(value)); +const marshalKeyPath = (keypath: string) => { + if (/^requestHeaders/.test(keypath)) return keypath.replace('requestHeaders', 'request.headers'); + if (/^requestBody/.test(keypath)) return keypath.replace('requestBody', 'request.body'); + if (/^responseHeaders/.test(keypath)) return keypath.replace('responseHeaders', 'response.headers'); + if (/^responseBody/.test(keypath)) return keypath.replace('responseBody', 'response.body'); + return keypath; +} + +const unmarshalKeyPath = (keypath: string) => { + if (/^request\.headers/.test(keypath)) return keypath.replace('request.headers', 'requestHeaders'); + if (/^request\.body/.test(keypath)) return keypath.replace('request.body', 'requestBody'); + if (/^response\.headers/.test(keypath)) return keypath.replace('response.headers', 'responseHeaders'); + if (/^response\.body/.test(keypath)) return keypath.replace('response.body', 'responseBody'); + return keypath; +} + +const expandSensitiveKeySetForArrays = (obj: any, sensitiveKeys: Array): Array => { + const expandKey = (key: string, obj: any): Array => { + // Split the key by dots, considering the array brackets as part of the key + const parts = key.match(/[^.\[\]]+|\[\d*\]|\[\*\]/g) || []; + + // Recursively expand the key + return expand(parts, obj, ''); + }; + + const expand = (parts: string[], obj: any, keyPath: string): Array => { + const path = keyPath; + if (parts.length === 0) { + return [path]; // Remove trailing dot + } + const part = parts[0]; + const isProperty = !part.startsWith('['); + const separator = path && isProperty ? '.' : ''; + + // Check for array notations + if (/\[\*?\]/.test(part)) { + if (!Array.isArray(obj)) { + return []; + } + // Expand for each element in the array + return obj.flatMap((_, index) => + expand(parts.slice(1), obj[index], `${path}${separator}[${index}]`) + ); + } else if (part.startsWith('[') && part.endsWith(']')) { + // Specific index in the array + const index = parseInt(part.slice(1, -1), 10); + if (!isNaN(index) && index < obj.length) { + return expand(parts.slice(1), obj[index], `${path}${separator}${part}`); + } else { + return []; + } + } else { + // Regular object property + if (obj && typeof obj === 'object' && part in obj) { + return expand(parts.slice(1), obj[part], `${path}${separator}${part}`); + } else { + return []; + } } + }; + + return sensitiveKeys.flatMap(key => expandKey(key, obj)); +}; + +const redactValuesFromKeys = ( + event: { request?: RequestType; response?: ResponseType }, + remoteConfig: RemoteConfigType +): { event: { request?: RequestType; response?: ResponseType }, sensitiveKeyMetadata: Array } => { + let sensitiveKeyMetadata: Array = []; + const endpointConfig = getEndpointConfigForRequest(event.request as RequestType, remoteConfig); + if (!endpointConfig || !endpointConfig?.sensitiveKeys?.length) return { event, sensitiveKeyMetadata }; + else { + const sensitiveKeys = expandSensitiveKeySetForArrays(event, endpointConfig.sensitiveKeys.map(key => marshalKeyPath(key))) + for (let i = 0; i < sensitiveKeys.length; i++) { + const keyPath = sensitiveKeys[i]; + // Add sensitive key for array expansion + const value = _get(event, keyPath); + if (value) { + _set(event, keyPath, null); + // Don't return : for null values + sensitiveKeyMetadata.push({ keyPath: unmarshalKeyPath(keyPath), ...redactValue(value) }); + } + } + return { event, sensitiveKeyMetadata }; } - return objCopy; }; const safeParseJson = (json: string) => { @@ -108,51 +181,54 @@ const safeParseJson = (json: string) => { } }; -const hashValue = ( +const redactValue = ( input: string | Record | [Record] | undefined ) => { - const hash = crypto.createHash('sha1'); - if (!input) return ''; + let dataLength; + let dataType; - if (Array.isArray(input)) { - return [hash.update(JSON.stringify(input)).digest('base64')]; + if (!input) { + dataLength = 0; + dataType = 'null'; } - if (typeof input === 'object') { - return { hashed: hash.update(JSON.stringify(input)).digest('base64') }; + else if (Array.isArray(input)) { + dataLength = input.length; + dataType = 'array'; } - if (typeof input === 'string') { - return hash.update(input).digest('base64'); - } -}; - -const getPayloadSize = ( - input: string | Record | [Record] | undefined -) => { - if (!input) return 0; - - if (Array.isArray(input)) { - return JSON.stringify(input).length; - } - if (typeof input === 'object') { - return JSON.stringify(input).length; - } - if (typeof input === 'string') { - return input.length; + else if (typeof input === 'object') { + dataLength = new Blob([JSON.stringify(input)]).size; + dataType = 'object'; + } else if (typeof input === 'string') { + dataLength = input.length; + dataType = 'string'; + } else if (typeof input === 'number') { + dataLength = (input as number).toString().length; + dataType = Number.isInteger(input) ? 'integer' : 'float'; + } else if (typeof input === 'boolean') { + dataLength = 1; + dataType = 'boolean'; } + return { length: dataLength, type: dataType }; }; const prepareData = ( events: Array, - keysToHash: Array + remoteConfig: RemoteConfigType, ) => { - return events.filter((e) => hashValuesFromKeys(e, keysToHash)); + return events.map((e) => { + const { event, sensitiveKeyMetadata } = redactValuesFromKeys(e, remoteConfig); + return ({ + ...event, + metadata: { sensitiveKeys: sensitiveKeyMetadata } + }) + }) }; -function post( +const post = ( url: string, data: Array | ErrorPayloadType, authorization: string -): Promise { +): Promise => { const dataString = JSON.stringify(data); const packageVersion = version; @@ -203,17 +279,125 @@ function post( }); } +const get = ( + url: string, + authorization: string +): Promise => { + const packageVersion = version; + + const options = { + method: 'GET', + headers: { + Authorization: authorization, + 'supergood-api': 'supergood-js', + 'supergood-api-version': packageVersion + }, + timeout: 5000 // in ms + }; + + return new Promise((resolve, reject) => { + const transport = url.startsWith('https') ? https : http; + const req = transport.request(url, options, (res) => { + if (res && res.statusCode) { + if (res.statusCode === 401) { + return reject(new Error(errors.UNAUTHORIZED)); + } + + if (res.statusCode < 200 || res.statusCode > 299) { + return reject(new Error(`HTTP status code ${res.statusCode}`)); + } + } + + const body = [] as Buffer[]; + res.on('data', (chunk) => body.push(chunk)); + res.on('end', () => { + const resString = Buffer.concat(body).toString(); + resolve(resString); + }); + }); + + req.on('error', (err) => { + reject(err); + }); + + req.on('timeout', () => { + req.destroy(); + reject(new Error('Request time out')); + }); + + req.end(); // Notice there is no req.write() for GET requests + }); +} + +const processRemoteConfig = (remoteConfigPayload: RemoteConfigPayloadType) => { + return (remoteConfigPayload || []).reduce((remoteConfig, domainConfig) => { + const { domain, endpoints } = domainConfig; + const endpointConfig = endpoints.reduce((endpointConfig, endpoint) => { + const { matchingRegex, endpointConfiguration } = endpoint; + const { regex, location } = matchingRegex; + const { action, sensitiveKeys } = endpointConfiguration; + endpointConfig[regex] = { + location, + regex, + ignored: action === 'Ignore', + sensitiveKeys: (sensitiveKeys || []).map((key) => key.keyPath) + }; + return endpointConfig; + }, {} as { [endpointName: string]: EndpointConfigType }); + remoteConfig[domain] = endpointConfig; + return remoteConfig; + }, {} as RemoteConfigType); +} + const sleep = (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; +const getStrRepresentationFromPath = (request: RequestType, location: string) => { + const url = new URL(request.url); + if (location === 'domain') return url.hostname.toString(); + if (location === 'url') return url.toString(); + if (location === 'path') return url.pathname.toString(); + if (location === 'requestHeaders') return request.headers.toString(); + if (location === 'requestBody') return request.body?.toString(); + return request[location as keyof RequestType]?.toString(); +} + +const getEndpointConfigForRequest = (request: RequestType, remoteConfig: RemoteConfigType) => { + const domains = Object.keys(remoteConfig); + const domain = domains.find((domain) => request.url.includes(domain)); + + // If the domain doesn't exist in the config, then we return nothing + if (!domain) return null; + const endpointConfigs = remoteConfig[domain]; + + for (let i = 0; i < Object.keys(endpointConfigs).length; i++) { + const endpointConfig = endpointConfigs[Object.keys(endpointConfigs)[i]]; + const { regex, location } = endpointConfig; + const regexObj = new RegExp(regex); + const strRepresentation = getStrRepresentationFromPath(request, location); + if (!strRepresentation) continue; + else { + const match = regexObj.test(strRepresentation); + if (match) { + return endpointConfig; + } + } + } + return null; +} + export { + processRemoteConfig, getHeaderOptions, - hashValue, - hashValuesFromKeys, + redactValue, + redactValuesFromKeys, logger, safeParseJson, prepareData, sleep, - post + post, + get, + getEndpointConfigForRequest, + expandSensitiveKeySetForArrays }; diff --git a/test/consts.ts b/test/consts.ts index 8892086..692b440 100644 --- a/test/consts.ts +++ b/test/consts.ts @@ -9,10 +9,12 @@ export const SUPERGOOD_SERVER = `http://localhost:${SUPERGOOD_SERVER_PORT}`; export const SUPERGOOD_CONFIG = { flushInterval: 30000, + remoteConfigFetchInterval: 10000, cacheTtl: 0, eventSinkEndpoint: `/events`, errorSinkEndpoint: `/errors`, - keysToHash: ['request.body', 'response.body'], + configFetchEndpoint: '/config', + allowLocalUrls: true, ignoredDomains: [] }; diff --git a/test/e2e/core.e2e.test.ts b/test/e2e/core.e2e.test.ts index fac73b2..e1405e4 100644 --- a/test/e2e/core.e2e.test.ts +++ b/test/e2e/core.e2e.test.ts @@ -29,7 +29,6 @@ describe('core functionality', () => { }, SUPERGOOD_SERVER ); - const numberOfHttpCalls = 5; for (let i = 0; i < numberOfHttpCalls; i++) { await axios.get(`${MOCK_DATA_SERVER}/posts`); @@ -83,7 +82,7 @@ describe('core functionality', () => { }, SUPERGOOD_SERVER ); - axios.get(`${MOCK_DATA_SERVER}/200?sleep=2000`); + axios.get(`${MOCK_DATA_SERVER}/200?sleep=3000`); await sleep(1000); await Supergood.close(); @@ -119,8 +118,8 @@ describe('core functionality', () => { }); }); - describe('config specifications', () => { - it('should hash keys provided in config', async () => { + xdescribe('config specifications', () => { + test('hashing', async () => { await Supergood.init( { config: { diff --git a/test/e2e/remote-config.e2e.test.ts b/test/e2e/remote-config.e2e.test.ts new file mode 100644 index 0000000..73899dd --- /dev/null +++ b/test/e2e/remote-config.e2e.test.ts @@ -0,0 +1,146 @@ +import fetch from 'node-fetch'; +import Supergood from '../../src'; +import { + MOCK_DATA_SERVER, + SUPERGOOD_CLIENT_ID, + SUPERGOOD_CLIENT_SECRET, + SUPERGOOD_CONFIG, + SUPERGOOD_SERVER +} from '../consts'; +import { RemoteConfigPayloadType } from '../../src/types'; +import { getEvents } from '../utils/function-call-args'; +import { mockApi } from '../utils/mock-api'; +import _get from 'lodash.get'; +import { fetchRemoteConfig } from '../../src/api'; + +describe('remote config functionality', () => { + + it('fetches remote config', async () => { + const fetchRemoteConfigResponse = [] as RemoteConfigPayloadType; + const { postEventsMock } = mockApi({ fetchRemoteConfigResponse }); + await Supergood.init( + { + config: { ...SUPERGOOD_CONFIG, allowLocalUrls: true }, + clientId: SUPERGOOD_CLIENT_ID, + clientSecret: SUPERGOOD_CLIENT_SECRET + }, + SUPERGOOD_SERVER + ); + await fetch(`${MOCK_DATA_SERVER}/posts`); + await Supergood.close(); + expect(getEvents(postEventsMock).length).toEqual(1); + }) + + it('fetches remote config and ignores some endpoints', async () => { + const fetchRemoteConfigResponse = [{ + domain: new URL(MOCK_DATA_SERVER).hostname, + endpoints: [{ + name: '/posts', + matchingRegex: { + regex: '/posts', + location: 'path' + }, + endpointConfiguration: { + action: 'Ignore', + sensitiveKeys: [] + } + }] + } + ] as RemoteConfigPayloadType; + const { postEventsMock } = mockApi({ fetchRemoteConfigResponse }); + await Supergood.init( + { + config: { ...SUPERGOOD_CONFIG, allowLocalUrls: true }, + clientId: SUPERGOOD_CLIENT_ID, + clientSecret: SUPERGOOD_CLIENT_SECRET + }, + SUPERGOOD_SERVER + ); + await fetch(`${MOCK_DATA_SERVER}/posts`); + await fetch(`${MOCK_DATA_SERVER}/gzipped-response`); + await Supergood.close(); + const eventsPosted = getEvents(postEventsMock); + expect(eventsPosted.length).toEqual(1); + }) + + it('fetches remote config and redacts sensitive keys', async () => { + const fetchRemoteConfigResponse = [{ + domain: new URL(MOCK_DATA_SERVER).hostname, + endpoints: [{ + name: '/profile', + matchingRegex: { + regex: '/profile', + location: 'path' + }, + endpointConfiguration: { + action: 'Allow', + sensitiveKeys: [{ + keyPath: 'responseBody.name' + }] + } + }] + }] as RemoteConfigPayloadType; + const { postEventsMock } = mockApi({ fetchRemoteConfigResponse }); + await Supergood.init( + { + config: { ...SUPERGOOD_CONFIG, allowLocalUrls: true }, + clientId: SUPERGOOD_CLIENT_ID, + clientSecret: SUPERGOOD_CLIENT_SECRET + }, + SUPERGOOD_SERVER + ); + await fetch(`${MOCK_DATA_SERVER}/profile`); + await Supergood.close(); + const eventsPosted = getEvents(postEventsMock); + expect(eventsPosted.length).toEqual(1); + expect(_get(eventsPosted[0], 'metadata.sensitiveKeys[0].length')).toEqual(8) + }) + + it('fetches remote config and redacts sensitive keys within an array', async () => { + const fetchRemoteConfigResponse = [{ + domain: new URL(MOCK_DATA_SERVER).hostname, + endpoints: [{ + name: '/posts', + matchingRegex: { + regex: '/posts', + location: 'path' + }, + endpointConfiguration: { + action: 'Allow', + sensitiveKeys: [{ + keyPath: 'response_body[0].title' + }] + } + }] + }] as RemoteConfigPayloadType; + const { postEventsMock } = mockApi({ fetchRemoteConfigResponse }); + await Supergood.init( + { + config: { ...SUPERGOOD_CONFIG, allowLocalUrls: true }, + clientId: SUPERGOOD_CLIENT_ID, + clientSecret: SUPERGOOD_CLIENT_SECRET + }, + SUPERGOOD_SERVER + ); + await fetch(`${MOCK_DATA_SERVER}/posts`); + await Supergood.close(); + const eventsPosted = getEvents(postEventsMock); + expect(eventsPosted.length).toEqual(1); + }) + + it('does not intercept anything if the remote config can not be fetched', async () => { + const fetchRemoteConfigFunction = () => { throw new Error('Cant fetch remote config') }; + const { postEventsMock } = mockApi({ fetchRemoteConfigFunction }); + await Supergood.init( + { + config: { ...SUPERGOOD_CONFIG, allowLocalUrls: true }, + clientId: SUPERGOOD_CLIENT_ID, + clientSecret: SUPERGOOD_CLIENT_SECRET + }, + SUPERGOOD_SERVER + ); + await fetch(`${MOCK_DATA_SERVER}/posts`); + await Supergood.close(); + expect(postEventsMock).toHaveBeenCalledTimes(0); + }) +}); diff --git a/test/jest.e2e.config.js b/test/jest.e2e.config.js index 2ec00a1..c058e1e 100644 --- a/test/jest.e2e.config.js +++ b/test/jest.e2e.config.js @@ -10,5 +10,5 @@ module.exports = { '^.+\\.(js)$': 'babel-jest' }, transformIgnorePatterns: [], - setupFilesAfterEnv: ['./setupTests.ts'], + setupFilesAfterEnv: ['./setupTests.ts'] }; diff --git a/test/mock-db.js b/test/mock-db.js index 3c2d07c..885ae4c 100644 --- a/test/mock-db.js +++ b/test/mock-db.js @@ -4,6 +4,21 @@ const db = { id: 1, title: 'json-server', author: 'typicode' + }, + { + id: 2, + title: 'nodejs', + author: 'alex' + }, + { + id: 3, + title: 'typescript', + author: 'zack' + }, + { + id: 4, + title: 'python', + author: 'steve' } ], comments: [ diff --git a/test/utils/mock-api.ts b/test/utils/mock-api.ts index 2254e9d..00d4300 100644 --- a/test/utils/mock-api.ts +++ b/test/utils/mock-api.ts @@ -1,12 +1,31 @@ import * as api from '../../src/api'; +import { RemoteConfigPayloadType } from '../../src/types'; + +export const mockApi = ( + { + postErrorsResponse, + postEventsResponse, + fetchRemoteConfigResponse, + fetchRemoteConfigFunction, + }: + { postErrorsResponse?: any, + postEventsResponse?: any, + fetchRemoteConfigResponse?: RemoteConfigPayloadType, + fetchRemoteConfigFunction?: () => Promise, + } = {} +) => { -export function mockApi() { const postEventsMock = jest .spyOn(api, 'postEvents') - .mockImplementation(async (_, data) => ({ data } as any)); + .mockImplementation((async (_, data) => postEventsResponse ?? ({ data } as any))); + const postErrorMock = jest .spyOn(api, 'postError') - .mockImplementation(async (_, payload) => ({ payload } as any)); + .mockImplementation((async (_, payload) => postErrorsResponse ?? ({ payload } as any))); + + const fetchRemoteConfigMock = jest + .spyOn(api, 'fetchRemoteConfig') + .mockImplementation(fetchRemoteConfigFunction ?? (async () => fetchRemoteConfigResponse ?? ([] as any))); - return { postEventsMock, postErrorMock }; + return { postEventsMock, postErrorMock, fetchRemoteConfigMock }; } diff --git a/yarn.lock b/yarn.lock index ee8db89..eb2b8b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -297,6 +297,18 @@ resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@eslint-community/eslint-utils@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.5.1": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + "@eslint/eslintrc@^1.4.1": version "1.4.1" resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz" @@ -729,6 +741,11 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/json-schema@^7.0.12": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" @@ -809,6 +826,11 @@ resolved "https://registry.npmjs.org/@types/semver/-/semver-7.3.12.tgz" integrity sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A== +"@types/semver@^7.5.0": + version "7.5.6" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339" + integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A== + "@types/serve-static@*": version "1.15.0" resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz" @@ -847,20 +869,22 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@^5.49.0": - version "5.49.0" - resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.49.0.tgz" - integrity sha512-IhxabIpcf++TBaBa1h7jtOWyon80SXPRLDq0dVz5SLFC/eW6tofkw/O7Ar3lkx5z5U6wzbKDrl2larprp5kk5Q== +"@typescript-eslint/eslint-plugin@^6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.2.tgz#2e03506c5362a65e43cb132c37c9ce2d3cb51470" + integrity sha512-3+9OGAWHhk4O1LlcwLBONbdXsAhLjyCFogJY/cWy2lxdVJ2JrcTF2pTGMaLl2AE7U1l31n8Py4a8bx5DLf/0dQ== dependencies: - "@typescript-eslint/scope-manager" "5.49.0" - "@typescript-eslint/type-utils" "5.49.0" - "@typescript-eslint/utils" "5.49.0" + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "6.13.2" + "@typescript-eslint/type-utils" "6.13.2" + "@typescript-eslint/utils" "6.13.2" + "@typescript-eslint/visitor-keys" "6.13.2" debug "^4.3.4" - ignore "^5.2.0" - natural-compare-lite "^1.4.0" - regexpp "^3.2.0" - semver "^7.3.7" - tsutils "^3.21.0" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" "@typescript-eslint/parser@^5.49.0": version "5.49.0" @@ -880,21 +904,34 @@ "@typescript-eslint/types" "5.49.0" "@typescript-eslint/visitor-keys" "5.49.0" -"@typescript-eslint/type-utils@5.49.0": - version "5.49.0" - resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.49.0.tgz" - integrity sha512-eUgLTYq0tR0FGU5g1YHm4rt5H/+V2IPVkP0cBmbhRyEmyGe4XvJ2YJ6sYTmONfjmdMqyMLad7SB8GvblbeESZA== +"@typescript-eslint/scope-manager@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.13.2.tgz#5fa4e4adace028dafac212c770640b94e7b61052" + integrity sha512-CXQA0xo7z6x13FeDYCgBkjWzNqzBn8RXaE3QVQVIUm74fWJLkJkaHmHdKStrxQllGh6Q4eUGyNpMe0b1hMkXFA== dependencies: - "@typescript-eslint/typescript-estree" "5.49.0" - "@typescript-eslint/utils" "5.49.0" + "@typescript-eslint/types" "6.13.2" + "@typescript-eslint/visitor-keys" "6.13.2" + +"@typescript-eslint/type-utils@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.13.2.tgz#ebec2da14a6bb7122e0fd31eea72a382c39c6102" + integrity sha512-Qr6ssS1GFongzH2qfnWKkAQmMUyZSyOr0W54nZNU1MDfo+U4Mv3XveeLZzadc/yq8iYhQZHYT+eoXJqnACM1tw== + dependencies: + "@typescript-eslint/typescript-estree" "6.13.2" + "@typescript-eslint/utils" "6.13.2" debug "^4.3.4" - tsutils "^3.21.0" + ts-api-utils "^1.0.1" "@typescript-eslint/types@5.49.0": version "5.49.0" resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.49.0.tgz" integrity sha512-7If46kusG+sSnEpu0yOz2xFv5nRz158nzEXnJFCGVEHWnuzolXKwrH5Bsf9zsNlOQkyZuk0BZKKoJQI+1JPBBg== +"@typescript-eslint/types@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.13.2.tgz#c044aac24c2f6cefb8e921e397acad5417dd0ae6" + integrity sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg== + "@typescript-eslint/typescript-estree@5.49.0": version "5.49.0" resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.49.0.tgz" @@ -908,7 +945,33 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.49.0", "@typescript-eslint/utils@^5.10.0": +"@typescript-eslint/typescript-estree@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.2.tgz#ae556ee154c1acf025b48d37c3ef95a1d55da258" + integrity sha512-SuD8YLQv6WHnOEtKv8D6HZUzOub855cfPnPMKvdM/Bh1plv1f7Q/0iFUDLKKlxHcEstQnaUU4QZskgQq74t+3w== + dependencies: + "@typescript-eslint/types" "6.13.2" + "@typescript-eslint/visitor-keys" "6.13.2" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/utils@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.13.2.tgz#8eb89e53adc6d703a879b131e528807245486f89" + integrity sha512-b9Ptq4eAZUym4idijCRzl61oPCwwREcfDI8xGk751Vhzig5fFZR9CyzDz4Sp/nxSLBYxUPyh4QdIDqWykFhNmQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.13.2" + "@typescript-eslint/types" "6.13.2" + "@typescript-eslint/typescript-estree" "6.13.2" + semver "^7.5.4" + +"@typescript-eslint/utils@^5.10.0": version "5.49.0" resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.49.0.tgz" integrity sha512-cPJue/4Si25FViIb74sHCLtM4nTSBXtLx1d3/QT6mirQ/c65bV8arBEebBJJizfq8W2YyMoPI/WWPFWitmNqnQ== @@ -930,6 +993,14 @@ "@typescript-eslint/types" "5.49.0" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@6.13.2": + version "6.13.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.2.tgz#e0a4a80cf842bb08e6127b903284166ac4a5594c" + integrity sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw== + dependencies: + "@typescript-eslint/types" "6.13.2" + eslint-visitor-keys "^3.4.1" + "@zxing/text-encoding@0.9.0": version "0.9.0" resolved "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz" @@ -1409,6 +1480,16 @@ color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" @@ -1798,6 +1879,11 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== +eslint-visitor-keys@^3.4.1: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + eslint@^8.32.0: version "8.33.0" resolved "https://registry.npmjs.org/eslint/-/eslint-8.33.0.tgz" @@ -2157,6 +2243,11 @@ function-bind@^1.1.1, function-bind@^1.1.2: resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -2307,6 +2398,11 @@ grapheme-splitter@^1.0.4: resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -2363,6 +2459,26 @@ headers-polyfill@^4.0.2: resolved "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.2.tgz" integrity sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw== +help-me@^4.0.1: + version "4.2.0" + resolved "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz#50712bfd799ff1854ae1d312c36eafcea85b0563" + integrity sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA== + dependencies: + glob "^8.0.0" + readable-stream "^3.6.0" + +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + +headers-polyfill@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.2.tgz" + integrity sha512-EWGTfnTqAO2L/j5HZgoM/3z82L7necsJ0pO9Tp0X1wil3PDLrkypTBRgVO2ExehEEvUycejZD3FuRaXpZZc3kw== + hexoid@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz" @@ -2418,6 +2534,11 @@ ignore@^5.2.0: resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== +ignore@^5.2.4: + version "5.3.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" + integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" @@ -3396,11 +3517,6 @@ nanoid@^3.1.23: resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== -natural-compare-lite@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz" - integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -3753,6 +3869,16 @@ process@^0.11.10: resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== +process-warning@^2.0.0: + version "2.3.1" + resolved "https://registry.npmjs.org/process-warning/-/process-warning-2.3.1.tgz" + integrity sha512-JjBvFEn7MwFbzUDa2SRtKJSsyO0LlER4V/FmwLMhBlXNbGgGxdyFCxIdMDLerWUycsVUyaoM9QFLvppFy4IWaQ== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + prompts@^2.0.1: version "2.4.2" resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -4284,6 +4410,11 @@ tr46@~0.0.3: resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +ts-api-utils@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331" + integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg== + ts-essentials@^9.4.1: version "9.4.1" resolved "https://registry.npmjs.org/ts-essentials/-/ts-essentials-9.4.1.tgz" @@ -4423,6 +4554,11 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + util@^0.12.3: version "0.12.5" resolved "https://registry.npmjs.org/util/-/util-0.12.5.tgz"