mpp-rb 0.1.5 → 0.1.7

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +150 -0
  3. data/lib/mpp/challenge.rb +31 -8
  4. data/lib/mpp/challenge_echo.rb +14 -3
  5. data/lib/mpp/challenge_id.rb +30 -6
  6. data/lib/mpp/client/transport.rb +1 -1
  7. data/lib/mpp/extensions/mcp/verify.rb +1 -1
  8. data/lib/mpp/methods/evm/charge_intent.rb +38 -17
  9. data/lib/mpp/methods/evm/evm_method.rb +18 -2
  10. data/lib/mpp/methods/evm.rb +5 -2
  11. data/lib/mpp/methods/stripe/analytics_metadata.rb +24 -0
  12. data/lib/mpp/methods/stripe/charge_intent.rb +34 -11
  13. data/lib/mpp/methods/stripe/crypto_payment_recorder.rb +92 -0
  14. data/lib/mpp/methods/stripe/defaults.rb +1 -0
  15. data/lib/mpp/methods/stripe/machine_payments.rb +128 -0
  16. data/lib/mpp/methods/stripe/payment_intent_method.rb +103 -0
  17. data/lib/mpp/methods/stripe/payment_intent_options.rb +81 -0
  18. data/lib/mpp/methods/stripe/stripe_method.rb +30 -3
  19. data/lib/mpp/methods/stripe.rb +26 -0
  20. data/lib/mpp/methods/tempo/client_method.rb +58 -12
  21. data/lib/mpp/methods/tempo/defaults.rb +51 -0
  22. data/lib/mpp/methods/tempo/intents.rb +195 -125
  23. data/lib/mpp/methods/tempo/relay.rb +33 -31
  24. data/lib/mpp/methods/tempo/schemas.rb +3 -4
  25. data/lib/mpp/parsing.rb +29 -4
  26. data/lib/mpp/server/compose.rb +32 -15
  27. data/lib/mpp/server/decorator.rb +3 -1
  28. data/lib/mpp/server/intent.rb +6 -1
  29. data/lib/mpp/server/intent_lifecycle.rb +41 -0
  30. data/lib/mpp/server/method.rb +41 -0
  31. data/lib/mpp/server/middleware.rb +16 -2
  32. data/lib/mpp/server/mpp_handler.rb +87 -21
  33. data/lib/mpp/server/verify.rb +25 -15
  34. data/lib/mpp/server.rb +1 -0
  35. data/lib/mpp/validation.rb +27 -0
  36. data/lib/mpp/version.rb +1 -1
  37. data/lib/mpp.rb +14 -3
  38. metadata +22 -1
@@ -96,8 +96,40 @@ module Mpp
96
96
  end
97
97
  private_class_method :merge_api_key_headers
98
98
 
99
+ def self.to_relay_input(credential)
100
+ echo = credential.challenge
101
+ challenge = {
102
+ "id" => echo.id,
103
+ "realm" => echo.realm,
104
+ "method" => echo.method,
105
+ "intent" => echo.intent,
106
+ "request" => decode_request(echo.request)
107
+ }
108
+ challenge["expires"] = echo.expires if echo.expires
109
+ challenge["digest"] = echo.digest if echo.digest
110
+ challenge["opaque"] = echo.opaque if echo.opaque
111
+
112
+ input = {
113
+ "challenge" => challenge,
114
+ "payload" => credential.payload
115
+ }
116
+ input["source"] = credential.source if credential.source
117
+ input
118
+ end
119
+
120
+ def self.decode_request(request)
121
+ return request if request.is_a?(Hash)
122
+ return request unless request.is_a?(String) && !request.empty?
123
+
124
+ Mpp::Parsing.b64_decode(request)
125
+ rescue Mpp::ParseError
126
+ request
127
+ end
128
+ private_class_method :decode_request
129
+
130
+ # @deprecated Use #validate followed by #broadcast.
99
131
  def verify(credential, _request = nil)
