hephaestus 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +13 -0
  4. data/LICENSE.txt +9 -0
  5. data/README.md +19 -0
  6. data/bin/hephaestus +55 -0
  7. data/lib/hephaestus/actions/strip_comments_action.rb +263 -0
  8. data/lib/hephaestus/actions.rb +116 -0
  9. data/lib/hephaestus/app_builder.rb +168 -0
  10. data/lib/hephaestus/exit_on_failure.rb +22 -0
  11. data/lib/hephaestus/generators/app_generator.rb +158 -0
  12. data/lib/hephaestus/generators/base.rb +65 -0
  13. data/lib/hephaestus/generators/config_generator.rb +102 -0
  14. data/lib/hephaestus/generators/core_generator.rb +50 -0
  15. data/lib/hephaestus/generators/deployment_generator.rb +18 -0
  16. data/lib/hephaestus/generators/lib_generator.rb +16 -0
  17. data/lib/hephaestus/generators/license_generator.rb +16 -0
  18. data/lib/hephaestus/generators/rubocop_generator.rb +18 -0
  19. data/lib/hephaestus/generators/sorbet_generator.rb +16 -0
  20. data/lib/hephaestus/version.rb +11 -0
  21. data/lib/hephaestus.rb +21 -0
  22. data/templates/Gemfile.erb +121 -0
  23. data/templates/Procfile.debug +2 -0
  24. data/templates/Procfile.dev +2 -0
  25. data/templates/README.md.erb +1 -0
  26. data/templates/app/controllers/application_controller.rb +107 -0
  27. data/templates/app/controllers/concerns/authable.rb +32 -0
  28. data/templates/app/controllers/root_controller.rb +11 -0
  29. data/templates/app/controllers/settings_controller.rb +7 -0
  30. data/templates/app/controllers/staff_controller.rb +15 -0
  31. data/templates/app/controllers/yetto_controller.rb +30 -0
  32. data/templates/app/jobs/application_job.rb +10 -0
  33. data/templates/app/jobs/update_yetto_job.rb +27 -0
  34. data/templates/app/lib/body_parameter/yetto_parameters.rb +8 -0
  35. data/templates/app/lib/body_parameter.rb +6 -0
  36. data/templates/app/lib/constants/app.rb +8 -0
  37. data/templates/app/lib/headers/yetto.rb +17 -0
  38. data/templates/app/lib/headers.rb +5 -0
  39. data/templates/app/lib/path_parameter/yetto_parameters.rb +25 -0
  40. data/templates/app/lib/path_parameter.rb +8 -0
  41. data/templates/app/lib/plug_app/http.rb +34 -0
  42. data/templates/app/lib/plug_app/middleware/malformed_request.rb +110 -0
  43. data/templates/app/lib/plug_app/middleware/not_found.rb +41 -0
  44. data/templates/app/lib/plug_app/middleware/openapi_validation.rb +54 -0
  45. data/templates/app/lib/plug_app/middleware/tracing_attributes.rb +42 -0
  46. data/templates/app/lib/query_parameter.rb +6 -0
  47. data/templates/app/serializers/error_serializer.rb +16 -0
  48. data/templates/app/services/yetto_service.rb +61 -0
  49. data/templates/app/views/settings/index.json.jbuilder +15 -0
  50. data/templates/config/initializers/cors.rb +18 -0
  51. data/templates/config/initializers/environment.rb +30 -0
  52. data/templates/config/initializers/filter_parameter_logging.rb +22 -0
  53. data/templates/config/initializers/inflections.rb +20 -0
  54. data/templates/config/initializers/lograge.rb +25 -0
  55. data/templates/config/initializers/open_telemetry.rb +27 -0
  56. data/templates/config/initializers/sidekiq.rb +11 -0
  57. data/templates/config/initializers/slack_webhook_logger.rb +17 -0
  58. data/templates/config/sidekiq.yml +18 -0
  59. data/templates/hephaestus_gitignore +296 -0
  60. data/templates/lib/plug_app/schemas/api/2023-03-06/components/parameters/headers/yetto.json +42 -0
  61. data/templates/lib/plug_app/schemas/api/2023-03-06/components/parameters/plugInstallation.json +12 -0
  62. data/templates/lib/plug_app/schemas/api/2023-03-06/components/schemas/plug.json +9 -0
  63. data/templates/lib/plug_app/schemas/api/2023-03-06/components/schemas/responses.json +64 -0
  64. data/templates/lib/plug_app/schemas/api/2023-03-06/components/schemas/yetto.json +1 -0
  65. data/templates/lib/plug_app/schemas/api/2023-03-06/openapi.json +27 -0
  66. data/templates/lib/plug_app/schemas/api/2023-03-06/paths/plug.json +91 -0
  67. data/templates/lib/plug_app/schemas/api/2023-03-06/paths/yetto/after_create_message.json +41 -0
  68. data/templates/lib/plug_app/schemas/api/2023-03-06/paths/yetto/after_create_plug_installation.json +41 -0
  69. data/templates/lib/tasks/test_tasks.rake +10 -0
  70. data/templates/script/ci +7 -0
  71. data/templates/script/hmac_text +22 -0
  72. data/templates/script/licenses +51 -0
  73. data/templates/script/ngrok +5 -0
  74. data/templates/script/security_checks/brakeman +5 -0
  75. data/templates/script/security_checks/bundle-audit +5 -0
  76. data/templates/script/server +5 -0
  77. data/templates/script/server-debug +5 -0
  78. data/templates/script/test +5 -0
  79. data/templates/script/typecheck +42 -0
  80. data/templates/sorbet/custom.rbi +14 -0
  81. data/templates/test/controllers/root_controller_test.rb +12 -0
  82. data/templates/test/controllers/settings_controller_test.rb +27 -0
  83. data/templates/test/controllers/yetto_controller_test.rb +130 -0
  84. data/templates/test/jobs/update_yetto_job_test.rb +41 -0
  85. data/templates/test/support/api.rb +74 -0
  86. data/templates/test/support/rails.rb +39 -0
  87. data/templates/test/support/webmocks/slack_webmock.rb +24 -0
  88. data/templates/test/support/webmocks/yetto.rb +94 -0
  89. data/templates/test/support/webmocks.rb +5 -0
  90. data/templates/test/test_helper.rb +24 -0
  91. metadata +209 -0
