mpp-rb 0.1.5 → 0.1.6

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.
@@ -19,14 +19,21 @@ module Mpp
19
19
  class TempoMethod
20
20
  attr_reader :name, :account, :fee_payer, :fee_payer_allowed_fee_tokens,
21
21
  :root_account, :rpc_url, :chain_id, :currency, :recipient, :decimals, :client_id,
22
- :expected_recipients, :relay
22
+ :expected_recipients, :relay, :on_payment_success
23
23
  attr_accessor :intents
24
24
 
25
25
  def initialize(account: nil, fee_payer: nil, root_account: nil,
26
26
  rpc_url: Defaults::RPC_URL, chain_id: nil, currency: nil,
27
27
  recipient: nil, decimals: 6, client_id: nil,
28
28
  expected_recipients: nil, fee_payer_allowed_fee_tokens: nil,
29
- relay: nil)
29
+ relay: nil, on_payment_success: nil, can_offer: nil)
30
+ unless on_payment_success.nil? || on_payment_success.respond_to?(:call)
31
+ raise ArgumentError, "on_payment_success must be callable"
32
+ end
33
+ if !can_offer.nil? && !can_offer.respond_to?(:call)
34
+ raise ArgumentError, "can_offer must be callable"
35
+ end
36
+
30
37
  @name = "tempo"
31
38
  @account = account
32
39
  @fee_payer = fee_payer
@@ -41,9 +48,17 @@ module Mpp
41
48
  @decimals = decimals
42
49
  @client_id = client_id
43
50
  @expected_recipients = expected_recipients&.map(&:downcase)&.to_set
51
+ @on_payment_success = on_payment_success
52
+ @can_offer = can_offer
44
53
  @intents = {}
45
54
  end
46
55
 
56
+ def can_offer?(request)
57
+ return true unless @can_offer
58
+
59
+ @can_offer.call(request)
60
+ end
61
+
47
62
  # Create a credential to satisfy the given challenge.
48
63
  #
49
64
  # mode: :pull (default) — return signed transaction for server to broadcast
@@ -251,7 +266,8 @@ module Mpp
251
266
  # Factory function to create a configured TempoMethod.
252
267
  def self.tempo(intents:, account: nil, fee_payer: nil, chain_id: nil, rpc_url: nil,
253
268
  root_account: nil, currency: nil, recipient: nil, decimals: 6, client_id: nil,
254
- expected_recipients: nil, fee_payer_allowed_fee_tokens: nil, relay: nil)
269
+ expected_recipients: nil, fee_payer_allowed_fee_tokens: nil, relay: nil,
270
+ on_payment_success: nil, can_offer: nil)
255
271
  rpc_url ||= chain_id ? Defaults.rpc_url_for_chain(chain_id) : Defaults::RPC_URL
256
272
  currency ||= Defaults.default_currency_for_chain(chain_id)
257
273
 
@@ -273,7 +289,9 @@ module Mpp
273
289
  client_id: client_id,
274
290
  expected_recipients: expected_recipients,
275
291
  fee_payer_allowed_fee_tokens: fee_payer_allowed_fee_tokens,
276
- relay: Relay.resolve_optional(relay)
292
+ relay: Relay.resolve_optional(relay),
293
+ on_payment_success: on_payment_success,
294
+ can_offer: can_offer
277
295
  )
278
296
 
279
297
  intents.each_value do |intent|
@@ -22,7 +22,9 @@ module Mpp
22
22
  attr_reader :name
23
23
  attr_accessor :rpc_url
24
24
 
25
- def initialize(chain_id: nil, rpc_url: nil, timeout: 30, store: nil, validate_sender: nil)
25
+ def initialize(chain_id: nil, rpc_url: nil, timeout: 30, store: Mpp::MemoryStore.new, validate_sender: nil)
26
+ raise ArgumentError, "store is required" if store.nil?
27
+
26
28
  @name = "charge"
27
29
  @rpc_url = rpc_url || (chain_id ? Defaults.rpc_url_for_chain(chain_id) : nil)
28
30
  @_method = nil
@@ -111,11 +113,9 @@ module Mpp
111
113
  # Validate the source before reserving the hash.
112
114
  source_address = parse_hash_credential_source(credential.source, request.method_details.chain_id)
113
115
 
