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.
@@ -10,7 +10,7 @@ module Mpp
10
10
  class MppHandler
11
11
  extend T::Sig
12
12
 
13
- REQUEST_OPTION_KEYS = [:body, :url, :payment_signature, :accept_payment, :http_method].freeze
13
+ REQUEST_OPTION_KEYS = [:body, :url, :payment_signature, :accept_payment, :http_method, :payment_authorization].freeze
14
14
 
15
15
  sig { returns(T.untyped) }
16
16
  attr_reader :method
@@ -27,6 +27,12 @@ module Mpp
27
27
  sig { returns(T::Hash[String, T.untyped]) }
28
28
  attr_reader :defaults
29
29
 
30
+ sig { returns(T::Boolean) }
31
+ attr_reader :requires_auth
32
+
33
+ sig { returns(T.nilable(String)) }
34
+ attr_reader :credential_header
35
+
30
36
  sig do
31
37
  params(
32
38
  realm: String,
@@ -34,16 +40,21 @@ module Mpp
34
40
  method: T.untyped,
35
41
  methods: T.nilable(T::Array[T.untyped]),
36
42
  defaults: T.nilable(T::Hash[String, T.untyped]),
37
- events: T.nilable(Mpp::Events::Dispatcher)
43
+ events: T.nilable(Mpp::Events::Dispatcher),
44
+ requires_auth: T::Boolean
38
45
  ).void
39
46
  end
40
- def initialize(realm:, secret_key:, method: nil, methods: nil, defaults: nil, events: nil)
47
+ def initialize(realm:, secret_key:, method: nil, methods: nil, defaults: nil, events: nil, requires_auth: false)
41
48
  @methods = T.let(normalize_methods(method, methods), T::Array[T.untyped])
42
49
  @method = T.let(@methods.first, T.untyped)
43
50
  @realm = T.let(realm, String)
44
51
  @secret_key = T.let(secret_key, String)
45
52
  @defaults = T.let(defaults || {}, T::Hash[String, T.untyped])
46
53
  @events = T.let(events || Mpp::Events.server_dispatcher, Mpp::Events::Dispatcher)
54
+ @method_hook_events = T.let(Mpp::Events.server_dispatcher, Mpp::Events::Dispatcher)
55
+ @requires_auth = T.let(requires_auth, T::Boolean)
56
+ @credential_header = T.let(requires_auth ? Mpp::PAYMENT_AUTHORIZATION_HEADER : nil, T.nilable(String))
57
+ register_method_payment_success
47
58
  end
48
59
 
49
60
  # Create with auto-detected realm and secret_key.
@@ -53,16 +64,18 @@ module Mpp
53
64
  methods: T.nilable(T::Array[T.untyped]),
54
65
  realm: T.untyped,
55
66
  secret_key: T.untyped,
56
- events: T.nilable(Mpp::Events::Dispatcher)
67
+ events: T.nilable(Mpp::Events::Dispatcher),
68
+ requires_auth: T::Boolean
57
69
  ).returns(T.attached_class)
58
70
  end
59
- def self.create(method: nil, methods: nil, realm: nil, secret_key: nil, events: nil)
71
+ def self.create(method: nil, methods: nil, realm: nil, secret_key: nil, events: nil, requires_auth: false)
60
72
  new(
61
73
  method: method,
62
74
  methods: methods,
63
75
  realm: realm || Defaults.detect_realm,
64
76
  secret_key: secret_key || Defaults.detect_secret_key,
65
- events: events
77
+ events: events,
78
+ requires_auth: requires_auth
66
79
  )
67
80
  end
68
81
 
@@ -86,6 +99,14 @@ module Mpp
86
99
  on(Mpp::Events::PAYMENT_SUCCESS, handler, &block)
87
100
  end
88
101
 
102
+ # Select the HTTP header value that carries the Payment credential.
103
+ # When requires_auth is true, Payment credentials ride in
104
+ # Payment-Authorization so Authorization can hold application auth.
105
+ sig { params(authorization: T.nilable(String), payment_authorization: T.nilable(String)).returns(T.nilable(String)) }
106
+ def payment_credential_value(authorization, payment_authorization = nil)
107
+ @requires_auth ? payment_authorization : authorization
108
+ end
109
+
89
110
  # Combine method offers into a single callable that presents every method.