100
- input = to_relay_input(credential)
132
+ input = self.class.to_relay_input(credential)
101
133
  validate(input)
102
134
  broadcast(input)
103
135
  end
@@ -155,36 +187,6 @@ module Mpp
155
187
  raise Mpp::VerificationFailedError.new(reason: "relay request failed: #{e.message}")
156
188
  end
157
189
 
158
- def to_relay_input(credential)
159
- echo = credential.challenge
160
- challenge = {
161
- "id" => echo.id,
162
- "realm" => echo.realm,
163
- "method" => echo.method,
164
- "intent" => echo.intent,
165
- "request" => decode_request(echo.request)
166
- }
167
- challenge["expires"] = echo.expires if echo.expires
168
- challenge["digest"] = echo.digest if echo.digest
169
- challenge["opaque"] = echo.opaque if echo.opaque
170
-
171
- input = {
172
- "challenge" => challenge,
173
- "payload" => credential.payload
174
- }
175
- input["source"] = credential.source if credential.source
176
- input
177
- end
178
-
179
- def decode_request(request)
180
- return request if request.is_a?(Hash)
181
- return request unless request.is_a?(String) && !request.empty?
182
-
183
- Mpp::Parsing.b64_decode(request)
184
- rescue Mpp::ParseError
185
- request
186
- end
187
-
188
190
  def idempotency_key(input)
189
191
  payload = input["payload"]
190
192
  if payload.is_a?(Hash) && payload["type"] == "transaction" && payload["signature"].is_a?(String)
@@ -7,8 +7,8 @@ module Mpp
7
7
  module Schemas
8
8
  HEX_PATTERN = /\A0x[a-fA-F0-9]+\z/
9
9
 
10
- MethodDetails = Data.define(:chain_id, :fee_payer, :fee_payer_url, :memo) do
11
- def initialize(chain_id: 4217, fee_payer: false, fee_payer_url: nil, memo: nil)
10
+ MethodDetails = Data.define(:chain_id, :fee_payer, :fee_payer_url) do
11
+ def initialize(chain_id: 4217, fee_payer: false, fee_payer_url: nil)
12
12
  super
13
13
  end
14
14
 
@@ -18,8 +18,7 @@ module Mpp
18
18
  new(
19
19
  chain_id: hash["chainId"] || 4217,
20
20
  fee_payer: hash["feePayer"] || false,
21
- fee_payer_url: hash["feePayerUrl"],
22
- memo: hash["memo"]
21
+ fee_payer_url: hash["feePayerUrl"]
23
22
  )
24
23
  end
25
24
  end
data/lib/mpp/parsing.rb CHANGED
@@ -58,10 +58,17 @@ module Mpp
58
58
  def parse_auth_params(params_str)
59
59
  params = {}
60
60
  params_str.scan(AUTH_PARAM_RE) do |key, quoted_val, token_val|
61
- Kernel.raise Mpp::ParseError, "Duplicate parameter: #{key}" if params.key?(key)
61
+ # Auth-parameter names are case-insensitive (RFC 9110 §11.2). Normalize
62
+ # to lowercase for both the duplicate check and storage, so "id" and
63
+ # "ID" are treated as the same parameter -- otherwise both get stored
64
+ # under their original casing and only the lowercase lookup downstream
65
+ # is ever read, silently ignoring the other value instead of rejecting
66
+ # the ambiguous header.
67
+ normalized_key = key.downcase
68
+ Kernel.raise Mpp::ParseError, "Duplicate parameter: #{key}" if params.key?(normalized_key)
62
69
 
63
70
  value = quoted_val.nil? ? token_val : unescape_quoted(quoted_val)
64
- params[key] = value
71
+ params[normalized_key] = value
65
72
  end
66
73
  params
67
74
  end
@@ -101,6 +108,12 @@ module Mpp
101
108
  opaque_b64 = params["opaque"]
