mpp-rb 0.1.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 (54) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +133 -0
  4. data/lib/mpp/body_digest.rb +37 -0
  5. data/lib/mpp/challenge.rb +115 -0
  6. data/lib/mpp/challenge_echo.rb +19 -0
  7. data/lib/mpp/challenge_id.rb +54 -0
  8. data/lib/mpp/client/transport.rb +137 -0
  9. data/lib/mpp/client.rb +9 -0
  10. data/lib/mpp/credential.rb +20 -0
  11. data/lib/mpp/errors.rb +190 -0
  12. data/lib/mpp/expires.rb +60 -0
  13. data/lib/mpp/extensions/mcp/capabilities.rb +23 -0
  14. data/lib/mpp/extensions/mcp/constants.rb +17 -0
  15. data/lib/mpp/extensions/mcp/decorator.rb +44 -0
  16. data/lib/mpp/extensions/mcp/errors.rb +110 -0
  17. data/lib/mpp/extensions/mcp/types.rb +205 -0
  18. data/lib/mpp/extensions/mcp/verify.rb +152 -0
  19. data/lib/mpp/extensions/mcp.rb +16 -0
  20. data/lib/mpp/json.rb +32 -0
  21. data/lib/mpp/methods/stripe/charge_intent.rb +90 -0
  22. data/lib/mpp/methods/stripe/client_method.rb +42 -0
  23. data/lib/mpp/methods/stripe/defaults.rb +14 -0
  24. data/lib/mpp/methods/stripe/stripe_method.rb +63 -0
  25. data/lib/mpp/methods/stripe.rb +14 -0
  26. data/lib/mpp/methods/tempo/account.rb +52 -0
  27. data/lib/mpp/methods/tempo/attribution.rb +112 -0
  28. data/lib/mpp/methods/tempo/client_method.rb +259 -0
  29. data/lib/mpp/methods/tempo/defaults.rb +77 -0
  30. data/lib/mpp/methods/tempo/fee_payer_envelope.rb +74 -0
  31. data/lib/mpp/methods/tempo/intents.rb +377 -0
  32. data/lib/mpp/methods/tempo/keychain.rb +31 -0
  33. data/lib/mpp/methods/tempo/proof.rb +127 -0
  34. data/lib/mpp/methods/tempo/rpc.rb +60 -0
  35. data/lib/mpp/methods/tempo/schemas.rb +96 -0
  36. data/lib/mpp/methods/tempo/transaction.rb +144 -0
  37. data/lib/mpp/methods/tempo.rb +22 -0
  38. data/lib/mpp/parsing.rb +252 -0
  39. data/lib/mpp/receipt.rb +31 -0
  40. data/lib/mpp/secure_compare.rb +25 -0
  41. data/lib/mpp/server/decorator.rb +32 -0
  42. data/lib/mpp/server/defaults.rb +45 -0
  43. data/lib/mpp/server/intent.rb +40 -0
  44. data/lib/mpp/server/method.rb +27 -0
  45. data/lib/mpp/server/middleware.rb +51 -0
  46. data/lib/mpp/server/mpp_handler.rb +97 -0
  47. data/lib/mpp/server/verify.rb +129 -0
  48. data/lib/mpp/server.rb +15 -0
  49. data/lib/mpp/store.rb +49 -0
  50. data/lib/mpp/units.rb +57 -0
  51. data/lib/mpp/version.rb +6 -0
  52. data/lib/mpp-rb.rb +3 -0
  53. data/lib/mpp.rb +68 -0
  54. metadata +111 -0
