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.
- checksums.yaml +4 -4
- data/README.md +150 -0
- data/lib/mpp/challenge.rb +31 -8
- data/lib/mpp/challenge_echo.rb +14 -3
- data/lib/mpp/challenge_id.rb +30 -6
- data/lib/mpp/client/transport.rb +1 -1
- data/lib/mpp/extensions/mcp/verify.rb +1 -1
- data/lib/mpp/methods/evm/charge_intent.rb +38 -17
- data/lib/mpp/methods/evm/evm_method.rb +18 -2
- data/lib/mpp/methods/evm.rb +5 -2
- data/lib/mpp/methods/stripe/analytics_metadata.rb +24 -0
- data/lib/mpp/methods/stripe/charge_intent.rb +34 -11
- data/lib/mpp/methods/stripe/crypto_payment_recorder.rb +92 -0
- data/lib/mpp/methods/stripe/defaults.rb +1 -0
- data/lib/mpp/methods/stripe/machine_payments.rb +128 -0
- data/lib/mpp/methods/stripe/payment_intent_method.rb +103 -0
- data/lib/mpp/methods/stripe/payment_intent_options.rb +81 -0
- data/lib/mpp/methods/stripe/stripe_method.rb +30 -3
- data/lib/mpp/methods/stripe.rb +26 -0
- data/lib/mpp/methods/tempo/client_method.rb +58 -12
- data/lib/mpp/methods/tempo/defaults.rb +51 -0
- data/lib/mpp/methods/tempo/intents.rb +195 -125
- data/lib/mpp/methods/tempo/relay.rb +33 -31
- data/lib/mpp/methods/tempo/schemas.rb +3 -4
- data/lib/mpp/parsing.rb +29 -4
- data/lib/mpp/server/compose.rb +32 -15
- data/lib/mpp/server/decorator.rb +3 -1
- data/lib/mpp/server/intent.rb +6 -1
- data/lib/mpp/server/intent_lifecycle.rb +41 -0
- data/lib/mpp/server/method.rb +41 -0
- data/lib/mpp/server/middleware.rb +16 -2
- data/lib/mpp/server/mpp_handler.rb +87 -21
- data/lib/mpp/server/verify.rb +25 -15
- data/lib/mpp/server.rb +1 -0
- data/lib/mpp/validation.rb +27 -0
- data/lib/mpp/version.rb +1 -1
- data/lib/mpp.rb +14 -3
- 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
|
|
11
|
-
def initialize(chain_id: 4217, fee_payer: false, fee_payer_url: 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
|
-
|
|
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[
|
|
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 = {
|
data/lib/mpp/server/compose.rb
CHANGED
|
@@ -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:
|
|
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.
|
|
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:
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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 :
|
|
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.
|
|
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]] }
|
data/lib/mpp/server/decorator.rb
CHANGED
|
@@ -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:
|
|
45
|
+
vary: vary
|
|
44
46
|
)
|
|
45
47
|
{
|
|
46
48
|
"_mpp_challenge" => true,
|
data/lib/mpp/server/intent.rb
CHANGED
|
@@ -6,7 +6,11 @@ module Mpp
|
|
|
6
6
|
extend T::Sig
|
|
7
7
|
|
|
8
8
|
# Intent interface (duck type):
|
|
9
|
-
# name
|
|
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
|
data/lib/mpp/server/method.rb
CHANGED
|
@@ -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 = [
|
|
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"
|