102
109
  opaque = (opaque_b64 && !opaque_b64.empty?) ? b64_decode(opaque_b64) : nil
103
110
 
111
+ header = begin
112
+ Mpp.advertised_credential_header(params["header"])
113
+ rescue ArgumentError => e
114
+ Kernel.raise Mpp::ParseError, e.message
115
+ end
116
+
104
117
  Mpp::Challenge.new(
105
118
  id: id,
106
119
  method: method,
@@ -111,7 +124,8 @@ module Mpp
111
124
  digest: params["digest"],
112
125
  expires: params["expires"],
113
126
  description: params["description"],
114
- opaque: opaque
127
+ opaque: opaque,
128
+ header: header
115
129
  )
116
130
  end
117
131
 
@@ -131,6 +145,9 @@ module Mpp
131
145
  parts << "digest=\"#{escape_quoted(challenge.digest)}\"" if challenge.digest
132
146
  parts << "expires=\"#{escape_quoted(challenge.expires)}\"" if challenge.expires
133
147
  parts << "description=\"#{escape_quoted(challenge.description)}\"" if challenge.description
148
+ if challenge.header
149
+ parts << "header=\"#{escape_quoted(challenge.header)}\""
150
+ end
134
151
  if challenge.opaque
135
152
  opaque_b64 = b64_encode(challenge.opaque)
136
153
  parts << "opaque=\"#{opaque_b64}\""
@@ -158,6 +175,12 @@ module Mpp
158
175
  method = challenge_data["method"]
159
176
  validate_payment_method_id(method)
160
177
 
178
+ header = begin
179
+ Mpp.advertised_credential_header(challenge_data["header"]&.to_s)
180
+ rescue ArgumentError => e
181
+ Kernel.raise Mpp::ParseError, e.message
182
+ end
183
+
161
184
  echo = Mpp::ChallengeEcho.new(
162
185
  id: challenge_data["id"].to_s,
163
186
  realm: (challenge_data["realm"] || "").to_s,
@@ -166,7 +189,8 @@ module Mpp
166
189
  request: (challenge_data["request"] || "").to_s,
167
190
  expires: challenge_data["expires"]&.to_s,
168
191
  digest: challenge_data["digest"]&.to_s,
169
- opaque: challenge_data["opaque"]&.to_s
192
+ opaque: challenge_data["opaque"]&.to_s,
193
+ header: header
170
194
  )
171
195
 
172
196
  Mpp::Credential.new(
@@ -188,6 +212,7 @@ module Mpp
188
212
  }
189
213
  challenge_dict["expires"] = credential.challenge.expires if credential.challenge.expires
190
214
  challenge_dict["digest"] = credential.challenge.digest if credential.challenge.digest
215
+ challenge_dict["header"] = credential.challenge.header if credential.challenge.header
191
216
  challenge_dict["opaque"] = credential.challenge.opaque if credential.challenge.opaque
192
217
 
