standard_id 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/standard_id/api_authentication.rb +6 -0
  3. data/app/controllers/concerns/standard_id/controller_policy.rb +99 -0
  4. data/app/controllers/concerns/standard_id/sentry_context.rb +36 -0
  5. data/app/controllers/concerns/standard_id/set_current_request_details.rb +1 -1
  6. data/app/controllers/concerns/standard_id/web_authentication.rb +5 -0
  7. data/app/controllers/standard_id/api/authorization_controller.rb +2 -0
  8. data/app/controllers/standard_id/api/base_controller.rb +1 -0
  9. data/app/controllers/standard_id/api/oauth/callback/providers_controller.rb +2 -0
  10. data/app/controllers/standard_id/api/oauth/tokens_controller.rb +2 -0
  11. data/app/controllers/standard_id/api/oidc/logout_controller.rb +2 -0
  12. data/app/controllers/standard_id/api/passwordless_controller.rb +2 -0
  13. data/app/controllers/standard_id/api/userinfo_controller.rb +2 -0
  14. data/app/controllers/standard_id/api/well_known/jwks_controller.rb +6 -0
  15. data/app/controllers/standard_id/web/account_controller.rb +2 -0
  16. data/app/controllers/standard_id/web/auth/callback/providers_controller.rb +2 -0
  17. data/app/controllers/standard_id/web/base_controller.rb +1 -0
  18. data/app/controllers/standard_id/web/login_controller.rb +2 -0
  19. data/app/controllers/standard_id/web/login_verify_controller.rb +12 -84
  20. data/app/controllers/standard_id/web/logout_controller.rb +7 -0
  21. data/app/controllers/standard_id/web/reset_password/confirm_controller.rb +2 -0
  22. data/app/controllers/standard_id/web/reset_password/start_controller.rb +2 -0
  23. data/app/controllers/standard_id/web/sessions_controller.rb +2 -0
  24. data/app/controllers/standard_id/web/signup_controller.rb +2 -0
  25. data/app/controllers/standard_id/web/verify_email/base_controller.rb +2 -0
  26. data/app/controllers/standard_id/web/verify_email/start_controller.rb +1 -1
  27. data/app/controllers/standard_id/web/verify_phone/base_controller.rb +2 -0
  28. data/app/controllers/standard_id/web/verify_phone/start_controller.rb +1 -1
  29. data/lib/standard_id/api/session_manager.rb +7 -3
  30. data/lib/standard_id/api/token_manager.rb +2 -2
  31. data/lib/standard_id/authorization_bypass.rb +121 -0
  32. data/lib/standard_id/config/schema.rb +2 -0
  33. data/lib/standard_id/jwt_service.rb +41 -15
  34. data/lib/standard_id/oauth/password_flow.rb +5 -1
  35. data/lib/standard_id/oauth/passwordless_otp_flow.rb +10 -61
  36. data/lib/standard_id/passwordless/base_strategy.rb +1 -1
  37. data/lib/standard_id/passwordless/verification_service.rb +227 -0
  38. data/lib/standard_id/testing/authentication_helpers.rb +75 -0
  39. data/lib/standard_id/testing/factories/credentials.rb +24 -0
  40. data/lib/standard_id/testing/factories/identifiers.rb +37 -0
  41. data/lib/standard_id/testing/factories/oauth.rb +89 -0
  42. data/lib/standard_id/testing/factories/sessions.rb +112 -0
  43. data/lib/standard_id/testing/factory_bot.rb +7 -0
  44. data/lib/standard_id/testing/request_helpers.rb +60 -0
  45. data/lib/standard_id/testing.rb +26 -0
  46. data/lib/standard_id/utils/ip_normalizer.rb +16 -0
  47. data/lib/standard_id/version.rb +1 -1
  48. data/lib/standard_id/web/session_manager.rb +14 -1
  49. data/lib/standard_id/web/token_manager.rb +1 -1
  50. data/lib/standard_id.rb +7 -0
  51. metadata +42 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a164420e3b36d754be94711721ead8cb4d29efed42fd6e75b0060e1c8c7c3bce