114
- if @store
115
- store_key = "mpp:charge:#{payload.hash.downcase}"
116
- raise Mpp::VerificationError, "Transaction hash already used" unless @store.put_if_absent(store_key,
117
- payload.hash)
118
- end
116
+ store_key = "mpp:charge:#{payload.hash.downcase}"
117
+ raise Mpp::VerificationError, "Transaction hash already used" unless @store.put_if_absent(store_key,
118
+ payload.hash)
119
119
 
120
120
  rpc_url = get_rpc_url
121
121
  result = Rpc.call(rpc_url, "eth_getTransactionReceipt", [payload.hash])
@@ -158,19 +158,15 @@ module Mpp
158
158
  end
159
159
 
160
160
  rpc_url = get_rpc_url
161
- reserved_tx_hash = T.let(nil, T.nilable(String))
162
- store_key = T.let(nil, T.nilable(String))
163
- if @store
164
- reserved_tx_hash = raw_transaction_hash(raw_tx)
165
- store_key = "mpp:charge:#{reserved_tx_hash.downcase}"
166
- unless @store.put_if_absent(store_key, TRANSACTION_PENDING)
167
- raise Mpp::VerificationError, "Transaction hash already used" unless @store.get(store_key) == TRANSACTION_PENDING
161
+ reserved_tx_hash = raw_transaction_hash(raw_tx)
162
+ store_key = "mpp:charge:#{reserved_tx_hash.downcase}"
163
+ unless @store.put_if_absent(store_key, TRANSACTION_PENDING)
164
+ raise Mpp::VerificationError, "Transaction hash already used" unless @store.get(store_key) == TRANSACTION_PENDING
168
165
 
169
- receipt_data = fetch_transaction_receipt(rpc_url, reserved_tx_hash)
170
- verify_transaction_receipt!(receipt_data, request, credential: credential)
171
- @store.put(store_key, TRANSACTION_VERIFIED)
172
- return Mpp::Receipt.success(reserved_tx_hash)
173
- end
166
+ receipt_data = fetch_transaction_receipt(rpc_url, reserved_tx_hash)
167
+ verify_transaction_receipt!(receipt_data, request, credential: credential)
168
+ @store.put(store_key, TRANSACTION_VERIFIED)
169
+ return Mpp::Receipt.success(reserved_tx_hash)
174
170
  end
175
171
 
176
172
  # We pay the gas, so simulate the co-signed tx first and bail if it
@@ -178,7 +174,7 @@ module Mpp
178
174
  begin
179
175
  simulate_before_broadcast(simulate_payload, rpc_url) if simulate_payload
180
176
  rescue
181
- @store.delete(store_key) if @store && store_key
177
+ @store.delete(store_key)
182
178
  raise
183
179
  end
184
180
 
@@ -186,33 +182,36 @@ module Mpp
186
182
  begin
187
183
  tx_hash = Rpc.call(rpc_url, "eth_sendRawTransaction", [raw_tx])
188
184
  rescue => e
189
- if reserved_tx_hash && store_key && transaction_submission_may_have_succeeded?(e)
185
+ if transaction_submission_may_have_succeeded?(e)
190
186
  receipt_data = fetch_transaction_receipt(rpc_url, reserved_tx_hash)
191
187
  verify_transaction_receipt!(receipt_data, request, credential: credential)
192
- @store&.put(store_key, TRANSACTION_VERIFIED)
188
+ @store.put(store_key, TRANSACTION_VERIFIED)
193
189
  return Mpp::Receipt.success(reserved_tx_hash)
194
190
  end
195
191
 
196
- @store.delete(store_key) if @store && store_key
192
+ @store.delete(store_key)
197
193
  raise Mpp::VerificationError, "Transaction submission failed: #{e.message}"
198
194
  end
199
195
 
200
196
  unless tx_hash
201
- @store.delete(store_key) if @store && store_key
197
+ @store.delete(store_key)
202
198
  raise Mpp::VerificationError, "No transaction hash returned"
203
199
  end
204
200
 