90
111
  sig { params(entries: T.untyped).returns(ComposedHandler) }
91
112
  def compose(*entries)
@@ -119,11 +140,12 @@ module Mpp
119
140
 
120
141
  request_opts = kwargs.slice(*REQUEST_OPTION_KEYS)
121
142
  offer_opts = kwargs.except(*REQUEST_OPTION_KEYS)
143
+ payment = payment_credential_value(authorization, request_opts[:payment_authorization])
122
144
 
123
145
  if charge_methods.length == 1
124
146
  return charge_one(
125
147
  charge_methods.first,
126
- authorization,
148
+ payment,
127
149
  amount,
128
150
  **request_opts,
129
151
  **offer_opts
@@ -133,6 +155,7 @@ module Mpp
133
155
  entries = charge_methods.map { |candidate| [candidate, offer_opts.merge(amount: amount)] }
134
156
  ComposedHandler.from_entries(self, entries).call(
135
157
  authorization: authorization,
158
+ payment_authorization: request_opts[:payment_authorization],
136
159
  payment_signature: request_opts[:payment_signature],
137
160
  body: request_opts[:body],
138
161
  url: request_opts[:url],
@@ -142,8 +165,10 @@ module Mpp
142
165
  end
143
166
 
144
167
  # Verify or challenge a single method. Used by compose and charge.
145
- sig { params(method: T.untyped, authorization: T.nilable(String), amount: String, kwargs: T.untyped).returns(T.untyped) }
146
- def charge_one(method, authorization, amount, **kwargs)
168
+ # The optional request guard can omit an offer after its canonical request
169
+ # is built, ensuring the guard and challenge use the exact same request.
170
+ sig { params(method: T.untyped, authorization: T.nilable(String), amount: String, kwargs: T.untyped, request_guard: T.nilable(T.proc.params(request: T::Hash[String, T.untyped]).returns(T::Boolean))).returns(T.untyped) }
171
+ def charge_one(method, authorization, amount, **kwargs, &request_guard)
147
172
  intent = method.intents["charge"]
148
173
  raise ArgumentError, "Method #{method.name} does not support charge intent" unless intent
149
174
 
@@ -152,6 +177,7 @@ module Mpp
152
177
  payment_signature = kwargs[:payment_signature]
153
178
  offer_opts = kwargs.except(*REQUEST_OPTION_KEYS)
154
179
  request = build_charge_request(method, amount, **offer_opts)
180
+ return nil if request_guard && !request_guard.call(request)
155
181
 
156
182
  if payment_signature && method.respond_to?(:bind_x402_credential)
157
183
  return verify_x402(
@@ -177,7 +203,9 @@ module Mpp
177
203
  description: offer_opts[:description],
178
204
  expires: offer_opts[:expires],
179
205
  events: @events,
180
- body: body
206
+ method_hook_events: @method_hook_events,
207
+ body: body,
208
+ header: @credential_header
181
209
  )
182
210
  end
183
211
 
@@ -304,7 +332,8 @@ module Mpp
304
332
  description,
305
333
  nil,
306
334
  expires,
307
- body
335
+ body,
336
+ @credential_header
308
337
  )
309
338
  method_context = {name: method.name, intent: intent.name}
310
339
 
@@ -346,9 +375,10 @@ module Mpp
346
375
  raise
347
376
  end
348
377
 
349
- if @events.has_handlers?(Mpp::Events::PAYMENT_SUCCESS)
378
+ if @events.has_handlers?(Mpp::Events::PAYMENT_SUCCESS) || @method_hook_events.has_handlers?(Mpp::Events::PAYMENT_SUCCESS)
350
379
  Verify.emit_payment_success(
351
380
  dispatcher: @events,
381
+ method_hook_dispatcher: @method_hook_events,
352
382
  challenge: challenge,
353
383
  credential: credential,
354
384
  method: method_context,
@@ -358,6 +388,30 @@ module Mpp
358
388
  end
359
389
  [credential, receipt]
360
390
  end
391
+
392
+ private
393
+
394
+ sig { void }
395
+ def register_method_payment_success
396
+ @methods.each do |payment_method|
397
+ next unless payment_method.respond_to?(:on_payment_success)
398
+
399
+ hook = payment_method.on_payment_success
400
+ next if hook.nil?
401
+ raise ArgumentError, "on_payment_success must be callable" unless hook.respond_to?(:call)
402
+
403
+ method_name = payment_method.name
404
+ intent_names = payment_method.intents.each_value.map { |intent| intent.name }
405
+ @method_hook_events.on(Mpp::Events::PAYMENT_SUCCESS) do |payload|
406
+ event_method = payload[:method]
407
+ next unless event_method.is_a?(Hash)
408
+ next unless event_method[:name] == method_name
409
+ next unless intent_names.include?(event_method[:intent])
410
+
411
+ hook.call(payload)
412
+ end
413
+ end
414
+ end
361
415
  end
362
416
  end
363
417
  end
@@ -15,17 +15,17 @@ 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), events: T.nilable(Mpp::Events::Dispatcher), body: T.untyped).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), method_hook_events: T.nilable(Mpp::Events::Dispatcher), body: T.untyped, header: T.nilable(String)).returns(T.untyped) }
19
19
  def verify_or_challenge(authorization:, intent:, request:, realm:, secret_key:,
20
- method: nil, description: nil, meta: nil, expires: nil, events: nil, body: nil)
20
+ method: nil, description: nil, meta: nil, expires: nil, events: nil, method_hook_events: nil, body: nil, header: nil)
21
21
  method_name = method || "tempo"