4
- data.tar.gz: cded0bf7e847cc77ed0f6aa7c69647435c847d62ae02c2af1466778a507f88f4
3
+ metadata.gz: 6f6c0f3655d0ae20c828fa745d3c600bcf9b0b11521c1c498eb601d35e24e3e2
4
+ data.tar.gz: 1f3cca500ec2bf46a4e92b9d8d4a14c7c5fff2d898567a616094b47fd7af5312
5
5
  SHA512:
6
- metadata.gz: 0b9b506d8014bd8d8cd380b1f5300a87d2d6825e073e8b959c67cbfeac3eb577abe671c9dac330833c8d27ee010b87786f533ac4e247459d691dd956d5053faf
7
- data.tar.gz: ec1a9973e61d08fff4579099dbcac25ad8355d140994c51c2621559d4aed39486d2fdd891b451ade775799f792c3db19916d555d3a50f3bc84d07ed77c1f0a52
6
+ metadata.gz: d9cb2f2cafa4ddfb0af5839168db2566769b968689a3386c5519b0dc0e06bf0a5cd1a1434df57885549000c85e5eabb2b2608b276338f969625833d09de0c118
7
+ data.tar.gz: 4a382d6268709dfe42bac61e60f969153e5054831345250ada9c08436f0681fe5a4b58454ffbf3c00f506ec40308f031e28ac65acd52c8484f7c1adbd0c88738
@@ -2,6 +2,12 @@ module StandardId
2
2
  module ApiAuthentication
3
3
  extend ActiveSupport::Concern
4
4
 
5
+ included do
6
+ if StandardId.config.alias_current_user
7
+ define_method(:current_user) { current_account }
8
+ end
9
+ end
10
+
5
11
  delegate :current_session, :current_account, :revoke_current_session!, to: :session_manager
6
12
 
7
13
  private
@@ -0,0 +1,99 @@
1
+ require "monitor"
2
+ require "set"
3
+
4
+ module StandardId
5
+ module ControllerPolicy
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :_standard_id_auth_policy, instance_writer: false
10
+ end
11
+
12
+ class_methods do
13
+ # Declares this controller as public (no host-app auth required).
14
+ def public_controller
15
+ self._standard_id_auth_policy = :public
16
+ ControllerPolicy.register(self, :public)
17
+ end
18
+
19
+ # Declares this controller as authenticated (requires host-app
20
+ # authentication but not authorization).
21
+ def authenticated_controller
22
+ self._standard_id_auth_policy = :authenticated
23
+ ControllerPolicy.register(self, :authenticated)
24
+ end
25
+
26
+ # Auto-register subclasses that inherit a policy declaration.
27
+ # This ensures controllers like VerifyEmail::ConfirmController
28
+ # (which inherits from VerifyEmail::BaseController) appear in
29
+ # the registry and receive AuthorizationBypass skips.
30
+ def inherited(subclass)
31
+ super
32
+ policy = subclass._standard_id_auth_policy
33
+ ControllerPolicy.register(subclass, policy) if policy
34
+ end
35
+ end
36
+
37
+ # Monitor instead of Mutex: registry is called from within synchronized
38
+ # blocks (e.g. register, public_controllers), so we need reentrant locking.
39
+ LOCK = Monitor.new
40
+ private_constant :LOCK
41
+
42
+ class << self
43
+ def public_controllers
44
+ LOCK.synchronize { registry[:public].dup }
45
+ end
46
+
47
+ def authenticated_controllers
48
+ LOCK.synchronize { registry[:authenticated].dup }
49
+ end
50
+
51
+ def all_controllers
52
+ LOCK.synchronize { registry[:public] + registry[:authenticated] }
53
+ end
54
+
55
+ # Registers a controller under the given policy. Raises if the
56
+ # controller is already registered under the opposite policy.
57
+ # Re-registering under the same policy is a safe no-op (Set semantics).
58
+ def register(controller, policy)
59
+ LOCK.synchronize do
60
+ other = policy == :public ? :authenticated : :public
61
+ if registry[other].include?(controller)
62
+ raise ArgumentError, "#{controller} is already registered as #{other}"
63
+ end
64
+
65
+ registry[policy] << controller
66
+ end
67
+
68
+ # If AuthorizationBypass has already been applied, apply skips to this
69
+ # newly registered controller immediately. This handles dev-mode lazy
70
+ # loading where controllers register after the initial apply_skips! call.
71
+ AuthorizationBypass.apply_to_controller(controller, policy) if AuthorizationBypass.applied?
72
+ end
73
+
74
+ # Returns a point-in-time copy of the registry, safe to iterate
75
+ # without holding the lock.
76
+ def registry_snapshot
77
+ LOCK.synchronize do
78
+ (@registry ||= { public: Set.new, authenticated: Set.new })
79
+ .transform_values(&:dup)
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def registry
86
+ LOCK.synchronize { @registry ||= { public: Set.new, authenticated: Set.new } }
87
+ end
88
+
89
+ public
90
+
91
+ # @api private — intended for test isolation only.
92
+ def reset_registry!
93
+ LOCK.synchronize do
94
+ @registry = { public: Set.new, authenticated: Set.new }
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,36 @@
1
+ module StandardId
2
+ # Sets Sentry user context from the current authenticated account.
3
+ #
4
+ # This is a standalone concern that host apps can include in their
5
+ # ApplicationController to automatically set Sentry user context
6
+ # for each request. It eliminates the need for apps to write
7
+ # their own SentryContext boilerplate.
8
+ #
9
+ # Safe to include even when the Sentry gem is not installed -- the
10
+ # callback is a no-op if `Sentry` is not defined.
11
+ #
12
+ # @example
13
+ # class ApplicationController < ActionController::Base
14
+ # include StandardId::WebAuthentication
15
+ # include StandardId::SentryContext
16
+ # end
17
+ module SentryContext
18
+ extend ActiveSupport::Concern
19
+
20
+ included do
21
+ before_action :set_standard_id_sentry_context
22
+ end
23
+
24
+ private
25
+
26
+ def set_standard_id_sentry_context
27
+ return unless defined?(Sentry)
28
+ return unless respond_to?(:current_account, true) && current_account.present?
29
+
30
+ context = { id: current_account.id }
31
+ context[:session_id] = current_session.id if respond_to?(:current_session, true) && current_session.present?
32
+
33
+ Sentry.set_user(context)
34
+ end
35
+ end
36
+ end
@@ -12,7 +12,7 @@ module StandardId
12
12
  return unless defined?(::Current)