@@ -0,0 +1,107 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ class ApplicationController < ActionController::Base
5
+ include ActionController::MimeResponds
6
+
7
+ rescue_from ActionController::UnknownFormat, with: :not_acceptable
8
+
9
+ before_action :set_request_span_context
10
+ def set_request_span_context
11
+ end
12
+
13
+ after_action :set_response_span_context
14
+ def set_response_span_context
15
+ OpenTelemetry::Trace.current_span.add_attributes({
16
+ OpenTelemetry::SemanticConventions::Trace::HTTP_RESPONSE_CONTENT_LENGTH => response.headers["content-length"] || 0,
17
+ })
18
+ end
19
+
20
+ def no_content
21
+ head(:no_content)
22
+ end
23
+
24
+ def okay
25
+ render(
26
+ json: {
27
+ message: "OK",
28
+ }.to_json,
29
+ status: :ok,
30
+ )
31
+ end
32
+
33
+ def created
34
+ render(
35
+ json: {
36
+ message: "Created",
37
+ }.to_json,
38
+ status: :created,
39
+ )
40
+ end
41
+
42
+ def bad_request
43
+ render(
44
+ json: {
45
+ errors: [
46
+ {
47
+ message: "Bad Request",
48
+ },
49
+ ],
50
+ }.to_json,
51
+ status: :bad_request,
52
+ )
53
+ end
54
+
55
+ def forbidden
56
+ render(
57
+ json: {
58
+ errors: [
59
+ {
60
+ message: "Forbidden",
61
+ },
62
+ ],
63
+ }.to_json,
64
+ status: :forbidden,
65
+ )
66
+ end
67
+
68
+ def not_acceptable(e)
69
+ render(
70
+ json: ::ErrorSerializer.format("Not Acceptable").to_json,
71
+ status: :not_acceptable,
72
+ )
73
+ end
74
+
75
+ def not_found
76
+ render(
77
+ json: ::ErrorSerializer.format("Not Found").to_json,
78
+ status: :not_found,
79
+ )
80
+ end
81
+
82
+ def service_unavailable(msg)
83
+ render(
84
+ json: {
85
+ errors: [
86
+ {
87
+ message: "Service Unavailable: #{msg}",
88
+ },
89
+ ],
90
+ }.to_json,
91
+ status: :service_unavailable,
92
+ )
93
+ end
94
+
95
+ def bad_gateway
96
+ render(
97
+ json: {
98
+ errors: [
99
+ {
100
+ message: "Bad Gateway",
101
+ },
102
+ ],
103
+ }.to_json,
104
+ status: :bad_gateway,
105
+ )
106
+ end
107
+ end
@@ -0,0 +1,32 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Authable
5
+ extend T::Sig
6
+
7
+ include ActionDispatch::Http::Cache::Response
8
+
9
+ include ActionController::Helpers::ClassMethods
10
+ include ActionController::HttpAuthentication::Basic::ControllerMethods
11
+ include BodyParameter::YettoParameters
12
+
13
+ SHA256_DIGEST = OpenSSL::Digest.new("sha256")
14
+
15
+ sig { void }
16
+ def from_yetto?
17
+ return bad_request if request.headers.blank?
18
+
19
+ yetto_signature = request.headers.fetch(Headers::Yetto::HEADER_SIGNATURE, "")
20
+
21
+ return bad_request unless yetto_signature.start_with?("sha256=")
22
+
23
+ hmac_header = yetto_signature.split("sha256=").last
24
+ body = request.env["RAW_POST_DATA"]
25
+
26
+ calculated_hmac = OpenSSL::HMAC.hexdigest(SHA256_DIGEST, YETTO_PLUG_APP_TOKEN, body)
27
+
28
+ return true if ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac_header)
29
+
30
+ bad_request
31
+ end
32
+ end
@@ -0,0 +1,11 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ class RootController < ApplicationController
5
+ extend T::Sig
6
+
7
+ sig { void }
8
+ def index
9
+ okay
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ class SettingsController < ApplicationController
5
+ def index
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ class StaffController < ApplicationController
5
+ extend T::Sig
6
+
7
+ class << self
8
+ extend T::Sig
9
+
10
+ sig { params(request: ActionDispatch::Request).returns(T::Boolean) }
11
+ def staff_request?(request)
12
+ false
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,30 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ class YettoController < ApplicationController
5
+ include BodyParameter::YettoParameters
6
+ include PathParameter::YettoParameters
7
+ include Authable
8
+
9
+ include Headers::Yetto
10
+
11
+ before_action :from_yetto?
12
+
13
+ def event
14
+ case pparam_yetto_event
15
+ when Headers::Yetto::EVENT_AFTER_CREATE
16
+ case pparam_yetto_record_type
17
+ when Headers::Yetto::RECORD_TYPE_PLUG_INSTALLATION
18
+
19
+ no_content
20
+ when Headers::Yetto::RECORD_TYPE_MESSAGE
21
+
22
+ no_content
23
+ else
24
+ not_found
25
+ end
26
+ else
27
+ not_found
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,10 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ class ApplicationJob < ActiveJob::Base
5
+ # Automatically retry jobs that encountered a deadlock
6
+ # retry_on ActiveRecord::Deadlocked
7
+
8
+ # Most jobs are safe to ignore if the underlying records are no longer available
9
+ # discard_on ActiveJob::DeserializationError
10
+ end
@@ -0,0 +1,27 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ # Send updated data to Yetto to store in the database
5
+ # This can be used to update installation data or message data
6
+
7
+ class UpdateYettoJob < ApplicationJob
8
+ queue_as :default
9
+
10
+ def perform(params)
11
+ type = params.delete(:type)
12
+
13
+ organization_id = params.fetch(:organization, {}).fetch(:id, nil)
14
+ inbox_id = params.fetch(:inbox, {}).fetch(:id, nil)
15
+ plug_installation_id = params.fetch(:plug_installation, {}).fetch(:id, nil)
16
+
17
+ case type
18
+ when "installation"
19
+ YettoService.update_installation(organization_id, inbox_id, plug_installation_id, params)
20
+ when "switch"
21
+ plug_id = params.fetch(:plug, {}).fetch(:id, nil)
22
+ YettoService.create_switch(organization_id, inbox_id, plug_id, params)
23
+ when "message"
24
+ YettoService.create_message(organization_id, inbox_id, params)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,8 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module BodyParameter
5
+ module YettoParameters
6
+ extend T::Sig
7
+ end
8
+ end
@@ -0,0 +1,6 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module BodyParameter
5
+ extend T::Sig
6
+ end
@@ -0,0 +1,8 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module Constants
5
+ module PlugApp
6
+ PLUG_APP_API_VERSION = "2023-03-06"
7
+ end
8
+ end
@@ -0,0 +1,17 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module Headers
5
+ module Yetto
6
+ YETTO_DELIVERY_ID = "HTTP_X_YETTO_DELIVERY_ID"
7
+
8
+ HEADER_EVENT = "HTTP_X_YETTO_EVENT"
9
+ EVENT_AFTER_CREATE = "after_create"
10
+
11
+ HEADER_RECORD_TYPE = "HTTP_X_YETTO_RECORD_TYPE"
12
+ RECORD_TYPE_PLUG_INSTALLATION = "plug_installation"
13
+ RECORD_TYPE_MESSAGE = "message"
14
+
15
+ HEADER_SIGNATURE = "HTTP_X_YETTO_SIGNATURE"
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module Headers
5
+ end
@@ -0,0 +1,25 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module PathParameter
5
+ module YettoParameters
6
+ extend T::Sig
7
+
8
+ sig { returns(String) }
9
+ def pparam_yetto_event
10
+ yetto_path_params.fetch(:event, "")
11
+ end
12
+
13
+ sig { returns(String) }
14
+ def pparam_yetto_record_type
15
+ yetto_path_params.fetch(:record_type, "")
16
+ end
17
+
18
+ sig { returns(ActionController::Parameters) }
19
+ def yetto_path_params
20
+ return ActionController::Parameters.new if params.blank?
21
+
22
+ params.permit(:event, :record_type)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,8 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module PathParameter
5
+ extend T::Sig
6
+
7
+ delegate :path_parameters, to: :request
8
+ end
@@ -0,0 +1,34 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module PlugApp
5
+ module HTTP
6
+ extend T::Sig
7
+
8
+ OK = "OK"
9
+ OK_I = 200
10
+
11
+ CREATED = "Created"
12
+ CREATED_I = 201
13
+ NO_CONTENT = "No Content"
14
+ NO_CONTENT_I = 204
15
+
16
+ NOT_FOUND = "Not Found"
17
+ NOT_FOUND_I = 404
18
+ BAD_REQUEST = "Bad Request"
19
+ BAD_REQUEST_I = 400
20
+ UNAUTHORIZED = "Unauthorized"
21
+ UNAUTHORIZED_I = 401
22
+ FORBIDDEN = "Forbidden"
23
+ FORBIDDEN_I = 403
24
+ NOT_ACCEPTABLE = "Not Acceptable"
25
+ NOT_ACCEPTABLE_I = 406
26
+ SERVICE_UNAVAILABLE = "Service Unavailable"
27
+ SERVICE_UNAVAILABLE_I = 503
28
+
29
+ sig { params(status: Integer).returns(T::Boolean) }
30
+ def status_ok?(status)
31
+ status == OK_I
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,110 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module PlugApp
5
+ module Middleware
6
+ # There is no valid reason for a request to contain a malformed string
7
+ # so just return HTTP 400 (Bad Request) if we receive one
8
+ class MalformedRequest
9
+ extend T::Sig
10
+
11
+ include ActionController::HttpAuthentication::Basic
12
+
13
+ NULL_BYTE_REGEX = T.let(Regexp.new(Regexp.escape("\u0000")).freeze, Regexp)
14
+
15
+ sig { returns(T.untyped) }
16
+ attr_reader :app
17
+
18
+ sig { params(app: T.untyped).void }
19
+ def initialize(app)
20
+ @app = T.let(app, T.untyped)
21
+ end
22
+
23
+ sig { params(env: T.untyped).returns(T.untyped) }
24
+ def call(env)
25
+ return [PlugApp::HTTP::BAD_REQUEST_I, { "Content-Type" => "text/plain" }, [PlugApp::HTTP::BAD_REQUEST]] if request_contains_malformed_string?(env)
26
+
27
+ app.call(env)
28
+ end
29
+
30
+ private
31
+
32
+ sig { params(env: T.untyped).returns(T::Boolean) }
33
+ def request_contains_malformed_string?(env)
34
+ # Duplicate the env, so it is not modified when accessing the parameters
35
+ # https://github.com/rails/rails/blob/34991a6ae2fc68347c01ea7382fa89004159e019/actionpack/lib/action_dispatch/http/parameters.rb#L59
36
+ request = ActionDispatch::Request.new(env.dup)
37
+
38
+ return true if malformed_path?(request.path)
39
+ return true if credentials_malformed?(request)
40
+
41
+ request.params.values.any? do |value|
42
+ param_has_null_byte?(value)
43
+ end
44
+ rescue ActionController::BadRequest
45
+ # If we can't build an ActionDispatch::Request something's wrong
46
+ # This would also happen if `#params` contains invalid UTF-8
47
+ # in this case we'll return a 400
48
+ true
49
+ end
50
+
51
+ sig { params(path: String).returns(T::Boolean) }
52
+ def malformed_path?(path)
53
+ string_malformed?(Rack::Utils.unescape(path))
54
+ rescue ArgumentError
55
+ # Rack::Utils.unescape raised this, path is malformed.
56
+ true
57
+ end
58
+
59
+ sig { params(request: T.untyped).returns(T::Boolean) }
60
+ def credentials_malformed?(request)
61
+ credentials = if has_basic_credentials?(request)
62
+ decode_credentials(request).presence
63
+ else
64
+ request.authorization.presence
65
+ end
66
+
67
+ return false unless credentials
68
+
69
+ string_malformed?(credentials)
70
+ end
71
+
72
+ sig { params(value: T.untyped, depth: Integer).returns(T::Boolean) }
73
+ def param_has_null_byte?(value, depth = 0)
74
+ # Guard against possible attack sending large amounts of nested params
75
+ # Should be safe as deeply nested params are highly uncommon.
76
+ return false if depth > 2
77
+
78
+ depth += 1
79
+
80
+ if value.respond_to?(:match)
81
+ string_malformed?(value)
82
+ elsif value.respond_to?(:values)
83
+ value.values.any? do |hash_value|
84
+ param_has_null_byte?(hash_value, depth)
85
+ end
86
+ elsif value.is_a?(Array)
87
+ value.any? do |array_value|
88
+ param_has_null_byte?(array_value, depth)
89
+ end
90
+ else
91
+ false
92
+ end
93
+ end
94
+
95
+ sig { params(string: String).returns(T::Boolean) }
96
+ def string_malformed?(string)
97
+ # We're using match instead of include because that raises an ArgumentError
98
+ # when the string contains invalid UTF-8
99
+ #
100
+ # We try to encode the string from ASCII-8BIT to UTF8. If we failed to do
101
+ # so for certain characters in the string, those chars are probably incomplete
102
+ # multibyte characters.
103
+ string.dup.force_encoding(Encoding::UTF_8).match?(NULL_BYTE_REGEX)
104
+ rescue ArgumentError, Encoding::UndefinedConversionError
105
+ # If we're here, we caught a malformed string. Return true
106
+ true
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,41 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require "openapi_first"
5
+
6
+ module PlugApp
7
+ module Middleware
8
+ # frozen_string_literal: true
9
+
10
+ # Rack::NotFound is a default endpoint. Optionally initialize with the
11
+ # path to a custom 404 page, to override the standard response body.
12
+ #
13
+ # Examples:
14
+ #
15
+ # Serve default 404 response:
16
+ # run Rack::NotFound.new
17
+ #
18
+ # Serve a custom 404 page:
19
+ # run Rack::NotFound.new('path/to/your/404.html')
20
+
21
+ class NotFound
22
+ F = ::File
23
+
24
+ def initialize(path = nil, content_type = "text/html")
25
+ if path.nil?
26
+ @content = "Not found\n"
27
+ else
28
+ @content = F.read(path)
29
+ @content = F.read(path)
30
+ end
31
+ @length = @content.bytesize.to_s
32
+
33
+ @content_type = content_type
34
+ end
35
+
36
+ def call(env)
37
+ [404, { "Content-Type" => @content_type, "Content-Length" => @length }, [@content]]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,54 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require "openapi_first"
5
+
6
+ module PlugApp
7
+ module Middleware
8
+ class OpenapiValidation
9
+ API_PATH_PREFIX = "/api/"
10
+ SPEC_PATH = Rails.root.join("lib/plug_app/schemas/api/2023-03-06/openapi.json")
11
+
12
+ def initialize(app)
13
+ @app = app
14
+ spec = OpenapiFirst.load(SPEC_PATH)
15
+ @response_validator = OpenapiFirst::ResponseValidator.new(spec)
16
+ @request_validator = OpenapiFirst::RequestValidation.new(->(_env) {}, spec: SPEC_PATH, raise_error: true)
17
+ end
18
+
19
+ def call(env)
20
+ request = Rack::Request.new(env)
21
+
22
+ if request.path.starts_with?(API_PATH_PREFIX)
23
+ # force content-type to JSON
24
+ if env["CONTENT_TYPE"] == "application/x-www-form-urlencoded"
25
+ env["CONTENT_TYPE"] = "application/json"
26
+ end
27
+
28
+ begin
29
+ @request_validator.call(env)
30
+ # response = @app.call(env)
31
+ # @response_validator.validate(request, response)
32
+ rescue OpenapiFirst::NotFoundError
33
+ PlugApp::Middleware::NotFound.new.call(env)
34
+ rescue OpenapiFirst::RequestInvalidError => e
35
+ error_hsh = ::ErrorSerializer.format(e.message)
36
+
37
+ return [PlugApp::HTTP::BAD_REQUEST_I, { "Content-Type" => "application/json" }, [error_hsh]]
38
+ rescue => e
39
+ raise e unless Rails.env.production?
40
+
41
+ logger.error(
42
+ "openapi.request_validation.error",
43
+ path: request.path,
44
+ method: request.env["REQUEST_METHOD"],
45
+ error_message: e.message,
46
+ )
47
+ end
48
+ end
49
+
50
+ @app.call(env)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,42 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require "openapi_first"
5
+ require "active_support/parameter_filter"
6
+
7
+ module PlugApp
8
+ module Middleware
9
+ class TracingAttributes
10
+ extend T::Sig
11
+
12
+ sig { returns(T.untyped) }
13
+ attr_reader :app
14
+
15
+ HTTP_REQUEST_BODY = "http.request.body"
16
+
17
+ sig { params(app: T.untyped).void }
18
+ def initialize(app)
19
+ @app = T.let(app, T.untyped)
20
+ @filterer = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
21
+ end
22
+
23
+ sig { params(env: T.untyped).returns(T.untyped) }
24
+ def call(env)
25
+ request = ActionDispatch::Request.new(env.dup)
26
+
27
+ OpenTelemetry::Trace.current_span.add_attributes({
28
+ OpenTelemetry::VERSION => PlugApp::Application::GIT_SHA,
29
+ OpenTelemetry::SemanticConventions::Trace::HTTP_REQUEST_CONTENT_LENGTH => env["CONTENT_LENGTH"].to_i,
30
+ HTTP_REQUEST_BODY => filtered_params(request),
31
+ })
32
+
33
+ app.call(env)
34
+ end
35
+
36
+ def filtered_params(request)
37
+ params = request.params
38
+ @filterer.filter(params).to_json
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,6 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module QueryParameter
5
+ extend T::Sig
6
+ end
@@ -0,0 +1,16 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ class ErrorSerializer
5
+ class << self
6
+ def format(message)
7
+ {
8
+ errors: [
9
+ {
10
+ message: message,
11
+ },
12
+ ],
13
+ }.to_json
14
+ end
15
+ end
16
+ end