205
- if reserved_tx_hash && tx_hash.downcase != reserved_tx_hash.downcase
206
- @store.delete(store_key) if @store && store_key
207
- raise Mpp::VerificationError, "Returned transaction hash does not match submitted transaction"
208
- end
201
+ # Tempo nodes hash canonical RLP, which can differ from keccak(raw
202
+ # bytes). Replay keys must follow the chain hash so a later hash
203
+ # credential for the same payment is rejected. Keep the raw-hash
204
+ # claim until verification finishes so a concurrent retry cannot
205
+ # rebroadcast the same payload.
206
+ raw_store_key = store_key
207
+ store_key, reserved_tx_hash = rebase_reservation_to_chain_hash(store_key, reserved_tx_hash, tx_hash)
209
208
 
210
- tx_hash = reserved_tx_hash || tx_hash
211
- receipt_data = fetch_transaction_receipt(rpc_url, tx_hash)
209
+ receipt_data = fetch_transaction_receipt(rpc_url, reserved_tx_hash)
212
210
  verify_transaction_receipt!(receipt_data, request, credential: credential)
213
- @store.put(store_key, TRANSACTION_VERIFIED) if @store && store_key
211
+ @store.put(store_key, TRANSACTION_VERIFIED)
212
+ @store.put(raw_store_key, TRANSACTION_VERIFIED) if raw_store_key != store_key
214
213
 
215
- Mpp::Receipt.success(tx_hash)
214
+ Mpp::Receipt.success(reserved_tx_hash)
216
215
  end
217
216
 
218
217
  def transaction_submission_may_have_succeeded?(error)
@@ -389,17 +388,32 @@ module Mpp
389
388
  end
390
389
 
391
390
  def raw_transaction_hash(raw_tx)
392
- Kernel.require "eth"
393
-
394
391
  hex = raw_tx.delete_prefix("0x")
395
392
  unless hex.match?(/\A[0-9a-fA-F]+\z/) && hex.length.even?
396
393
  raise Mpp::VerificationError, "Invalid transaction signature"
397
394
  end
398
395
 
399
- "0x#{Eth::Util.keccak256([hex].pack("H*")).unpack1("H*")}"
400
- rescue LoadError
401
- raise Mpp::VerificationError,
402
- "eth gem is required to compute transaction hash for transaction replay protection"
396
+ "0x#{Attribution.keccak256([hex].pack("H*")).unpack1("H*")}"
397
+ end
398
+
399
+ def normalize_tx_hash(tx_hash)
400
+ hex = tx_hash.to_s.downcase
401
+ hex.start_with?("0x") ? hex : "0x#{hex}"
402
+ end
403
+
404
+ def rebase_reservation_to_chain_hash(store_key, reserved_tx_hash, returned_tx_hash)
405
+ canonical = normalize_tx_hash(returned_tx_hash)
406
+ return [store_key, reserved_tx_hash] if canonical == reserved_tx_hash.downcase
407
+
408
+ canonical_key = "mpp:charge:#{canonical}"
409
+ unless @store.put_if_absent(canonical_key, TRANSACTION_PENDING)
410
+ unless @store.get(canonical_key) == TRANSACTION_PENDING
411
+ @store.put(store_key, TRANSACTION_VERIFIED)
412
+ raise Mpp::VerificationError, "Transaction hash already used"
413
+ end
414
+ end
415
+
416
+ [canonical_key, canonical]
403
417
  end
404
418
 
405
419
  def match_transfer_calldata(call_data_hex, request, challenge: nil)
@@ -455,10 +469,8 @@ module Mpp
455
469
  )
456
470
  raise Mpp::VerificationError, "Proof signature does not match source" unless valid
457
471
 
458
- if @store
459
- store_key = "mpp:proof:#{credential.challenge.id}"
460
- raise Mpp::VerificationError, "Proof credential has already been used" unless @store.put_if_absent(store_key, true)
461
- end
472
+ store_key = "mpp:proof:#{credential.challenge.id}"
473
+ raise Mpp::VerificationError, "Proof credential has already been used" unless @store.put_if_absent(store_key, true)
462
474
 
463
475
  Mpp::Receipt.success(credential.challenge.id)
464
476
  end
data/lib/mpp/parsing.rb CHANGED
@@ -101,6 +101,12 @@ module Mpp
101
101
  opaque_b64 = params["opaque"]
102
102
  opaque = (opaque_b64 && !opaque_b64.empty?) ? b64_decode(opaque_b64) : nil
103
103
 