13
13
 
14
14
  ::Current.request_id = request.request_id if ::Current.respond_to?(:request_id=)
15
- ::Current.ip_address = request.remote_ip if ::Current.respond_to?(:ip_address=)
15
+ ::Current.ip_address = StandardId::Utils::IpNormalizer.normalize(request.remote_ip) if ::Current.respond_to?(:ip_address=)
16
16
  ::Current.user_agent = request.user_agent if ::Current.respond_to?(:user_agent=)
17
17
  end
18
18
  end
@@ -5,6 +5,11 @@ module StandardId
5
5
  included do
6
6
  include StandardId::InertiaSupport
7
7
  helper_method :current_account, :authenticated?
8
+
9
+ if StandardId.config.alias_current_user
10
+ define_method(:current_user) { current_account }
11
+ helper_method :current_user
12
+ end
8
13
  end
9
14
 
10
15
  delegate :current_session, :current_account, :revoke_current_session!, to: :session_manager
@@ -1,6 +1,8 @@
1
1
  module StandardId
2
2
  module Api
3
3
  class AuthorizationController < BaseController
4
+ public_controller
5
+
4
6
  include ActionController::Cookies
5
7
 
6
8
  skip_before_action :validate_content_type!
@@ -1,6 +1,7 @@
1
1
  module StandardId
2
2
  module Api
3
3
  class BaseController < ActionController::API
4
+ include StandardId::ControllerPolicy
4
5
  include StandardId::ApiAuthentication
5
6
  include StandardId::SetCurrentRequestDetails
6
7
 
@@ -2,6 +2,8 @@ module StandardId
2
2
  module Api::Oauth
3
3
  module Callback
4
4
  class ProvidersController < BaseController
5
+ public_controller
6
+
5
7
  include StandardId::SocialAuthentication
6
8
 
7
9
  skip_before_action :validate_content_type!
@@ -2,6 +2,8 @@ module StandardId
2
2
  module Api
3
3
  module Oauth