22
22
  request = Mpp::Units.transform_units(request)
23
23
  dispatcher = events
24
- events_enabled = dispatcher&.has_handlers?
24
+ events_enabled = dispatcher&.has_handlers? || method_hook_events&.has_handlers?
25
25
  method_context = events_enabled ? {name: method_name, intent: intent.name} : nil
26
26
 
27
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)
28
+ challenge = create_challenge(method_name, intent.name, request, realm, secret_key, description, meta, expires, body, header)
29
29
  if error && dispatcher&.has_handlers?(Mpp::Events::PAYMENT_FAILED)
30
30
  emit_payment_failed(
31
31
  dispatcher: dispatcher,
@@ -79,7 +79,8 @@ module Mpp
79
79
  request: echo_request,
80
80
  expires: echo.expires,
81
81
  digest: echo.digest,
82
- opaque: echo_opaque
82
+ opaque: echo_opaque,
83
+ header: echo.header
83
84
  )
84
85
  unless Mpp.secure_compare(echo.id, expected_id)
85
86
  return new_challenge.call(
@@ -172,9 +173,10 @@ module Mpp
172
173
  end
173
174
  Kernel.raise
174
175
  end
175
- if dispatcher&.has_handlers?(Mpp::Events::PAYMENT_SUCCESS)
176
+ if dispatcher&.has_handlers?(Mpp::Events::PAYMENT_SUCCESS) || method_hook_events&.has_handlers?(Mpp::Events::PAYMENT_SUCCESS)
176
177
  emit_payment_success(
177
178
  dispatcher: dispatcher,
179
+ method_hook_dispatcher: method_hook_events,
178
180
  challenge: echo,
179
181
  credential: credential,
180
182
  method: T.must(method_context),
@@ -185,9 +187,9 @@ module Mpp
185
187
  [credential, receipt]
186
188
  end
187
189
 
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) }
190
+ 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, header: T.nilable(String)).returns(Mpp::Challenge) }
189
191
  def create_challenge(method, intent_name, request, realm, secret_key,
190
- description = nil, meta = nil, expires = nil, body = nil)
192
+ description = nil, meta = nil, expires = nil, body = nil, header = nil)
191
193
  expires = nil if expires && !expires.is_a?(String)
192
194
 
193
195
  if expires.nil?
@@ -205,7 +207,8 @@ module Mpp
205
207
  expires: expires,
206
208
  digest: digest,
207
209
  description: description,
208
- meta: meta
210
+ meta: meta,
211
+ header: header
209
212
  )
210
213
  end
211
214
 
@@ -258,17 +261,24 @@ module Mpp
258
261
  dispatcher.emit(Mpp::Events::PAYMENT_FAILED, payload)
259
262
  end
