mpp-rb 0.1.2 → 0.1.3
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 +37 -5
- data/lib/mpp/body_digest.rb +1 -1
- data/lib/mpp/challenge.rb +71 -6
- data/lib/mpp/client/transport.rb +153 -18
- data/lib/mpp/errors.rb +3 -0
- data/lib/mpp/events.rb +124 -0
- data/lib/mpp/extensions/mcp/types.rb +2 -1
- data/lib/mpp/methods/stripe/charge_intent.rb +41 -16
- data/lib/mpp/methods/stripe/client_method.rb +5 -1
- data/lib/mpp/methods/stripe/stripe_method.rb +15 -4
- data/lib/mpp/methods/tempo/client_method.rb +28 -8
- data/lib/mpp/methods/tempo/fee_payer_policy.rb +45 -0
- data/lib/mpp/methods/tempo/intents.rb +394 -64
- data/lib/mpp/methods/tempo/proof.rb +19 -15
- data/lib/mpp/methods/tempo/transaction.rb +4 -3
- data/lib/mpp/methods/tempo.rb +1 -0
- data/lib/mpp/parsing.rb +14 -2
- data/lib/mpp/receipt.rb +3 -2
- data/lib/mpp/server/decorator.rb +1 -0
- data/lib/mpp/server/middleware.rb +101 -2
- data/lib/mpp/server/mpp_handler.rb +40 -8
- data/lib/mpp/server/verify.rb +173 -27
- data/lib/mpp/version.rb +1 -1
- data/lib/mpp.rb +5 -3
- metadata +3 -1
data/lib/mpp/server/verify.rb
CHANGED
|
@@ -15,14 +15,40 @@ module Mpp
|
|
|
15
15
|
# Verify a payment credential or generate a new challenge.
|
|
16
16
|
#
|
|
17
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) }
|
|
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), events: T.nilable(Mpp::Events::Dispatcher), body: T.untyped).returns(T.untyped) }
|
|
19
19
|
def verify_or_challenge(authorization:, intent:, request:, realm:, secret_key:,
|
|
20
|
-
method: nil, description: nil, meta: nil, expires: nil)
|
|
20
|
+
method: nil, description: nil, meta: nil, expires: nil, events: nil, body: nil)
|
|
21
21
|
method_name = method || "tempo"
|
|
22
22
|
request = Mpp::Units.transform_units(request)
|
|
23
|
+
dispatcher = events
|
|
24
|
+
events_enabled = dispatcher&.has_handlers?
|
|
25
|
+
method_context = events_enabled ? {name: method_name, intent: intent.name} : nil
|
|
23
26
|
|
|
24
|
-
new_challenge = Kernel.lambda {
|
|
25
|
-
create_challenge(method_name, intent.name, request, realm, secret_key, description, meta, expires)
|
|
27
|
+
new_challenge = Kernel.lambda { |credential = nil, error = nil, submitted_challenge = nil|
|
|
28
|
+
challenge = create_challenge(method_name, intent.name, request, realm, secret_key, description, meta, expires, body)
|
|
29
|
+
if error && dispatcher&.has_handlers?(Mpp::Events::PAYMENT_FAILED)
|
|
30
|
+
emit_payment_failed(
|
|
31
|
+
dispatcher: dispatcher,
|
|
32
|
+
challenge: challenge,
|
|
33
|
+
credential: credential,
|
|
34
|
+
error: error,
|
|
35
|
+
method: T.must(method_context),
|
|
36
|
+
request: request,
|
|
37
|
+
retry_challenge: challenge,
|
|
38
|
+
submitted_challenge: submitted_challenge
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
if dispatcher&.has_handlers?(Mpp::Events::CHALLENGE_CREATED)
|
|
42
|
+
emit_challenge_created(
|
|
43
|
+
dispatcher: dispatcher,
|
|
44
|
+
challenge: challenge,
|
|
45
|
+
credential: credential,
|
|
46
|
+
error: error,
|
|
47
|
+
method: T.must(method_context),
|
|
48
|
+
request: request
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
challenge
|
|
26
52
|
}
|
|
27
53
|
|
|
28
54
|
return new_challenge.call if authorization.nil?
|
|
@@ -32,8 +58,8 @@ module Mpp
|
|
|
32
58
|
|
|
33
59
|
begin
|
|
34
60
|
credential = Mpp::Credential.from_authorization(payment_scheme)
|
|
35
|
-
rescue Mpp::ParseError
|
|
36
|
-
return new_challenge.call
|
|
61
|
+
rescue Mpp::ParseError => e
|
|
62
|
+
return new_challenge.call(nil, Mpp::MalformedCredentialError.new(reason: e.message))
|
|
37
63
|
end
|
|
38
64
|
|
|
39
65
|
# Stateless challenge verification
|
|
@@ -41,8 +67,8 @@ module Mpp
|
|
|
41
67
|
begin
|
|
42
68
|
echo_request = echo.request.empty? ? {} : Mpp::Parsing.b64_decode(echo.request)
|
|
43
69
|
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
|
|
70
|
+
rescue Mpp::ParseError => e
|
|
71
|
+
return new_challenge.call(credential, Mpp::MalformedCredentialError.new(reason: e.message), echo)
|
|
46
72
|
end
|
|
47
73
|
|
|
48
74
|
expected_id = Mpp.generate_challenge_id(
|
|
@@ -55,54 +81,120 @@ module Mpp
|
|
|
55
81
|
digest: echo.digest,
|
|
56
82
|
opaque: echo_opaque
|
|
57
83
|
)
|
|
58
|
-
|
|
84
|
+
unless Mpp.secure_compare(echo.id, expected_id)
|
|
85
|
+
return new_challenge.call(
|
|
86
|
+
credential,
|
|
87
|
+
Mpp::InvalidChallengeError.new(challenge_id: echo.id, reason: "challenge id mismatch"),
|
|
88
|
+
echo
|
|
89
|
+
)
|
|
90
|
+
end
|
|
59
91
|
|
|
60
92
|
# Assert echoed fields match server's values
|
|
61
|
-
|
|
93
|
+
unless echo.realm == realm && echo.method == method_name && echo.intent == intent.name
|
|
94
|
+
return new_challenge.call(
|
|
95
|
+
credential,
|
|
96
|
+
Mpp::InvalidChallengeError.new(challenge_id: echo.id, reason: "challenge binding mismatch"),
|
|
97
|
+
echo
|
|
98
|
+
)
|
|
99
|
+
end
|
|
62
100
|
|
|
63
101
|
# Assert echoed request matches server's current request
|
|
64
|
-
|
|
102
|
+
unless echo_request == request
|
|
103
|
+
return new_challenge.call(
|
|
104
|
+
credential,
|
|
105
|
+
Mpp::InvalidChallengeError.new(challenge_id: echo.id, reason: "request mismatch"),
|
|
106
|
+
echo
|
|
107
|
+
)
|
|
108
|
+
end
|
|
65
109
|
|
|
66
|
-
|
|
110
|
+
unless echo_opaque == meta
|
|
111
|
+
return new_challenge.call(
|
|
112
|
+
credential,
|
|
113
|
+
Mpp::InvalidChallengeError.new(challenge_id: echo.id, reason: "metadata mismatch"),
|
|
114
|
+
echo
|
|
115
|
+
)
|
|
116
|
+
end
|
|
67
117
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
# If we can't parse, continue to stricter check below
|
|
75
|
-
end
|
|
118
|
+
unless body_digest_matches?(echo, body)
|
|
119
|
+
return new_challenge.call(
|
|
120
|
+
credential,
|
|
121
|
+
Mpp::InvalidChallengeError.new(challenge_id: echo.id, reason: "body digest mismatch"),
|
|
122
|
+
echo
|
|
123
|
+
)
|
|
76
124
|
end
|
|
77
125
|
|
|
78
126
|
# Verify echoed request parameters match endpoint's expected request
|
|
79
127
|
request.each do |key, value|
|
|
80
|
-
|
|
128
|
+
unless echo_request[key] == value
|
|
129
|
+
return new_challenge.call(
|
|
130
|
+
credential,
|
|
131
|
+
Mpp::InvalidChallengeError.new(challenge_id: echo.id, reason: "request field #{key} mismatch"),
|
|
132
|
+
echo
|
|
133
|
+
)
|
|
134
|
+
end
|
|
81
135
|
end
|
|
82
136
|
|
|
83
137
|
# Enforce challenge expiry - fail closed
|
|
84
|
-
|
|
138
|
+
unless echo.expires
|
|
139
|
+
return new_challenge.call(
|
|
140
|
+
credential,
|
|
141
|
+
Mpp::InvalidChallengeError.new(challenge_id: echo.id, reason: "missing expiry"),
|
|
142
|
+
echo
|
|
143
|
+
)
|
|
144
|
+
end
|
|
85
145
|
|
|
86
146
|
begin
|
|
87
147
|
expires_dt = Time.iso8601(T.must(echo.expires).gsub("Z", "+00:00"))
|
|
88
148
|
rescue ArgumentError
|
|
89
|
-
return new_challenge.call
|
|
149
|
+
return new_challenge.call(
|
|
150
|
+
credential,
|
|
151
|
+
Mpp::InvalidChallengeError.new(challenge_id: echo.id, reason: "invalid expiry"),
|
|
152
|
+
echo
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
if expires_dt < Time.now.utc
|
|
156
|
+
return new_challenge.call(credential, Mpp::PaymentExpiredError.new(expires: echo.expires), echo)
|
|
90
157
|
end
|
|
91
|
-
return new_challenge.call if expires_dt < Time.now.utc
|
|
92
158
|
|
|
93
|
-
|
|
159
|
+
begin
|
|
160
|
+
receipt = intent.verify(credential, request)
|
|
161
|
+
rescue => e
|
|
162
|
+
if dispatcher&.has_handlers?(Mpp::Events::PAYMENT_FAILED)
|
|
163
|
+
emit_payment_failed(
|
|
164
|
+
dispatcher: dispatcher,
|
|
165
|
+
challenge: echo,
|
|
166
|
+
credential: credential,
|
|
167
|
+
error: e,
|
|
168
|
+
method: T.must(method_context),
|
|
169
|
+
request: request,
|
|
170
|
+
submitted_challenge: echo
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
Kernel.raise
|
|
174
|
+
end
|
|
175
|
+
if dispatcher&.has_handlers?(Mpp::Events::PAYMENT_SUCCESS)
|
|
176
|
+
emit_payment_success(
|
|
177
|
+
dispatcher: dispatcher,
|
|
178
|
+
challenge: echo,
|
|
179
|
+
credential: credential,
|
|
180
|
+
method: T.must(method_context),
|
|
181
|
+
receipt: receipt,
|
|
182
|
+
request: request
|
|
183
|
+
)
|
|
184
|
+
end
|
|
94
185
|
[credential, receipt]
|
|
95
186
|
end
|
|
96
187
|
|
|
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) }
|
|
188
|
+
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), body: T.untyped).returns(Mpp::Challenge) }
|
|
98
189
|
def create_challenge(method, intent_name, request, realm, secret_key,
|
|
99
|
-
description = nil, meta = nil, expires = nil)
|
|
190
|
+
description = nil, meta = nil, expires = nil, body = nil)
|
|
100
191
|
expires = nil if expires && !expires.is_a?(String)
|
|
101
192
|
|
|
102
193
|
if expires.nil?
|
|
103
194
|
expires_dt = Time.now.utc + (DEFAULT_EXPIRES_MINUTES * 60)
|
|
104
195
|
expires = expires_dt.strftime("%Y-%m-%dT%H:%M:%S.%LZ")
|
|
105
196
|
end
|
|
197
|
+
digest = body.nil? ? nil : Mpp::BodyDigest.compute(body)
|
|
106
198
|
|
|
107
199
|
Mpp::Challenge.create(
|
|
108
200
|
secret_key: secret_key,
|
|
@@ -111,11 +203,22 @@ module Mpp
|
|
|
111
203
|
intent: intent_name,
|
|
112
204
|
request: request,
|
|
113
205
|
expires: expires,
|
|
206
|
+
digest: digest,
|
|
114
207
|
description: description,
|
|
115
208
|
meta: meta
|
|
116
209
|
)
|
|
117
210
|
end
|
|
118
211
|
|
|
212
|
+
sig { params(echo: Mpp::ChallengeEcho, body: T.untyped).returns(T::Boolean) }
|
|
213
|
+
def body_digest_matches?(echo, body)
|
|
214
|
+
if body.nil?
|
|
215
|
+
return echo.digest.nil?
|
|
216
|
+
end
|
|
217
|
+
return false unless echo.digest
|
|
218
|
+
|
|
219
|
+
Mpp::BodyDigest.verify(T.must(echo.digest), body)
|
|
220
|
+
end
|
|
221
|
+
|
|
119
222
|
sig { params(header: String).returns(T.nilable(String)) }
|
|
120
223
|
def extract_payment_scheme(header)
|
|
121
224
|
header.split(",").each do |scheme|
|
|
@@ -124,6 +227,49 @@ module Mpp
|
|
|
124
227
|
end
|
|
125
228
|
nil
|
|
126
229
|
end
|
|
230
|
+
|
|
231
|
+
sig { params(dispatcher: T.nilable(Mpp::Events::Dispatcher), challenge: T.untyped, credential: T.untyped, error: T.untyped, method: T::Hash[Symbol, T.untyped], request: T::Hash[String, T.untyped]).void }
|
|
232
|
+
def emit_challenge_created(dispatcher:, challenge:, credential:, error:, method:, request:)
|
|
233
|
+
return unless dispatcher&.has_handlers?(Mpp::Events::CHALLENGE_CREATED)
|
|
234
|
+
|
|
235
|
+
payload = {
|
|
236
|
+
challenge: challenge,
|
|
237
|
+
method: method,
|
|
238
|
+
request: request
|
|
239
|
+
}
|
|
240
|
+
payload[:credential] = credential unless credential.nil?
|
|
241
|
+
payload[:error] = error unless error.nil?
|
|
242
|
+
dispatcher.emit(Mpp::Events::CHALLENGE_CREATED, payload)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
sig { params(dispatcher: Mpp::Events::Dispatcher, challenge: T.untyped, credential: T.untyped, error: T.untyped, method: T::Hash[Symbol, T.untyped], request: T::Hash[String, T.untyped], retry_challenge: T.untyped, submitted_challenge: T.untyped).void }
|
|
246
|
+
def emit_payment_failed(dispatcher:, challenge:, credential:, error:, method:, request:, retry_challenge: nil, submitted_challenge: nil)
|
|
247
|
+
return unless dispatcher.has_handlers?(Mpp::Events::PAYMENT_FAILED)
|
|
248
|
+
|
|
249
|
+
payload = {
|
|
250
|
+
challenge: challenge,
|
|
251
|
+
credential: credential,
|
|
252
|
+
error: error,
|
|
253
|
+
method: method,
|
|
254
|
+
request: request
|
|
255
|
+
}
|
|
256
|
+
payload[:retry_challenge] = retry_challenge unless retry_challenge.nil?
|
|
257
|
+
payload[:submitted_challenge] = submitted_challenge unless submitted_challenge.nil?
|
|
258
|
+
dispatcher.emit(Mpp::Events::PAYMENT_FAILED, payload)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
sig { params(dispatcher: Mpp::Events::Dispatcher, challenge: T.untyped, credential: T.untyped, method: T::Hash[Symbol, T.untyped], receipt: T.untyped, request: T::Hash[String, T.untyped]).void }
|
|
262
|
+
def emit_payment_success(dispatcher:, challenge:, credential:, method:, receipt:, request:)
|
|
263
|
+
return unless dispatcher.has_handlers?(Mpp::Events::PAYMENT_SUCCESS)
|
|
264
|
+
|
|
265
|
+
dispatcher.emit(Mpp::Events::PAYMENT_SUCCESS, {
|
|
266
|
+
challenge: challenge,
|
|
267
|
+
credential: credential,
|
|
268
|
+
method: method,
|
|
269
|
+
receipt: receipt,
|
|
270
|
+
request: request
|
|
271
|
+
})
|
|
272
|
+
end
|
|
127
273
|
end
|
|
128
274
|
end
|
|
129
275
|
end
|
data/lib/mpp/version.rb
CHANGED
data/lib/mpp.rb
CHANGED
|
@@ -19,6 +19,7 @@ module Mpp
|
|
|
19
19
|
autoload :Json, "mpp/json"
|
|
20
20
|
autoload :BodyDigest, "mpp/body_digest"
|
|
21
21
|
autoload :Expires, "mpp/expires"
|
|
22
|
+
autoload :Events, "mpp/events"
|
|
22
23
|
autoload :Units, "mpp/units"
|
|
23
24
|
autoload :MemoryStore, "mpp/store"
|
|
24
25
|
|
|
@@ -39,9 +40,9 @@ module Mpp
|
|
|
39
40
|
autoload :MCP, "mpp/extensions/mcp"
|
|
40
41
|
end
|
|
41
42
|
|
|
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)
|
|
43
|
+
sig { params(method: T.untyped, realm: T.untyped, secret_key: T.untyped, events: T.nilable(Mpp::Events::Dispatcher)).returns(T.untyped) }
|
|
44
|
+
def self.create(method:, realm: nil, secret_key: nil, events: nil)
|
|
45
|
+
Server::MppHandler.create(method: method, realm: realm, secret_key: secret_key, events: events)
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
# Error hierarchy
|
|
@@ -57,6 +58,7 @@ module Mpp
|
|
|
57
58
|
autoload :PaymentActionRequiredError, "mpp/errors"
|
|
58
59
|
autoload :BadRequestError, "mpp/errors"
|
|
59
60
|
autoload :VerificationError, "mpp/errors"
|
|
61
|
+
autoload :TransactionPendingError, "mpp/errors"
|
|
60
62
|
autoload :ParseError, "mpp/errors"
|
|
61
63
|
autoload :InsufficientBalanceError, "mpp/errors"
|
|
62
64
|
autoload :InvalidSignatureError, "mpp/errors"
|
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.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stripe
|
|
@@ -41,6 +41,7 @@ files:
|
|
|
41
41
|
- lib/mpp/client/transport.rb
|
|
42
42
|
- lib/mpp/credential.rb
|
|
43
43
|
- lib/mpp/errors.rb
|
|
44
|
+
- lib/mpp/events.rb
|
|
44
45
|
- lib/mpp/expires.rb
|
|
45
46
|
- lib/mpp/extensions/mcp.rb
|
|
46
47
|
- lib/mpp/extensions/mcp/capabilities.rb
|
|
@@ -61,6 +62,7 @@ files:
|
|
|
61
62
|
- lib/mpp/methods/tempo/client_method.rb
|
|
62
63
|
- lib/mpp/methods/tempo/defaults.rb
|
|
63
64
|
- lib/mpp/methods/tempo/fee_payer_envelope.rb
|
|
65
|
+
- lib/mpp/methods/tempo/fee_payer_policy.rb
|
|
64
66
|
- lib/mpp/methods/tempo/intents.rb
|
|
65
67
|
- lib/mpp/methods/tempo/keychain.rb
|
|
66
68
|
- lib/mpp/methods/tempo/proof.rb
|