4
4
  class TokensController < BaseController
5
+ public_controller
6
+
5
7
  skip_before_action :validate_content_type!
6
8
 
7
9
  FLOW_STRATEGIES = {
@@ -2,6 +2,8 @@ module StandardId
2
2
  module Api
3
3
  module Oidc
4
4
  class LogoutController < ::StandardId::Api::BaseController
5
+ public_controller
6
+
5
7
  include ActionController::Cookies
6
8
 
7
9
  skip_before_action :validate_content_type!
@@ -1,6 +1,8 @@
1
1
  module StandardId
2
2
  module Api
3
3
  class PasswordlessController < BaseController
4
+ public_controller
5
+
4
6
  include StandardId::PasswordlessStrategy
5
7
 
6
8
  def start
@@ -1,6 +1,8 @@
1
1
  module StandardId
2
2
  module Api
3
3
  class UserinfoController < BaseController
4
+ authenticated_controller
5
+
4
6
  skip_before_action :validate_content_type!
5
7
 
6
8
  def show
@@ -3,7 +3,13 @@
3
3
  module StandardId
4
4
  module Api
5
5
  module WellKnown
6
+ # Inherits from ActionController::API (not Api::BaseController) to avoid
7
+ # content-type validation and no-store cache headers — JWKS is a public,
8
+ # cacheable endpoint. Includes ControllerPolicy directly as a result.
6
9
  class JwksController < ActionController::API
10
+ include StandardId::ControllerPolicy
11
+ public_controller
12
+
7
13
  def show
8
14
  jwks = StandardId::JwtService.jwks
9
15
 
@@ -1,6 +1,8 @@
1
1
  module StandardId
2
2
  module Web
3
3
  class AccountController < BaseController
4
+ authenticated_controller
5
+
4
6
  def show
5
7
  @account = current_account
6
8
  @sessions = current_account.sessions.active.order(created_at: :desc)
@@ -3,6 +3,8 @@ module StandardId
3
3
  module Auth
4
4
  module Callback
5
5
  class ProvidersController < StandardId::Web::BaseController
6
+ public_controller
7
+
6
8
  include StandardId::WebAuthentication
7
9
  include StandardId::SocialAuthentication
8
10
  include StandardId::Web::SocialLoginParams
@@ -1,6 +1,7 @@
1
1
  module StandardId
2
2
  module Web
3
3
  class BaseController < ApplicationController
4
+ include StandardId::ControllerPolicy
4
5
  include StandardId::WebAuthentication
5
6
  include StandardId::SetCurrentRequestDetails
6
7
 
@@ -1,6 +1,8 @@
1
1
  module StandardId
2
2
  module Web
3
3
  class LoginController < BaseController
4
+ public_controller
5
+
4
6
  include StandardId::InertiaRendering
5
7
  include StandardId::Web::SocialLoginParams
6
8
  include StandardId::PasswordlessStrategy
@@ -1,10 +1,9 @@
1
1
  module StandardId
2
2
  module Web
3
3
  class LoginVerifyController < BaseController
4
- include StandardId::InertiaRendering
5
- include StandardId::PasswordlessStrategy
4
+ public_controller
6
5
 
7
- class OtpVerificationFailed < StandardError; end
6
+ include StandardId::InertiaRendering
8
7
 
9
8
  layout "public"
10
9
 
@@ -27,42 +26,21 @@ module StandardId
27
26
  return
28
27
  end
29
28
 
30
- # Record failed attempts outside the main transaction so they persist
31
- challenge = find_active_challenge
32
- attempts = record_attempt(challenge, code)
33
-
34
- if challenge.blank? || !ActiveSupport::SecurityUtils.secure_compare(challenge.code, code)
35
- emit_otp_validation_failed(attempts) if challenge.present?
29
+ result = StandardId::Passwordless::VerificationService.verify(
30
+ connection: @otp_data[:connection],
31
+ username: @otp_data[:username],
32
+ code: code,
33
+ request: request
34
+ )
36
35
 
37
- flash.now[:alert] = "Invalid or expired verification code"
36
+ unless result.success?
37
+ flash.now[:alert] = result.error
38
38
  render_with_inertia action: :show, props: verify_page_props, status: :unprocessable_content
39
39
  return
40
40
  end
41
41
 
42
- strategy = strategy_for(@otp_data[:connection])
43
-
44
- begin
45
- ActiveRecord::Base.transaction do
46
- # Re-fetch with lock inside transaction to prevent concurrent use
47
- locked_challenge = StandardId::CodeChallenge.lock.find(challenge.id)
48
- raise OtpVerificationFailed unless locked_challenge.active?
49
-
50
- account = strategy.find_or_create_account(@otp_data[:username])
51
- locked_challenge.use!
52
-
53
- emit_otp_validated(account, locked_challenge)
54
- session_manager.sign_in_account(account)
55
- emit_authentication_succeeded(account)
56
- end
57
- rescue OtpVerificationFailed
58
- flash.now[:alert] = "Invalid or expired verification code"
59
- render_with_inertia action: :show, props: verify_page_props, status: :unprocessable_content
60
- return
61
- rescue ActiveRecord::RecordInvalid => e
62
- flash.now[:alert] = "Unable to complete sign in: #{e.record.errors.full_messages.to_sentence}"
63
- render_with_inertia action: :show, props: verify_page_props, status: :unprocessable_content
64
- return
65
- end
42
+ session_manager.sign_in_account(result.account)
43
+ emit_authentication_succeeded(result.account)
66
44
 
67
45
  session.delete(:standard_id_otp_payload)
68
46
 
@@ -98,56 +76,6 @@ module StandardId
98
76
  end
99
77
  end
100
78
 
101
- def find_active_challenge
102
- StandardId::CodeChallenge.active.find_by(
103
- realm: "authentication",
104
- channel: @otp_data[:connection],
105
- target: @otp_data[:username]
106
- )
107
- end
108
-
109
- def record_attempt(challenge, code)
110
- return 0 if challenge.blank?
111
- return 0 if ActiveSupport::SecurityUtils.secure_compare(challenge.code, code)
112
-
113
- attempts = (challenge.metadata["attempts"] || 0) + 1
114
- challenge.update!(metadata: challenge.metadata.merge("attempts" => attempts))
115
-
116
- max_attempts = StandardId.config.passwordless.max_attempts
117
- challenge.use! if attempts >= max_attempts
118
-
119
- attempts
120
- end
121
-
122
- def emit_otp_validated(account, challenge)
123
- StandardId::Events.publish(
124
- StandardId::Events::OTP_VALIDATED,
125
- account: account,
126
- channel: @otp_data[:connection]
127
- )
128
- StandardId::Events.publish(
129
- StandardId::Events::PASSWORDLESS_CODE_VERIFIED,
130
- code_challenge: challenge,
131
- account: account,
132
- channel: @otp_data[:connection]
133
- )
134
- end
135
-
136
- def emit_otp_validation_failed(attempts)
137
- StandardId::Events.publish(
138
- StandardId::Events::OTP_VALIDATION_FAILED,
139
- identifier: @otp_data[:username],
140
- channel: @otp_data[:connection],
141
- attempts: attempts
142
- )
143
- StandardId::Events.publish(
144
- StandardId::Events::PASSWORDLESS_CODE_FAILED,
145
- identifier: @otp_data[:username],
146
- channel: @otp_data[:connection],
147
- attempts: attempts
148
- )
149
- end
150
-
151
79
  def emit_authentication_succeeded(account)
152
80
  StandardId::Events.publish(
153
81
  StandardId::Events::AUTHENTICATION_SUCCEEDED,
@@ -1,6 +1,13 @@
1
1
  module StandardId
2
2
  module Web
3
3
  class LogoutController < BaseController
4
+ # Classified as authenticated (not public) because logout requires
5
+ # host-app authentication. The controller handles unauthenticated
6
+ # users gracefully via redirect_if_not_authenticated rather than
7
+ # raising, and skips require_browser_session! to allow session
8
+ # revocation even with an expired browser session.
9
+ authenticated_controller
10
+
4
11
  skip_before_action :require_browser_session!, only: [:create]
5
12
 
6
13
  before_action :redirect_if_not_authenticated
@@ -2,6 +2,8 @@ module StandardId
2
2
  module Web
3
3
  module ResetPassword
4
4
  class ConfirmController < BaseController
5
+ public_controller
6
+
5
7
  layout "public"
6
8
 
7
9
  skip_before_action :require_browser_session!, only: [:show, :update]
@@ -2,6 +2,8 @@ module StandardId
2
2
  module Web
3
3
  module ResetPassword
4
4
  class StartController < BaseController
5
+ public_controller
6
+
5
7
  layout "public"
6
8
 
7
9
  skip_before_action :require_browser_session!, only: [:show, :create]
@@ -1,6 +1,8 @@
1
1
  module StandardId
2
2
  module Web
3
3
  class SessionsController < BaseController
4
+ authenticated_controller
5
+
4
6
  def index
5
7
  @sessions = current_account.sessions.active.order(created_at: :desc)
6
8
  @current_session = current_session
@@ -1,6 +1,8 @@
1
1
  module StandardId
2
2
  module Web
3
3
  class SignupController < BaseController
4
+ public_controller
5
+
4
6
  include StandardId::InertiaRendering
5
7
 
6
8
  layout "public"
@@ -2,6 +2,8 @@ module StandardId
2
2
  module Web
3
3
  module VerifyEmail
4
4
  class BaseController < StandardId::Web::BaseController
5
+ public_controller
6
+
5
7
  skip_before_action :require_browser_session!
6
8
  end
7
9
  end
@@ -19,7 +19,7 @@ module StandardId
19
19
  target: email,
20
20
  code: generate_otp_code,
21
21
  expires_at: 10.minutes.from_now,
22
- ip_address: request.remote_ip,
22
+ ip_address: StandardId::Utils::IpNormalizer.normalize(request.remote_ip),
23
23
  user_agent: request.user_agent
24
24
  )
25
25
 
@@ -2,6 +2,8 @@ module StandardId
2
2
  module Web
3
3
  module VerifyPhone
4
4
  class BaseController < StandardId::Web::BaseController
5
+ public_controller
6
+
5
7
  skip_before_action :require_browser_session!
6
8
  end
7
9
  end
@@ -19,7 +19,7 @@ module StandardId
19
19
  target: phone,
20
20
  code: generate_otp_code,
21
21
  expires_at: 10.minutes.from_now,
22
- ip_address: request.remote_ip,
22
+ ip_address: StandardId::Utils::IpNormalizer.normalize(request.remote_ip),
23
23
  user_agent: request.user_agent
24
24
  )