104
+ header = begin
105
+ Mpp.advertised_credential_header(params["header"])
106
+ rescue ArgumentError => e
107
+ Kernel.raise Mpp::ParseError, e.message
108
+ end
109
+
104
110
  Mpp::Challenge.new(
105
111
  id: id,
106
112
  method: method,
@@ -111,7 +117,8 @@ module Mpp
111
117
  digest: params["digest"],
112
118
  expires: params["expires"],
113
119
  description: params["description"],
114
- opaque: opaque
120
+ opaque: opaque,
121
+ header: header
115
122
  )
116
123
  end
117
124
 
@@ -131,6 +138,9 @@ module Mpp
131
138
  parts << "digest=\"#{escape_quoted(challenge.digest)}\"" if challenge.digest
132
139
  parts << "expires=\"#{escape_quoted(challenge.expires)}\"" if challenge.expires
133
140
  parts << "description=\"#{escape_quoted(challenge.description)}\"" if challenge.description
141
+ if challenge.header
142
+ parts << "header=\"#{escape_quoted(challenge.header)}\""
143
+ end
134
144
  if challenge.opaque
135
145
  opaque_b64 = b64_encode(challenge.opaque)
136
146
  parts << "opaque=\"#{opaque_b64}\""
@@ -158,6 +168,12 @@ module Mpp
158
168
  method = challenge_data["method"]
159
169
  validate_payment_method_id(method)
160
170
 
171
+ header = begin
172
+ Mpp.advertised_credential_header(challenge_data["header"]&.to_s)
173
+ rescue ArgumentError => e
174
+ Kernel.raise Mpp::ParseError, e.message
175
+ end
176
+
161
177
  echo = Mpp::ChallengeEcho.new(
162
178
  id: challenge_data["id"].to_s,
163
179
  realm: (challenge_data["realm"] || "").to_s,
@@ -166,7 +182,8 @@ module Mpp
166
182
  request: (challenge_data["request"] || "").to_s,
167
183
  expires: challenge_data["expires"]&.to_s,
168
184
  digest: challenge_data["digest"]&.to_s,
169
- opaque: challenge_data["opaque"]&.to_s
185
+ opaque: challenge_data["opaque"]&.to_s,
186
+ header: header
170
187
  )
171
188
 
172
189
  Mpp::Credential.new(
@@ -188,6 +205,7 @@ module Mpp
188
205
  }
189
206
  challenge_dict["expires"] = credential.challenge.expires if credential.challenge.expires
190
207
  challenge_dict["digest"] = credential.challenge.digest if credential.challenge.digest
208
+ challenge_dict["header"] = credential.challenge.header if credential.challenge.header
191
209
  challenge_dict["opaque"] = credential.challenge.opaque if credential.challenge.opaque
192
210
 
193
211
  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
@@ -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,
@@ -330,10 +337,15 @@ module Mpp
330
337
  ).returns(ComposedResult)
331
338
  end
332
339
  def merge_challenges(body:, url:, accept_payment:, http_method:)
333
- pairs = @offers.map do |offer|
340
+ pairs = @offers.filter_map do |offer|
334
341
  challenge = offer.challenge(body: body, url: url, http_method: http_method)
342
+ next if challenge.nil?
343
+
335
344
  [offer, challenge]
336
345
  end
346
+ if pairs.empty?
347
+ Kernel.raise ArgumentError, "No payment offers are available for this request"
348
+ end
337
349
 
338
350
  ranked = AcceptPayment.apply(pairs.map { |_offer, challenge| challenge }, accept_payment)
339
351
  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,
@@ -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,20 @@ module Mpp
22
23
  request
23
24
  end
24
25
  end
26
+
27
+ # Check whether a method should be advertised for a canonical request.
28
+ # This only governs composing new 402 offers, never credential redemption.
29
+ sig { params(method: T.untyped, request: T::Hash[String, T.untyped]).returns(T::Boolean) }
30
+ def can_offer?(method, request)
31
+ return true unless method.respond_to?(:can_offer?)
32
+
33
+ available = method.can_offer?(request)
34
+ unless available == true || available == false
35
+ Kernel.raise ArgumentError, "can_offer? must return true or false"
36
+ end
37
+
38
+ available
39
+ end
25
40
  end
26
41
  end
27
42
  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"