193
218
  payload = {
@@ -42,8 +42,8 @@ module Mpp
42
42
  @canonical_request ||= @handler.build_charge_request(@method, amount, **charge_kwargs)
43
43
  end
44
44
 
45
- sig { params(authorization: T.nilable(String), payment_signature: T.nilable(String), body: T.untyped, url: T.nilable(String), http_method: T.nilable(String)).returns(T.untyped) }
46
- def verify(authorization: nil, payment_signature: nil, body: nil, url: nil, http_method: nil)
45
+ sig { params(authorization: T.nilable(String), payment_signature: T.nilable(String), body: T.untyped, url: T.nilable(String), http_method: T.nilable(String), request_guard: T.nilable(T.proc.params(request: T::Hash[String, T.untyped]).returns(T::Boolean))).returns(T.untyped) }
46
+ def verify(authorization: nil, payment_signature: nil, body: nil, url: nil, http_method: nil, &request_guard)
47
47
  @handler.charge_one(
48
48
  @method,
49
49
  authorization,
@@ -52,13 +52,17 @@ module Mpp
52
52
  url: url,
53
53
  body: body,
54
54
  http_method: http_method,
55
- **charge_kwargs
55
+ **charge_kwargs,
56
+ &request_guard
56
57
  )
57
58
  end
58
59
 
59
- sig { params(body: T.untyped, url: T.nilable(String), http_method: T.nilable(String)).returns(Mpp::Challenge) }
60
+ sig { params(body: T.untyped, url: T.nilable(String), http_method: T.nilable(String)).returns(T.nilable(Mpp::Challenge)) }
60
61
  def challenge(body: nil, url: nil, http_method: nil)
61
- result = verify(body: body, url: url, http_method: http_method)
62
+ result = verify(body: body, url: url, http_method: http_method) do |request|
63
+ Mpp::Server::MethodHelper.can_offer?(@method, request)
64
+ end
65
+ return nil if result.nil?
62
66
  return result if result.is_a?(Mpp::Challenge)
63
67
 
64
68
  Kernel.raise "expected a challenge from unpaid offer #{key}"
@@ -68,7 +72,7 @@ module Mpp
68
72
  def decorate_challenge(headers, challenge, url: nil, http_method: nil)
69
73
  return headers unless @method.respond_to?(:decorate_challenge)
70
74
 
71
- @method.decorate_challenge(headers, challenge, url: url, http_method: http_method, request: canonical_request)
75
+ @method.decorate_challenge(headers, challenge, url: url, http_method: http_method, request: challenge.request)
72
76
  headers
73
77
  end
74
78
 
@@ -100,7 +104,7 @@ module Mpp
100
104
  @options[:amount].to_s
101
105
  end
102
106
 
103
- REQUEST_OPTION_KEYS = [:body, :url, :payment_signature, :accept_payment, :authorization, :http_method].freeze
107
+ REQUEST_OPTION_KEYS = [:body, :url, :payment_signature, :accept_payment, :authorization, :http_method, :payment_authorization].freeze
104
108
 
105
109
  sig { returns(T::Hash[Symbol, T.untyped]) }
106
110
  def charge_kwargs
@@ -136,7 +140,7 @@ module Mpp
136
140
 
137
141
  sig { params(handler: T.untyped, entries: T::Array[T.untyped]).returns(ComposedHandler) }
138
142
  def self.from_entries(handler, entries)
139
- new(handler: handler, offers: entries.map { |entry| offer_from_entry(handler, entry) })
143
+ new(handler: handler, offers: entries.flat_map { |entry| offers_from_entry(handler, entry) })
140
144
  end
141
145
 
142
146
  # Flatten nested compositions into a single handler.
@@ -159,6 +163,7 @@ module Mpp
159
163
  sig do
160
164
  params(
161
165
  authorization: T.nilable(String),
166
+ payment_authorization: T.nilable(String),
162
167
  payment_signature: T.nilable(String),
163
168
  body: T.untyped,
164
169
  url: T.nilable(String),
@@ -167,10 +172,11 @@ module Mpp
167
172
  scope: T.nilable(T::Hash[String, String])
168
173
  ).returns(ComposedResult)
169
174
  end
170
- def call(authorization: nil, payment_signature: nil, body: nil, url: nil, accept_payment: nil, http_method: nil, scope: nil)
175
+ def call(authorization: nil, payment_authorization: nil, payment_signature: nil, body: nil, url: nil, accept_payment: nil, http_method: nil, scope: nil)
171
176
  if scope && !scope.empty?
172
177
  return self.class.new(handler: @handler, offers: @offers.map { |offer| offer.with_scope(scope) }).call(
173
178
  authorization: authorization,
179
+ payment_authorization: payment_authorization,
174
180
  payment_signature: payment_signature,
175
181
  body: body,
176
182
  url: url,
@@ -179,8 +185,9 @@ module Mpp
179
185
  )
180
186
  end
181
187
 
188
+ credential = @handler.payment_credential_value(authorization, payment_authorization)
182
189
  dispatched = dispatch_credential(
183
- authorization: authorization,
190
+ authorization: credential,
184
191
  payment_signature: payment_signature,
185
192
  body: body,
186
193
  url: url,
@@ -193,8 +200,9 @@ module Mpp
193
200
 
194
201
  private
195
202
 
196
- sig { params(handler: T.untyped, entry: T.untyped).returns(ComposeOffer) }
197
- def self.offer_from_entry(handler, entry)
203
+ # A method accepting several currencies yields one offer per currency.
204
+ sig { params(handler: T.untyped, entry: T.untyped).returns(T::Array[ComposeOffer]) }
205
+ def self.offers_from_entry(handler, entry)
198
206
  unless entry.is_a?(Array) && entry.length == 2
199
207
  Kernel.raise ArgumentError, "compose() entries must be [method, options] tuples"
200
208
  end
@@ -205,9 +213,13 @@ module Mpp
205
213
  Kernel.raise ArgumentError, "compose() options must be a Hash"
206
214
  end
207
215
 
208
- ComposeOffer.new(handler, handler.resolve_method(method_ref), options)
216
+ method = handler.resolve_method(method_ref)
217
+ currencies = Mpp::Server::MethodHelper.offer_currencies(method, options)
218
+ return [ComposeOffer.new(handler, method, options)] if currencies.nil?
219
+
220
+ currencies.map { |currency| ComposeOffer.new(handler, method, options.merge(currency: currency)) }
209
221
  end
210
- private_class_method :offer_from_entry
222
+ private_class_method :offers_from_entry
211
223
 
212
224
  sig do
213
225
  params(
@@ -330,10 +342,15 @@ module Mpp
330
342
  ).returns(ComposedResult)
331
343
  end
332
344
  def merge_challenges(body:, url:, accept_payment:, http_method:)
333
- pairs = @offers.map do |offer|
345
+ pairs = @offers.filter_map do |offer|
334
346
  challenge = offer.challenge(body: body, url: url, http_method: http_method)
347
+ next if challenge.nil?
348
+
335
349
  [offer, challenge]
336
350
  end
351
+ if pairs.empty?
352
+ Kernel.raise ArgumentError, "No payment offers are available for this request"
353
+ end
337
354
 
338
355
  ranked = AcceptPayment.apply(pairs.map { |_offer, challenge| challenge }, accept_payment)
339
356
  by_id = pairs.to_h { |offer, challenge| [challenge.id, [offer, challenge]] }
@@ -38,9 +38,11 @@ module Mpp
38
38
 
39
39
  headers[key] = value
40
40
  end
41
+ credential_headers = challenges.map(&:credential_header).uniq
42
+ vary = extra_headers.key?("PAYMENT-REQUIRED") ? credential_headers + ["PAYMENT-SIGNATURE"] : credential_headers
41
43
  Mpp::Server::Middleware.mark_authorization_bound_response(
42
44
  headers,
43
- vary: extra_headers.key?("PAYMENT-REQUIRED") ? ["Authorization", "PAYMENT-SIGNATURE"] : ["Authorization"]
45
+ vary: vary
44
46
  )
45
47
  {
46
48
  "_mpp_challenge" => true,
@@ -6,7 +6,11 @@ module Mpp
6
6
  extend T::Sig
7
7
 
8
8
  # Intent interface (duck type):
9
- # name -> String
9
+ # name -> String
10
+ # validate(credential, request) -> Validation
11
+ # broadcast(credential, request) -> Receipt
12
+ #
13
+ # Intents may temporarily implement only the deprecated verify API:
10
14
  # verify(credential, request) -> Receipt
11
15
  #
12
16
  # Implement this interface for custom payment intents.
@@ -25,6 +29,7 @@ module Mpp
25
29
  end
26
30
 
27
31
  sig { params(credential: Mpp::Credential, request: T::Hash[String, T.untyped]).returns(Mpp::Receipt) }
32
+ # @deprecated Implement #validate and #broadcast for new intents.
28
33
  def verify(credential, request)
29
34
  @verify_fn.call(credential, request)
30
35
  end
@@ -0,0 +1,41 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Mpp
5
+ module Server
6
+ # Dispatches an intent through its optional two-phase lifecycle.
7
+ #
8
+ # Two-phase intents expose separate non-mutating validation and terminal
9
+ # payment operations:
10
+ # validate(credential, request) -> Validation
11
+ # broadcast(credential, request) -> Receipt
12
+ #
13
+ # Broadcast receives the original inputs rather than trusting a prior
14
+ # validation result, matching mppx's re-validation behavior.
15
+ #
16
+ # Intents that only implement the deprecated #verify API continue to work
17
+ # through the legacy path.
18
+ module IntentLifecycle
19
+ extend T::Sig
20
+
21
+ module_function
22
+
23
+ sig { params(intent: T.untyped, credential: Mpp::Credential, request: T::Hash[String, T.untyped]).returns(Mpp::Receipt) }
24
+ def call(intent, credential, request)
25
+ has_validate = intent.respond_to?(:validate)
26
+ has_broadcast = intent.respond_to?(:broadcast)
27
+
28
+ if has_validate != has_broadcast
29
+ Kernel.raise ArgumentError, "intent must implement both #validate and #broadcast"
30
+ end
31
+
32
+ if has_validate
33
+ intent.validate(credential, request)
34
+ intent.broadcast(credential, request)
35
+ else
36
+ intent.verify(credential, request)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -7,6 +7,7 @@ module Mpp
7
7
  # name -> String
8
8
  # intents -> Hash[String, Intent]
9
9
  # create_credential(challenge) -> Credential
10
+ # on_payment_success -> optional callable receiving a payment.success payload
10
11
 
11
12
  module MethodHelper
12
13
  extend T::Sig
@@ -22,6 +23,46 @@ module Mpp
22
23
  request
23
24
  end
24
25
  end
26
+
27
+ # Give a method a chance to create a request-scoped intent view and to
28
+ # remove private server input before the canonical request is built.
29
+ sig { params(method: T.untyped, intent: T.untyped, input: T::Hash[Symbol, T.untyped]).returns([T.untyped, T::Hash[Symbol, T.untyped]]) }
30
+ def prepare_intent(method, intent, input)
31
+ return [intent, input] unless method.respond_to?(:prepare_intent)
32
+
33
+ prepared = method.prepare_intent(intent, input)
34
+ unless prepared.is_a?(Array) && prepared.length == 2 && prepared[1].is_a?(Hash)
35
+ Kernel.raise ArgumentError, "prepare_intent must return [intent, request_input]"
36
+ end
37
+
38
+ [prepared[0], prepared[1]]
39
+ end
40
+
41
+ # Check whether a method should be advertised for a canonical request.
42
+ # This only governs composing new 402 offers, never credential redemption.
43
+ sig { params(method: T.untyped, request: T::Hash[String, T.untyped]).returns(T::Boolean) }
44
+ def can_offer?(method, request)
45
+ return true unless method.respond_to?(:can_offer?)
46
+
47
+ available = method.can_offer?(request)
48
+ unless available == true || available == false
49
+ Kernel.raise ArgumentError, "can_offer? must return true or false"
50
+ end
51
+
52
+ available
53
+ end
54
+
55
+ # Currencies to fan out into one offer each, in order. Returns nil when
56
+ # the offer needs no fan-out: the method accepts at most one currency or
57
+ # the request already pins a currency.
58
+ sig { params(method: T.untyped, options: T::Hash[T.untyped, T.untyped]).returns(T.nilable(T::Array[String])) }
59
+ def offer_currencies(method, options)
60
+ return nil unless options[:currency].nil? && options["currency"].nil?
61
+ return nil unless method.respond_to?(:currencies)
62
+
63
+ currencies = method.currencies
64
+ (currencies.is_a?(Array) && currencies.length > 1) ? currencies : nil
65
+ end
25
66
  end
26
67
  end
27
68
  end
@@ -37,6 +37,7 @@ module Mpp
37
37
  return @app.call(env) unless charge_opts
38
38
 
39
39
  authorization = env["HTTP_AUTHORIZATION"]
40
+ payment_authorization = env["HTTP_PAYMENT_AUTHORIZATION"]
40
41
  payment_signature = env["HTTP_PAYMENT_SIGNATURE"]
41
42
  accept_payment = env["HTTP_ACCEPT_PAYMENT"]
42
43
  http_method = env["REQUEST_METHOD"]
@@ -50,6 +51,7 @@ module Mpp
50
51
  env,
51
52
  charge_opts,
52
53
  authorization: authorization,
54
+ payment_authorization: payment_authorization,
53
55
  payment_signature: payment_signature,
54
56
  accept_payment: accept_payment,
55
57
  http_method: http_method,
@@ -67,7 +69,7 @@ module Mpp
67
69
  extra_headers.each { |key, value| headers[key] = value unless value.nil? }
68
70
  decorate_single_method_receipt(headers, credential, receipt, payment_signature)
69
71
  end
70
- vary = ["Authorization"]
72
+ vary = [credential_vary_field]
71
73
  vary << "PAYMENT-SIGNATURE" if x402_bound?(headers, payment_signature)
72
74
  self.class.mark_authorization_bound_response(headers, vary: vary)
73
75
 
@@ -99,6 +101,7 @@ module Mpp
99
101
  env: T.untyped,
100
102
  charge_opts: T.untyped,
101
103
  authorization: T.untyped,
104
+ payment_authorization: T.untyped,
102
105
  payment_signature: T.untyped,
103
106
  accept_payment: T.untyped,
104
107
  http_method: T.untyped,
@@ -106,10 +109,11 @@ module Mpp
106
109
  request_body: T.untyped
107
110
  ).returns(T.untyped)
108
111
  end
109
- def verify_payment(env, charge_opts, authorization:, payment_signature:, accept_payment:, http_method:, url:, request_body:)
112
+ def verify_payment(env, charge_opts, authorization:, payment_authorization:, payment_signature:, accept_payment:, http_method:, url:, request_body:)
110
113
  if @handler.is_a?(Mpp::Server::ComposedHandler)
111
114
  return @handler.call(
112
115
  authorization: authorization,
116
+ payment_authorization: payment_authorization,
113
117
  payment_signature: payment_signature,
114
118
  body: request_body,
115
119
  url: url,
@@ -127,6 +131,7 @@ module Mpp
127
131
  amount,
128
132
  **opts,
129
133
  body: request_body,
134
+ payment_authorization: payment_authorization,
130
135
  payment_signature: payment_signature,
131
136
  url: url,
132
137
  accept_payment: accept_payment,
@@ -188,6 +193,15 @@ module Mpp
188
193
  headers.key?("PAYMENT-REQUIRED")
189
194
  end
190
195
 
196
+ sig { returns(String) }
197
+ def credential_vary_field
198
+ if @handler.respond_to?(:requires_auth) && @handler.requires_auth
199
+ Mpp::PAYMENT_AUTHORIZATION_HEADER
200
+ else
201
+ Mpp::AUTHORIZATION_HEADER
202
+ end
203
+ end
204
+
191
205
  sig { params(env: T.untyped).returns(String) }
192
206
  def request_url(env)
193
207
  scheme = env["HTTP_X_FORWARDED_PROTO"] || env["rack.url_scheme"] || "http"