25
25
 
@@ -12,9 +12,7 @@ module StandardId
12
12
 
13
13
  def current_account
14
14
  return unless current_session
15
- @current_account ||= StandardId.account_class
16
- .find_by(id: current_session.account_id)
17
- &.tap { |a| a.strict_loading!(false) }
15
+ @current_account ||= load_current_account
18
16
  end
19
17
 
20
18
  def revoke_current_session!
@@ -28,6 +26,12 @@ module StandardId
28
26
 
29
27
  private
30
28
 
29
+ def load_current_account
30
+ scope = StandardId.account_class
31
+ scope = StandardId.config.account_scope.call(scope) if StandardId.config.account_scope
32
+ scope.find_by(id: current_session.account_id)&.tap { |a| a.strict_loading!(false) }
33
+ end
34
+
31
35
  def load_current_session
32
36
  return @current_session if @current_session.present?
33
37
 
@@ -10,7 +10,7 @@ module StandardId
10
10
  def create_device_session(account, device_id: nil, device_agent: nil)
11
11
  StandardId::DeviceSession.create!(
12
12
  account:,
13
- ip_address: @request.remote_ip,
13
+ ip_address: StandardId::Utils::IpNormalizer.normalize(@request.remote_ip),
14
14
  device_id: device_id || SecureRandom.uuid,
15
15
  device_agent: device_agent || @request.user_agent,
16
16
  expires_at: StandardId::DeviceSession.expiry
@@ -21,7 +21,7 @@ module StandardId
21
21
  StandardId::ServiceSession.create!(
22
22
  account:,
23
23
  owner:,
24
- ip_address: @request.remote_ip,
24
+ ip_address: StandardId::Utils::IpNormalizer.normalize(@request.remote_ip),
25
25
  service_name:,
26
26
  service_version:,
27
27
  metadata: metadata || {},