260
263
 
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
+ # Method hooks are owned by a handler, while `dispatcher` may be shared
265
+ # by many handlers. Emit the same payload to each without exposing that
266
+ # handler ownership in the public event payload.
267
+ sig { params(dispatcher: T.nilable(Mpp::Events::Dispatcher), challenge: T.untyped, credential: T.untyped, method: T::Hash[Symbol, T.untyped], receipt: T.untyped, request: T::Hash[String, T.untyped], method_hook_dispatcher: T.nilable(Mpp::Events::Dispatcher)).void }
268
+ def emit_payment_success(dispatcher:, challenge:, credential:, method:, receipt:, request:, method_hook_dispatcher: nil)
269
+ public_handlers = dispatcher&.has_handlers?(Mpp::Events::PAYMENT_SUCCESS)
270
+ hook_handlers = method_hook_dispatcher&.has_handlers?(Mpp::Events::PAYMENT_SUCCESS)
271
+ return unless public_handlers || hook_handlers
264
272
 
265
- dispatcher.emit(Mpp::Events::PAYMENT_SUCCESS, {
273
+ payload = {
266
274
  challenge: challenge,
267
275
  credential: credential,
268
276
  method: method,
269
277
  receipt: receipt,
270
278
  request: request
271
- })
279
+ }
280
+ dispatcher.emit(Mpp::Events::PAYMENT_SUCCESS, payload) if public_handlers
281
+ method_hook_dispatcher.emit(Mpp::Events::PAYMENT_SUCCESS, payload) if hook_handlers && method_hook_dispatcher != dispatcher
272
282
  end
273
283
  end
274
284
  end
data/lib/mpp/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Mpp
5
- VERSION = "0.1.5"
5
+ VERSION = "0.1.6"
6
6
  end
data/lib/mpp.rb CHANGED
@@ -11,6 +11,9 @@ require_relative "mpp/secure_compare"
11
11
  module Mpp
12
12
  extend T::Sig
13
13
 
14
+ AUTHORIZATION_HEADER = T.let("Authorization", String)
15
+ PAYMENT_AUTHORIZATION_HEADER = T.let("Payment-Authorization", String)
16
+
14
17
  autoload :Challenge, "mpp/challenge"
15
18
  autoload :ChallengeEcho, "mpp/challenge_echo"
16
19
  autoload :Credential, "mpp/credential"
@@ -43,9 +46,16 @@ module Mpp
43
46
  autoload :MCP, "mpp/extensions/mcp"
44
47
  end
45
48
 
46
- sig { params(method: T.untyped, methods: T.nilable(T::Array[T.untyped]), realm: T.untyped, secret_key: T.untyped, events: T.nilable(Mpp::Events::Dispatcher)).returns(T.untyped) }
47
- def self.create(method: nil, methods: nil, realm: nil, secret_key: nil, events: nil)
48
- Server::MppHandler.create(method: method, methods: methods, realm: realm, secret_key: secret_key, events: events)
49
+ sig { params(method: T.untyped, methods: T.nilable(T::Array[T.untyped]), realm: T.untyped, secret_key: T.untyped, events: T.nilable(Mpp::Events::Dispatcher), requires_auth: T::Boolean).returns(T.untyped) }
50
+ def self.create(method: nil, methods: nil, realm: nil, secret_key: nil, events: nil, requires_auth: false)
51
+ Server::MppHandler.create(
52
+ method: method,
53
+ methods: methods,
54
+ realm: realm,
55
+ secret_key: secret_key,
56
+ events: events,
57
+ requires_auth: requires_auth
58
+ )
49
59
  end
50
60
 
51
61
  # Error hierarchy
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.5
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stripe
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '1.3'
40
+ - !ruby/object:Gem::Dependency
41
+ name: sorbet-runtime
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
40
54
  description: Ruby SDK for the Machine Payments Protocol (MPP) — an HTTP 402 Payment
41
55
  Authentication scheme.
42
56
  executables: []
@@ -75,7 +89,9 @@ files:
75
89
  - lib/mpp/methods/stripe.rb
76
90
  - lib/mpp/methods/stripe/charge_intent.rb
77
91
  - lib/mpp/methods/stripe/client_method.rb
92
+ - lib/mpp/methods/stripe/crypto_payment_recorder.rb
78
93
  - lib/mpp/methods/stripe/defaults.rb
94
+ - lib/mpp/methods/stripe/machine_payments.rb
79
95
  - lib/mpp/methods/stripe/stripe_method.rb
80
96
  - lib/mpp/methods/tempo.rb
81
97
  - lib/mpp/methods/tempo/account.rb