@@ -0,0 +1,97 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "method"
5
+
6
+ module Mpp
7
+ module Server
8
+ DEFAULT_DECIMALS = 6
9
+
10
+ class MppHandler
11
+ extend T::Sig
12
+
13
+ sig { returns(T.untyped) }
14
+ attr_reader :method
15
+
16
+ sig { returns(String) }
17
+ attr_reader :realm
18
+
19
+ sig { returns(String) }
20
+ attr_reader :secret_key
21
+
22
+ sig { returns(T::Hash[String, T.untyped]) }
23
+ attr_reader :defaults
24
+
25
+ sig { params(method: T.untyped, realm: String, secret_key: String, defaults: T.nilable(T::Hash[String, T.untyped])).void }
26
+ def initialize(method:, realm:, secret_key:, defaults: nil)
27
+ @method = T.let(method, T.untyped)
28
+ @realm = T.let(realm, String)
29
+ @secret_key = T.let(secret_key, String)
30
+ @defaults = T.let(defaults || {}, T::Hash[String, T.untyped])
31
+ end
32
+
33
+ # Create with auto-detected realm and secret_key.
34
+ sig { params(method: T.untyped, realm: T.untyped, secret_key: T.untyped).returns(T.attached_class) }
35
+ def self.create(method:, realm: nil, secret_key: nil)
36
+ new(
37
+ method: method,
38
+ realm: realm || Defaults.detect_realm,
39
+ secret_key: secret_key || Defaults.detect_secret_key
40
+ )
41
+ end
42
+
43
+ # Handle a charge intent.
44
+ sig { params(authorization: T.nilable(String), amount: String, currency: T.nilable(String), recipient: T.nilable(String), expires: T.nilable(String), description: T.nilable(String), memo: T.nilable(String), fee_payer: T::Boolean, chain_id: T.nilable(Integer), extra: T.nilable(T::Hash[String, String])).returns(T.untyped) }
45
+ def charge(authorization, amount, currency: nil, recipient: nil, expires: nil,
46
+ description: nil, memo: nil, fee_payer: false, chain_id: nil, extra: nil)
47
+ intent = @method.intents["charge"]
48
+ raise ArgumentError, "Method #{@method.name} does not support charge intent" unless intent
49
+
50
+ resolved_currency = currency || (@method.respond_to?(:currency) ? @method.currency : nil)
51
+ resolved_recipient = recipient || (@method.respond_to?(:recipient) ? @method.recipient : nil)
52
+ raise ArgumentError, "currency must be set on the method or passed to charge()" unless resolved_currency
53
+ raise ArgumentError, "recipient must be set on the method or passed to charge()" unless resolved_recipient
54
+
55
+ decimals = @method.respond_to?(:decimals) ? @method.decimals : DEFAULT_DECIMALS
56
+ base_amount = Mpp::Units.parse_units(amount, decimals).to_s
57
+
58
+ request = {
59
+ "amount" => base_amount,
60
+ "currency" => resolved_currency,
61
+ "recipient" => resolved_recipient
62
+ }
63
+
64
+ if extra
65
+ extra.each do |k, v|
66
+ raise ArgumentError, "extra must be a dict[str, str]" unless k.is_a?(String) && v.is_a?(String)
67
+ end
68
+ request["extra"] = extra
69
+ end
70
+
71
+ resolved_chain_id = chain_id
72
+ resolved_chain_id ||= @method.chain_id if @method.respond_to?(:chain_id)
73
+
74
+ if memo || fee_payer || !resolved_chain_id.nil?
75
+ method_details = {}
76
+ method_details["chainId"] = resolved_chain_id unless resolved_chain_id.nil?
77
+ method_details["memo"] = memo if memo
78
+ method_details["feePayer"] = true if fee_payer
79
+ request["methodDetails"] = method_details
80
+ end
81
+
82
+ request = Mpp::Server::MethodHelper.transform_request(@method, request, nil)
83
+
84
+ Verify.verify_or_challenge(
85
+ authorization: authorization,
86
+ intent: intent,
87
+ request: request,
88
+ realm: @realm,
89
+ secret_key: @secret_key,
90
+ method: @method.name,
91
+ description: description,
92
+ expires: expires
93
+ )
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,129 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "time"
5
+
6
+ module Mpp
7
+ module Server
8
+ module Verify
9
+ extend T::Sig
10
+
11
+ DEFAULT_EXPIRES_MINUTES = 5
12
+
13
+ module_function
14
+
15
+ # Verify a payment credential or generate a new challenge.
16
+ #
17
+ # Returns Challenge (payment required) or [Credential, Receipt] (verified).
18
+ sig { params(authorization: T.nilable(String), intent: T.untyped, request: T::Hash[String, T.untyped], realm: String, secret_key: String, method: T.nilable(String), description: T.nilable(String), meta: T.nilable(T::Hash[String, T.untyped]), expires: T.nilable(String)).returns(T.untyped) }
19
+ def verify_or_challenge(authorization:, intent:, request:, realm:, secret_key:,
20
+ method: nil, description: nil, meta: nil, expires: nil)
21
+ method_name = method || "tempo"
22
+ request = Mpp::Units.transform_units(request)
23
+
24
+ new_challenge = Kernel.lambda {
25
+ create_challenge(method_name, intent.name, request, realm, secret_key, description, meta, expires)
26
+ }
27
+
28
+ return new_challenge.call if authorization.nil?
29
+
30
+ payment_scheme = extract_payment_scheme(authorization)
31
+ return new_challenge.call if payment_scheme.nil?
32
+
33
+ begin
34
+ credential = Mpp::Credential.from_authorization(payment_scheme)
35
+ rescue Mpp::ParseError
36
+ return new_challenge.call
37
+ end
38
+
39
+ # Stateless challenge verification
40
+ echo = credential.challenge
41
+ begin
42
+ echo_request = echo.request.empty? ? {} : Mpp::Parsing.b64_decode(echo.request)
43
+ echo_opaque = (echo.opaque && !T.must(echo.opaque).empty?) ? Mpp::Parsing.b64_decode(echo.opaque) : nil
44
+ rescue Mpp::ParseError
45
+ return new_challenge.call
46
+ end
47
+
48
+ expected_id = Mpp.generate_challenge_id(
49
+ secret_key: secret_key,
50
+ realm: echo.realm,
51
+ method: echo.method,
52
+ intent: echo.intent,
53
+ request: echo_request,
54
+ expires: echo.expires,
55
+ digest: echo.digest,
56
+ opaque: echo_opaque
57
+ )
58
+ return new_challenge.call unless Mpp.secure_compare(echo.id, expected_id)
59
+
60
+ # Assert echoed fields match server's values
61
+ return new_challenge.call unless echo.realm == realm && echo.method == method_name && echo.intent == intent.name
62
+
63
+ # Assert echoed request matches server's current request
64
+ return new_challenge.call unless echo_request == request
65
+
66
+ return new_challenge.call unless echo_opaque == meta
67
+
68
+ # Reject expired challenges as defense-in-depth
69
+ if echo.expires
70
+ begin
71
+ expires_dt = Time.iso8601(T.must(echo.expires).gsub("Z", "+00:00"))
72
+ return new_challenge.call if expires_dt < Time.now.utc
73
+ rescue ArgumentError
74
+ # If we can't parse, continue to stricter check below
75
+ end
76
+ end
77
+
78
+ # Verify echoed request parameters match endpoint's expected request
79
+ request.each do |key, value|
80
+ return new_challenge.call unless echo_request[key] == value
81
+ end
82
+
83
+ # Enforce challenge expiry - fail closed
84
+ return new_challenge.call unless echo.expires
85
+
86
+ begin
87
+ expires_dt = Time.iso8601(T.must(echo.expires).gsub("Z", "+00:00"))
88
+ rescue ArgumentError
89
+ return new_challenge.call
90
+ end
91
+ return new_challenge.call if expires_dt < Time.now.utc
92
+
93
+ receipt = intent.verify(credential, request)
94
+ [credential, receipt]
95
+ end
96
+
97
+ sig { params(method: String, intent_name: String, request: T::Hash[String, T.untyped], realm: String, secret_key: String, description: T.nilable(String), meta: T.nilable(T::Hash[String, T.untyped]), expires: T.nilable(String)).returns(Mpp::Challenge) }
98
+ def create_challenge(method, intent_name, request, realm, secret_key,
99
+ description = nil, meta = nil, expires = nil)
100
+ expires = nil if expires && !expires.is_a?(String)
101
+
102
+ if expires.nil?
103
+ expires_dt = Time.now.utc + (DEFAULT_EXPIRES_MINUTES * 60)
104
+ expires = expires_dt.strftime("%Y-%m-%dT%H:%M:%S.%LZ")
105
+ end
106
+
107
+ Mpp::Challenge.create(
108
+ secret_key: secret_key,
109
+ realm: realm,
110
+ method: method,
111
+ intent: intent_name,
112
+ request: request,
113
+ expires: expires,
114
+ description: description,
115
+ meta: meta
116
+ )
117
+ end
118
+
119
+ sig { params(header: String).returns(T.nilable(String)) }
120
+ def extract_payment_scheme(header)
121
+ header.split(",").each do |scheme|
122
+ scheme = scheme.strip
123
+ return scheme if scheme.downcase.start_with?("payment ")
124
+ end
125
+ nil
126
+ end
127
+ end
128
+ end
129
+ end
data/lib/mpp/server.rb ADDED
@@ -0,0 +1,15 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Mpp
5
+ module Server
6
+ autoload :Defaults, "mpp/server/defaults"
7
+ autoload :Intent, "mpp/server/intent"
8
+ autoload :FunctionalIntent, "mpp/server/intent"
9
+ autoload :Method, "mpp/server/method"
10
+ autoload :Verify, "mpp/server/verify"
11
+ autoload :MppHandler, "mpp/server/mpp_handler"
12
+ autoload :Decorator, "mpp/server/decorator"
13
+ autoload :Middleware, "mpp/server/middleware"
14
+ end
15
+ end
data/lib/mpp/store.rb ADDED
@@ -0,0 +1,49 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Mpp
5
+ # In-memory key-value store for development/testing.
6
+ # Production implementations should use Redis, DynamoDB, etc.
7
+ #
8
+ # Duck type interface (Store):
9
+ # get(key) -> value or nil
10
+ # put(key, value) -> void
11
+ # delete(key) -> void
12
+ # put_if_absent(key, value) -> bool
13
+ class MemoryStore
14
+ extend T::Sig
15
+
16
+ sig { void }
17
+ def initialize
18
+ @data = T.let({}, T::Hash[T.untyped, T.untyped])
19
+ @mutex = T.let(Mutex.new, Thread::Mutex)
20
+ end
21
+
22
+ sig { params(key: String).returns(T.untyped) }
23
+ def get(key)
24
+ @mutex.synchronize { @data[key] }
25
+ end
26
+
27
+ sig { params(key: String, value: T.untyped).returns(T.untyped) }
28
+ def put(key, value)
29
+ @mutex.synchronize { @data[key] = value }
30
+ end
31
+
32
+ sig { params(key: String).returns(T.untyped) }
33
+ def delete(key)
34
+ @mutex.synchronize { @data.delete(key) }
35
+ end
36
+
37
+ # Store value under key only if key does not already exist.
38
+ # Returns true if the key was new, false if it already existed.
39
+ sig { params(key: String, value: T.untyped).returns(T::Boolean) }
40
+ def put_if_absent(key, value)
41
+ @mutex.synchronize do
42
+ return false if @data.key?(key)
43
+
44
+ @data[key] = value
45
+ true
46
+ end
47
+ end
48
+ end
49
+ end
data/lib/mpp/units.rb ADDED
@@ -0,0 +1,57 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "bigdecimal"
5
+
6
+ module Mpp
7
+ module Units
8
+ extend T::Sig
9
+
10
+ module_function
11
+
12
+ # Convert a human-readable decimal string to base units.
13
+ # e.g. parse_units("1.5", 6) => 1500000
14
+ sig { params(value: T.untyped, decimals: T.any(Integer, Float, Rational, BigDecimal)).returns(Integer) }
15
+ def parse_units(value, decimals)
16
+ Kernel.raise ArgumentError, "amount is required" unless value.is_a?(String) && !value.strip.empty?
17
+
18
+ stripped = value.strip
19
+ d = Kernel.BigDecimal(stripped)
20
+ rescue ArgumentError
21
+ Kernel.raise ArgumentError, "Invalid amount: #{value.inspect}"
22
+ else
23
+ Kernel.raise ArgumentError, "amount must be finite" unless d.finite?
24
+ Kernel.raise ArgumentError, "amount must be non-negative" if d.negative?
25
+
26
+ result = d * (Kernel.BigDecimal(10)**decimals)
27
+ int_result = result.to_i
28
+
29
+ unless T.unsafe(result) == int_result
30
+ Kernel.raise ArgumentError,
31
+ "Amount #{value.inspect} with #{decimals} decimals produces fractional base units"
32
+ end
33
+
34
+ int_result
35
+ end
36
+
37
+ # Transform request amounts from human-readable to base units.
38
+ # If `decimals` is present, converts amount and optional suggestedDeposit.
39
+ sig { params(request: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped]) }
40
+ def transform_units(request)
41
+ return request unless request.key?("decimals")
42
+
43
+ result = request.dup
44
+ decimals = result.delete("decimals")
45
+
46
+ Kernel.raise ArgumentError, "decimals must be an integer, got #{decimals.class.name}" unless decimals.is_a?(Integer)
47
+
48
+ result["amount"] = parse_units(result["amount"], decimals).to_s if result.key?("amount")
49
+
50
+ if result.key?("suggestedDeposit") && !result["suggestedDeposit"].nil?
51
+ result["suggestedDeposit"] = parse_units(result["suggestedDeposit"], decimals).to_s
52
+ end
53
+
54
+ result
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,6 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Mpp
5
+ VERSION = "0.1.0"
6
+ end
data/lib/mpp-rb.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mpp"
data/lib/mpp.rb ADDED
@@ -0,0 +1,68 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ require_relative "mpp/version"
7
+ require_relative "mpp/json"
8
+ require_relative "mpp/challenge_id"
9
+ require_relative "mpp/secure_compare"
10
+
11
+ module Mpp
12
+ extend T::Sig
13
+
14
+ autoload :Challenge, "mpp/challenge"
15
+ autoload :ChallengeEcho, "mpp/challenge_echo"
16
+ autoload :Credential, "mpp/credential"
17
+ autoload :Receipt, "mpp/receipt"
18
+ autoload :Parsing, "mpp/parsing"
19
+ autoload :Json, "mpp/json"
20
+ autoload :BodyDigest, "mpp/body_digest"
21
+ autoload :Expires, "mpp/expires"
22
+ autoload :Units, "mpp/units"
23
+ autoload :MemoryStore, "mpp/store"
24
+
25
+ # Server module (autoloaded)
26
+ autoload :Server, "mpp/server"
27
+
28
+ # Client module (autoloaded)
29
+ autoload :Client, "mpp/client"
30
+
31
+ # Methods namespace
32
+ module Methods
33
+ autoload :Tempo, "mpp/methods/tempo"
34
+ autoload :Stripe, "mpp/methods/stripe"
35
+ end
36
+
37
+ # Extensions namespace
38
+ module Extensions
39
+ autoload :MCP, "mpp/extensions/mcp"
40
+ end
41
+
42
+ sig { params(method: T.untyped, realm: T.untyped, secret_key: T.untyped).returns(T.untyped) }
43
+ def self.create(method:, realm: nil, secret_key: nil)
44
+ Server::MppHandler.create(method: method, realm: realm, secret_key: secret_key)
45
+ end
46
+
47
+ # Error hierarchy
48
+ autoload :PaymentError, "mpp/errors"
49
+ autoload :PaymentRequiredError, "mpp/errors"
50
+ autoload :MalformedCredentialError, "mpp/errors"
51
+ autoload :InvalidChallengeError, "mpp/errors"
52
+ autoload :VerificationFailedError, "mpp/errors"
53
+ autoload :PaymentExpiredError, "mpp/errors"
54
+ autoload :InvalidPayloadError, "mpp/errors"
55
+ autoload :PaymentInsufficientError, "mpp/errors"
56
+ autoload :PaymentMethodUnsupportedError, "mpp/errors"
57
+ autoload :PaymentActionRequiredError, "mpp/errors"
58
+ autoload :BadRequestError, "mpp/errors"
59
+ autoload :VerificationError, "mpp/errors"
60
+ autoload :ParseError, "mpp/errors"
61
+ autoload :InsufficientBalanceError, "mpp/errors"
62
+ autoload :InvalidSignatureError, "mpp/errors"
63
+ autoload :SignerMismatchError, "mpp/errors"
64
+ autoload :AmountExceedsDepositError, "mpp/errors"
65
+ autoload :DeltaTooSmallError, "mpp/errors"
66
+ autoload :ChannelNotFoundError, "mpp/errors"
67
+ autoload :ChannelClosedError, "mpp/errors"
68
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mpp-rb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Stripe
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-04-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: base64
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.3'
27
+ description: Ruby SDK for the Machine Payments Protocol (MPP) — an HTTP 402 Payment
28
+ Authentication scheme.
29
+ email:
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - LICENSE
35
+ - README.md
36
+ - lib/mpp-rb.rb
37
+ - lib/mpp.rb
38
+ - lib/mpp/body_digest.rb
39
+ - lib/mpp/challenge.rb
40
+ - lib/mpp/challenge_echo.rb
41
+ - lib/mpp/challenge_id.rb
42
+ - lib/mpp/client.rb
43
+ - lib/mpp/client/transport.rb
44
+ - lib/mpp/credential.rb
45
+ - lib/mpp/errors.rb
46
+ - lib/mpp/expires.rb
47
+ - lib/mpp/extensions/mcp.rb
48
+ - lib/mpp/extensions/mcp/capabilities.rb
49
+ - lib/mpp/extensions/mcp/constants.rb
50
+ - lib/mpp/extensions/mcp/decorator.rb
51
+ - lib/mpp/extensions/mcp/errors.rb
52
+ - lib/mpp/extensions/mcp/types.rb
53
+ - lib/mpp/extensions/mcp/verify.rb
54
+ - lib/mpp/json.rb
55
+ - lib/mpp/methods/stripe.rb
56
+ - lib/mpp/methods/stripe/charge_intent.rb
57
+ - lib/mpp/methods/stripe/client_method.rb
58
+ - lib/mpp/methods/stripe/defaults.rb
59
+ - lib/mpp/methods/stripe/stripe_method.rb
60
+ - lib/mpp/methods/tempo.rb
61
+ - lib/mpp/methods/tempo/account.rb
62
+ - lib/mpp/methods/tempo/attribution.rb
63
+ - lib/mpp/methods/tempo/client_method.rb
64
+ - lib/mpp/methods/tempo/defaults.rb
65
+ - lib/mpp/methods/tempo/fee_payer_envelope.rb
66
+ - lib/mpp/methods/tempo/intents.rb
67
+ - lib/mpp/methods/tempo/keychain.rb
68
+ - lib/mpp/methods/tempo/proof.rb
69
+ - lib/mpp/methods/tempo/rpc.rb
70
+ - lib/mpp/methods/tempo/schemas.rb
71
+ - lib/mpp/methods/tempo/transaction.rb
72
+ - lib/mpp/parsing.rb
73
+ - lib/mpp/receipt.rb
74
+ - lib/mpp/secure_compare.rb
75
+ - lib/mpp/server.rb
76
+ - lib/mpp/server/decorator.rb
77
+ - lib/mpp/server/defaults.rb
78
+ - lib/mpp/server/intent.rb
79
+ - lib/mpp/server/method.rb
80
+ - lib/mpp/server/middleware.rb
81
+ - lib/mpp/server/mpp_handler.rb
82
+ - lib/mpp/server/verify.rb
83
+ - lib/mpp/store.rb
84
+ - lib/mpp/units.rb
85
+ - lib/mpp/version.rb
86
+ homepage: https://github.com/stripe/mpp-rb
87
+ licenses:
88
+ - MIT
89
+ metadata:
90
+ rubygems_mfa_required: 'true'
91
+ source_code_uri: https://github.com/stripe/mpp-rb
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 3.3.0
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 3.5.22
108
+ signing_key:
109
+ specification_version: 4
110
+ summary: HTTP 402 Payment Authentication for Ruby
111
+ test_files: []