mpp-rb 0.1.6 → 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 +85 -0
- data/lib/mpp/extensions/mcp/verify.rb +1 -1
- data/lib/mpp/methods/evm/charge_intent.rb +38 -17
- data/lib/mpp/methods/stripe/analytics_metadata.rb +24 -0
- data/lib/mpp/methods/stripe/charge_intent.rb +29 -5
- data/lib/mpp/methods/stripe/crypto_payment_recorder.rb +46 -9
- data/lib/mpp/methods/stripe/machine_payments.rb +6 -8
- 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 +8 -0
- data/lib/mpp/methods/stripe.rb +3 -0
- data/lib/mpp/methods/tempo/client_method.rb +40 -12
- data/lib/mpp/methods/tempo/defaults.rb +51 -0
- data/lib/mpp/methods/tempo/intents.rb +147 -89
- data/lib/mpp/methods/tempo/relay.rb +33 -31
- data/lib/mpp/methods/tempo/schemas.rb +3 -4
- data/lib/mpp/parsing.rb +9 -2
- data/lib/mpp/server/compose.rb +10 -5
- data/lib/mpp/server/intent.rb +6 -1
- data/lib/mpp/server/intent_lifecycle.rb +41 -0
- data/lib/mpp/server/method.rb +26 -0
- data/lib/mpp/server/mpp_handler.rb +21 -9
- data/lib/mpp/server/verify.rb +1 -1
- 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 +1 -0
- metadata +6 -1
|
@@ -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
|
data/lib/mpp/server/compose.rb
CHANGED
|
@@ -140,7 +140,7 @@ module Mpp
|
|
|
140
140
|
|
|
141
141
|
sig { params(handler: T.untyped, entries: T::Array[T.untyped]).returns(ComposedHandler) }
|
|
142
142
|
def self.from_entries(handler, entries)
|
|
143
|
-
new(handler: handler, offers: entries.
|
|
143
|
+
new(handler: handler, offers: entries.flat_map { |entry| offers_from_entry(handler, entry) })
|
|
144
144
|
end
|
|
145
145
|
|
|
146
146
|
# Flatten nested compositions into a single handler.
|
|
@@ -200,8 +200,9 @@ module Mpp
|
|
|
200
200
|
|
|
201
201
|
private
|
|
202
202
|
|
|
203
|
-
|
|
204
|
-
|
|
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)
|
|
205
206
|
unless entry.is_a?(Array) && entry.length == 2
|
|
206
207
|
Kernel.raise ArgumentError, "compose() entries must be [method, options] tuples"
|
|
207
208
|
end
|
|
@@ -212,9 +213,13 @@ module Mpp
|
|
|
212
213
|
Kernel.raise ArgumentError, "compose() options must be a Hash"
|
|
213
214
|
end
|
|
214
215
|
|
|
215
|
-
|
|
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)) }
|
|
216
221
|
end
|
|
217
|
-
private_class_method :
|
|
222
|
+
private_class_method :offers_from_entry
|
|
218
223
|
|
|
219
224
|
sig do
|
|
220
225
|
params(
|
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
|
@@ -24,6 +24,20 @@ module Mpp
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
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
|
+
|
|
27
41
|
# Check whether a method should be advertised for a canonical request.
|
|
28
42
|
# This only governs composing new 402 offers, never credential redemption.
|
|
29
43
|
sig { params(method: T.untyped, request: T::Hash[String, T.untyped]).returns(T::Boolean) }
|
|
@@ -37,6 +51,18 @@ module Mpp
|
|
|
37
51
|
|
|
38
52
|
available
|
|
39
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
|
|
40
66
|
end
|
|
41
67
|
end
|
|
42
68
|
end
|
|
@@ -130,9 +130,10 @@ module Mpp
|
|
|
130
130
|
|
|
131
131
|
# Handle a charge intent.
|
|
132
132
|
#
|
|
133
|
-
# With a single registered charge method
|
|
134
|
-
# [Credential, Receipt]. With multiple charge methods
|
|
135
|
-
#
|
|
133
|
+
# With a single registered charge method offering a single currency this
|
|
134
|
+
# returns Challenge or [Credential, Receipt]. With multiple charge methods,
|
|
135
|
+
# or a method accepting several currencies, it implicitly composes one
|
|
136
|
+
# offer per method and currency and returns a ComposedResult.
|
|
136
137
|
sig { params(authorization: T.nilable(String), amount: String, kwargs: T.untyped).returns(T.untyped) }
|
|
137
138
|
def charge(authorization, amount, **kwargs)
|
|
138
139
|
charge_methods = @methods.select { |candidate| candidate.intents.key?("charge") }
|
|
@@ -142,7 +143,7 @@ module Mpp
|
|
|
142
143
|
offer_opts = kwargs.except(*REQUEST_OPTION_KEYS)
|
|
143
144
|
payment = payment_credential_value(authorization, request_opts[:payment_authorization])
|
|
144
145
|
|
|
145
|
-
if charge_methods.length == 1
|
|
146
|
+
if charge_methods.length == 1 && MethodHelper.offer_currencies(charge_methods.first, offer_opts).nil?
|
|
146
147
|
return charge_one(
|
|
147
148
|
charge_methods.first,
|
|
148
149
|
payment,
|
|
@@ -176,6 +177,7 @@ module Mpp
|
|
|
176
177
|
url = kwargs[:url]
|
|
177
178
|
payment_signature = kwargs[:payment_signature]
|
|
178
179
|
offer_opts = kwargs.except(*REQUEST_OPTION_KEYS)
|
|
180
|
+
intent, offer_opts = Mpp::Server::MethodHelper.prepare_intent(method, intent, offer_opts)
|
|
179
181
|
request = build_charge_request(method, amount, **offer_opts)
|
|
180
182
|
return nil if request_guard && !request_guard.call(request)
|
|
181
183
|
|
|
@@ -215,7 +217,6 @@ module Mpp
|
|
|
215
217
|
currency = kwargs[:currency]
|
|
216
218
|
recipient = kwargs[:recipient]
|
|
217
219
|
external_id = kwargs[:external_id]
|
|
218
|
-
memo = kwargs[:memo]
|
|
219
220
|
fee_payer = if kwargs.key?(:fee_payer)
|
|
220
221
|
kwargs[:fee_payer]
|
|
221
222
|
else
|
|
@@ -225,7 +226,7 @@ module Mpp
|
|
|
225
226
|
extra = kwargs[:extra]
|
|
226
227
|
mppx_scope = kwargs[:mppx_scope]
|
|
227
228
|
|
|
228
|
-
resolved_currency = currency || (method
|
|
229
|
+
resolved_currency = currency || primary_currency(method)
|
|
229
230
|
resolved_recipient = recipient || (method.respond_to?(:recipient) ? method.recipient : nil)
|
|
230
231
|
raise ArgumentError, "currency must be set on the method or passed to charge()" unless resolved_currency
|
|
231
232
|
raise ArgumentError, "recipient must be set on the method or passed to charge()" unless resolved_recipient
|
|
@@ -256,10 +257,9 @@ module Mpp
|
|
|
256
257
|
resolved_chain_id = chain_id
|
|
257
258
|
resolved_chain_id ||= method.chain_id if method.respond_to?(:chain_id)
|
|
258
259
|
|
|
259
|
-
if
|
|
260
|
+
if fee_payer || !resolved_chain_id.nil?
|
|
260
261
|
method_details = {}
|
|
261
262
|
method_details["chainId"] = resolved_chain_id unless resolved_chain_id.nil?
|
|
262
|
-
method_details["memo"] = memo if memo
|
|
263
263
|
method_details["feePayer"] = true if fee_payer
|
|
264
264
|
request["methodDetails"] = method_details
|
|
265
265
|
end
|
|
@@ -296,6 +296,15 @@ module Mpp
|
|
|
296
296
|
list
|
|
297
297
|
end
|
|
298
298
|
|
|
299
|
+
# A method's first accepted currency, else its singular currency.
|
|
300
|
+
sig { params(method: T.untyped).returns(T.untyped) }
|
|
301
|
+
def primary_currency(method)
|
|
302
|
+
currencies = method.respond_to?(:currencies) ? method.currencies : nil
|
|
303
|
+
return currencies.first if currencies.is_a?(Array) && !currencies.empty?
|
|
304
|
+
|
|
305
|
+
method.respond_to?(:currency) ? method.currency : nil
|
|
306
|
+
end
|
|
307
|
+
|
|
299
308
|
sig { params(key: String).returns(T.untyped) }
|
|
300
309
|
def resolve_method_key(key)
|
|
301
310
|
name, intent = key.split("/", 2)
|
|
@@ -346,7 +355,7 @@ module Mpp
|
|
|
346
355
|
body: body,
|
|
347
356
|
http_method: http_method
|
|
348
357
|
)
|
|
349
|
-
receipt =
|
|
358
|
+
receipt = IntentLifecycle.call(intent, credential, challenge.request)
|
|
350
359
|
rescue => error
|
|
351
360
|
if @events.has_handlers?(Mpp::Events::PAYMENT_FAILED)
|
|
352
361
|
Verify.emit_payment_failed(
|
|
@@ -359,6 +368,9 @@ module Mpp
|
|
|
359
368
|
retry_challenge: challenge
|
|
360
369
|
)
|
|
361
370
|
end
|
|
371
|
+
if error.is_a?(Mpp::PaymentError) && error.status != 402
|
|
372
|
+
raise
|
|
373
|
+
end
|
|
362
374
|
if error.is_a?(Mpp::ParseError) || error.is_a?(Mpp::VerificationError) || error.is_a?(Mpp::PaymentError)
|
|
363
375
|
if @events.has_handlers?(Mpp::Events::CHALLENGE_CREATED)
|
|
364
376
|
Verify.emit_challenge_created(
|
data/lib/mpp/server/verify.rb
CHANGED
data/lib/mpp/server.rb
CHANGED
|
@@ -6,6 +6,7 @@ module Mpp
|
|
|
6
6
|
autoload :Defaults, "mpp/server/defaults"
|
|
7
7
|
autoload :Intent, "mpp/server/intent"
|
|
8
8
|
autoload :FunctionalIntent, "mpp/server/intent"
|
|
9
|
+
autoload :IntentLifecycle, "mpp/server/intent_lifecycle"
|
|
9
10
|
autoload :Method, "mpp/server/method"
|
|
10
11
|
autoload :Verify, "mpp/server/verify"
|
|
11
12
|
autoload :MppHandler, "mpp/server/mpp_handler"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Mpp
|
|
5
|
+
# Non-mutating validation result, matching mppx's Method.Validation fields.
|
|
6
|
+
# Details are method-specific, not a receipt or a guarantee of settlement.
|
|
7
|
+
# Challenge issuance and route binding are checked by the server, not by
|
|
8
|
+
# constructing this record or calling a rail's validate method directly.
|
|
9
|
+
Validation = Data.define(:challenge, :credential, :intent, :method, :request, :details, :source) do
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig do
|
|
13
|
+
params(
|
|
14
|
+
challenge: Mpp::ChallengeEcho,
|
|
15
|
+
credential: Mpp::Credential,
|
|
16
|
+
intent: String,
|
|
17
|
+
method: String,
|
|
18
|
+
request: T::Hash[String, T.untyped],
|
|
19
|
+
details: T.untyped,
|
|
20
|
+
source: T.nilable(String)
|
|
21
|
+
).void
|
|
22
|
+
end
|
|
23
|
+
def initialize(challenge:, credential:, intent:, method:, request:, details:, source: nil)
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/mpp/version.rb
CHANGED
data/lib/mpp.rb
CHANGED
|
@@ -18,6 +18,7 @@ module Mpp
|
|
|
18
18
|
autoload :ChallengeEcho, "mpp/challenge_echo"
|
|
19
19
|
autoload :Credential, "mpp/credential"
|
|
20
20
|
autoload :Receipt, "mpp/receipt"
|
|
21
|
+
autoload :Validation, "mpp/validation"
|
|
21
22
|
autoload :Parsing, "mpp/parsing"
|
|
22
23
|
autoload :Json, "mpp/json"
|
|
23
24
|
autoload :BodyDigest, "mpp/body_digest"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mpp-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stripe
|
|
@@ -87,11 +87,14 @@ files:
|
|
|
87
87
|
- lib/mpp/methods/evm/charge_intent.rb
|
|
88
88
|
- lib/mpp/methods/evm/evm_method.rb
|
|
89
89
|
- lib/mpp/methods/stripe.rb
|
|
90
|
+
- lib/mpp/methods/stripe/analytics_metadata.rb
|
|
90
91
|
- lib/mpp/methods/stripe/charge_intent.rb
|
|
91
92
|
- lib/mpp/methods/stripe/client_method.rb
|
|
92
93
|
- lib/mpp/methods/stripe/crypto_payment_recorder.rb
|
|
93
94
|
- lib/mpp/methods/stripe/defaults.rb
|
|
94
95
|
- lib/mpp/methods/stripe/machine_payments.rb
|
|
96
|
+
- lib/mpp/methods/stripe/payment_intent_method.rb
|
|
97
|
+
- lib/mpp/methods/stripe/payment_intent_options.rb
|
|
95
98
|
- lib/mpp/methods/stripe/stripe_method.rb
|
|
96
99
|
- lib/mpp/methods/tempo.rb
|
|
97
100
|
- lib/mpp/methods/tempo/account.rb
|
|
@@ -117,6 +120,7 @@ files:
|
|
|
117
120
|
- lib/mpp/server/decorator.rb
|
|
118
121
|
- lib/mpp/server/defaults.rb
|
|
119
122
|
- lib/mpp/server/intent.rb
|
|
123
|
+
- lib/mpp/server/intent_lifecycle.rb
|
|
120
124
|
- lib/mpp/server/method.rb
|
|
121
125
|
- lib/mpp/server/middleware.rb
|
|
122
126
|
- lib/mpp/server/mpp_handler.rb
|
|
@@ -124,6 +128,7 @@ files:
|
|
|
124
128
|
- lib/mpp/server/verify.rb
|
|
125
129
|
- lib/mpp/store.rb
|
|
126
130
|
- lib/mpp/units.rb
|
|
131
|
+
- lib/mpp/validation.rb
|
|
127
132
|
- lib/mpp/version.rb
|
|
128
133
|
- lib/mpp/x402.rb
|
|
129
134
|
- lib/mpp/x402/facilitator.rb
|