mppx 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.
- checksums.yaml +7 -0
- data/LICENSE +25 -0
- data/README.md +133 -0
- data/lib/mppx/base64url.rb +23 -0
- data/lib/mppx/body_digest.rb +21 -0
- data/lib/mppx/canonical_json.rb +26 -0
- data/lib/mppx/challenge.rb +276 -0
- data/lib/mppx/constant_time_equal.rb +19 -0
- data/lib/mppx/credential.rb +90 -0
- data/lib/mppx/env.rb +38 -0
- data/lib/mppx/errors.rb +219 -0
- data/lib/mppx/expires.rb +39 -0
- data/lib/mppx/mcp.rb +10 -0
- data/lib/mppx/method.rb +27 -0
- data/lib/mppx/payment_request.rb +28 -0
- data/lib/mppx/receipt.rb +47 -0
- data/lib/mppx/server/handler.rb +368 -0
- data/lib/mppx/server/transport.rb +49 -0
- data/lib/mppx/store.rb +33 -0
- data/lib/mppx/version.rb +5 -0
- data/lib/mppx.rb +24 -0
- metadata +106 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Mppx
|
|
6
|
+
module Server
|
|
7
|
+
module Handler
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def create(methods:, secret_key:, realm: nil, transport: nil)
|
|
11
|
+
realm ||= Env.get(:realm) || "MPP Payment"
|
|
12
|
+
secret_key ||= Env.get(:secret_key)
|
|
13
|
+
transport ||= Transport.http
|
|
14
|
+
|
|
15
|
+
raise "Missing secret key. Set the MPP_SECRET_KEY environment variable or pass secret_key to Handler.create()." unless secret_key
|
|
16
|
+
|
|
17
|
+
flat_methods = methods.flatten
|
|
18
|
+
handlers = {}
|
|
19
|
+
intent_count = Hash.new(0)
|
|
20
|
+
|
|
21
|
+
flat_methods.each do |mi|
|
|
22
|
+
intent_count[mi[:intent]] += 1
|
|
23
|
+
key = "#{mi[:name]}/#{mi[:intent]}"
|
|
24
|
+
handlers[key] = create_method_fn(
|
|
25
|
+
method: mi,
|
|
26
|
+
realm: realm,
|
|
27
|
+
secret_key: secret_key,
|
|
28
|
+
transport: transport,
|
|
29
|
+
verify: mi[:verify],
|
|
30
|
+
defaults: mi[:defaults],
|
|
31
|
+
request_fn: mi[:request],
|
|
32
|
+
respond_fn: mi[:respond]
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Shorthand intent keys when unique
|
|
37
|
+
flat_methods.each do |mi|
|
|
38
|
+
handlers[mi[:intent]] = handlers["#{mi[:name]}/#{mi[:intent]}"] if intent_count[mi[:intent]] == 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Nested accessors: handler[:tempo][:charge]
|
|
42
|
+
flat_methods.each do |mi|
|
|
43
|
+
handlers[mi[:name]] ||= {}
|
|
44
|
+
handlers[mi[:name]][mi[:intent]] = handlers["#{mi[:name]}/#{mi[:intent]}"]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
{
|
|
48
|
+
methods: flat_methods,
|
|
49
|
+
realm: realm,
|
|
50
|
+
transport: transport,
|
|
51
|
+
handlers: handlers,
|
|
52
|
+
compose: ->(*entries) { compose_handlers(handlers, entries) }
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def call_handler(handler, method_key, options)
|
|
57
|
+
fn = handler[:handlers][method_key]
|
|
58
|
+
raise "No handler for \"#{method_key}\"." unless fn
|
|
59
|
+
|
|
60
|
+
fn.call(options)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def compose_handlers(handlers, entries)
|
|
64
|
+
raise "compose() requires at least one entry" if entries.empty?
|
|
65
|
+
|
|
66
|
+
configured = entries.map do |method_or_key, options|
|
|
67
|
+
key = if method_or_key.is_a?(String)
|
|
68
|
+
method_or_key
|
|
69
|
+
else
|
|
70
|
+
"#{method_or_key[:name]}/#{method_or_key[:intent]}"
|
|
71
|
+
end
|
|
72
|
+
handler_fn = handlers[key]
|
|
73
|
+
raise "No handler for \"#{key}\". Is this method in your methods array?" unless handler_fn
|
|
74
|
+
|
|
75
|
+
handler_fn.call(options)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
compose(*configured)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def compose(*configured_handlers)
|
|
82
|
+
raise "compose() requires at least one handler" if configured_handlers.empty?
|
|
83
|
+
|
|
84
|
+
->(env) {
|
|
85
|
+
# Try to extract a Payment credential
|
|
86
|
+
header = env["HTTP_AUTHORIZATION"]
|
|
87
|
+
payment_header = header ? Credential.extract_payment_scheme(header) : nil
|
|
88
|
+
|
|
89
|
+
if payment_header
|
|
90
|
+
# Parse credential to find method+intent for dispatch
|
|
91
|
+
credential = begin
|
|
92
|
+
Credential.deserialize(payment_header)
|
|
93
|
+
rescue StandardError
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if credential
|
|
98
|
+
cred_method = credential[:challenge][:method]
|
|
99
|
+
cred_intent = credential[:challenge][:intent]
|
|
100
|
+
cred_req = credential[:challenge][:request]
|
|
101
|
+
|
|
102
|
+
# Filter by name+intent, then narrow by request fields
|
|
103
|
+
candidates = configured_handlers.select do |h|
|
|
104
|
+
meta = h.respond_to?(:_internal) ? h._internal : (h.respond_to?(:[]) && h[:_internal])
|
|
105
|
+
next false unless meta && meta[:name] == cred_method && meta[:intent] == cred_intent
|
|
106
|
+
|
|
107
|
+
canonical = meta[:_canonical_request]
|
|
108
|
+
next true unless canonical
|
|
109
|
+
|
|
110
|
+
match = true
|
|
111
|
+
%w[amount currency recipient chainId].each do |field|
|
|
112
|
+
if canonical[field] && cred_req[field] && canonical[field].to_s != cred_req[field].to_s
|
|
113
|
+
match = false
|
|
114
|
+
break
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
match
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
match = candidates.first || configured_handlers.find do |h|
|
|
121
|
+
meta = h.respond_to?(:_internal) ? h._internal : (h.respond_to?(:[]) && h[:_internal])
|
|
122
|
+
meta && meta[:name] == cred_method && meta[:intent] == cred_intent
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
return match.call(env) if match
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Credential present but no match, dispatch to first handler
|
|
129
|
+
return configured_handlers.first.call(env)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# No credential: call all handlers and merge 402 challenges
|
|
133
|
+
results = configured_handlers.map { |h| h.call(env) }
|
|
134
|
+
|
|
135
|
+
merged_headers = {
|
|
136
|
+
"Cache-Control" => "no-store"
|
|
137
|
+
}
|
|
138
|
+
www_auth_parts = []
|
|
139
|
+
body = nil
|
|
140
|
+
content_type = nil
|
|
141
|
+
|
|
142
|
+
results.each do |result|
|
|
143
|
+
next unless result[:status] == 402
|
|
144
|
+
|
|
145
|
+
challenge_response = result[:challenge]
|
|
146
|
+
if challenge_response.is_a?(Array)
|
|
147
|
+
_status, headers, resp_body = challenge_response
|
|
148
|
+
www_auth = headers["WWW-Authenticate"]
|
|
149
|
+
www_auth_parts << www_auth if www_auth
|
|
150
|
+
unless body
|
|
151
|
+
content_type = headers["Content-Type"]
|
|
152
|
+
body = resp_body.is_a?(Array) ? resp_body.join : resp_body
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
merged_headers["WWW-Authenticate"] = www_auth_parts.join(", ") unless www_auth_parts.empty?
|
|
158
|
+
merged_headers["Content-Type"] = content_type if content_type
|
|
159
|
+
|
|
160
|
+
{
|
|
161
|
+
status: 402,
|
|
162
|
+
challenge: [402, merged_headers, body ? [body] : []]
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# @private
|
|
168
|
+
def create_method_fn(method:, realm:, secret_key:, transport:, verify:, defaults: nil, request_fn: nil, respond_fn: nil)
|
|
169
|
+
->(options) {
|
|
170
|
+
description = options[:description]
|
|
171
|
+
meta = options[:meta]
|
|
172
|
+
merged = (defaults || {}).merge(options.reject { |k, _| %i[description meta expires].include?(k) })
|
|
173
|
+
|
|
174
|
+
configured = ->(env) {
|
|
175
|
+
expires = options.key?(:expires) ? options[:expires] : Expires.minutes(5)
|
|
176
|
+
|
|
177
|
+
# Extract credential
|
|
178
|
+
credential = nil
|
|
179
|
+
credential_error = nil
|
|
180
|
+
begin
|
|
181
|
+
credential = transport[:get_credential].call(env)
|
|
182
|
+
rescue StandardError => e
|
|
183
|
+
credential_error = e
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Transform request if method provides a request function
|
|
187
|
+
request = if request_fn
|
|
188
|
+
request_fn.call(credential: credential, request: merged)
|
|
189
|
+
else
|
|
190
|
+
merged
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Recompute challenge from options (HMAC-bound, stateless)
|
|
194
|
+
challenge = Challenge.from_method(method, {
|
|
195
|
+
description: description,
|
|
196
|
+
expires: expires,
|
|
197
|
+
meta: meta,
|
|
198
|
+
realm: realm,
|
|
199
|
+
request: request,
|
|
200
|
+
secret_key: secret_key
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
# Credential was provided but malformed
|
|
204
|
+
if credential_error
|
|
205
|
+
response = transport[:respond_challenge].call(
|
|
206
|
+
challenge: challenge,
|
|
207
|
+
error: Errors::MalformedCredentialError.new(reason: credential_error.message)
|
|
208
|
+
)
|
|
209
|
+
next { status: 402, challenge: response }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# No credential provided
|
|
213
|
+
unless credential
|
|
214
|
+
response = transport[:respond_challenge].call(
|
|
215
|
+
challenge: challenge,
|
|
216
|
+
error: Errors::PaymentRequiredError.new(description: description)
|
|
217
|
+
)
|
|
218
|
+
next { status: 402, challenge: response }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Verify HMAC
|
|
222
|
+
unless Challenge.verify(credential[:challenge], secret_key: secret_key)
|
|
223
|
+
response = transport[:respond_challenge].call(
|
|
224
|
+
challenge: challenge,
|
|
225
|
+
error: Errors::InvalidChallengeError.new(
|
|
226
|
+
id: credential[:challenge][:id],
|
|
227
|
+
reason: "challenge was not issued by this server"
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
next { status: 402, challenge: response }
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Cross-route scope confusion check
|
|
234
|
+
route_req = challenge[:request]
|
|
235
|
+
echoed_req = credential[:challenge][:request]
|
|
236
|
+
scope_error = nil
|
|
237
|
+
%w[amount currency recipient].each do |field|
|
|
238
|
+
field_sym = field.to_sym
|
|
239
|
+
route_val = route_req[field_sym] || route_req[field]
|
|
240
|
+
echoed_val = echoed_req[field_sym] || echoed_req[field]
|
|
241
|
+
if route_val && echoed_val && route_val.to_s != echoed_val.to_s
|
|
242
|
+
scope_error = field
|
|
243
|
+
break
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
if scope_error
|
|
248
|
+
response = transport[:respond_challenge].call(
|
|
249
|
+
challenge: challenge,
|
|
250
|
+
error: Errors::InvalidChallengeError.new(
|
|
251
|
+
id: credential[:challenge][:id],
|
|
252
|
+
reason: "credential #{scope_error} does not match this route's requirements"
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
next { status: 402, challenge: response }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Reject expired credentials
|
|
259
|
+
if credential[:challenge][:expires] && Time.parse(credential[:challenge][:expires]) < Time.now
|
|
260
|
+
response = transport[:respond_challenge].call(
|
|
261
|
+
challenge: challenge,
|
|
262
|
+
error: Errors::PaymentExpiredError.new(expires: credential[:challenge][:expires])
|
|
263
|
+
)
|
|
264
|
+
next { status: 402, challenge: response }
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Validate payload schema
|
|
268
|
+
if method[:schema][:credential] && method[:schema][:credential][:payload]
|
|
269
|
+
begin
|
|
270
|
+
method[:schema][:credential][:payload].call(credential[:payload])
|
|
271
|
+
rescue StandardError => e
|
|
272
|
+
response = transport[:respond_challenge].call(
|
|
273
|
+
challenge: challenge,
|
|
274
|
+
error: Errors::InvalidPayloadError.new(reason: e.message)
|
|
275
|
+
)
|
|
276
|
+
next { status: 402, challenge: response }
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# User-provided verification
|
|
281
|
+
receipt_data = nil
|
|
282
|
+
begin
|
|
283
|
+
receipt_data = verify.call(credential: credential, request: request)
|
|
284
|
+
rescue Errors::PaymentError => e
|
|
285
|
+
response = transport[:respond_challenge].call(
|
|
286
|
+
challenge: challenge,
|
|
287
|
+
error: e
|
|
288
|
+
)
|
|
289
|
+
next { status: 402, challenge: response }
|
|
290
|
+
rescue StandardError => e
|
|
291
|
+
response = transport[:respond_challenge].call(
|
|
292
|
+
challenge: challenge,
|
|
293
|
+
error: Errors::VerificationFailedError.new(reason: e.message)
|
|
294
|
+
)
|
|
295
|
+
next { status: 402, challenge: response }
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Management response via respond hook
|
|
299
|
+
management_response = respond_fn ? respond_fn.call(
|
|
300
|
+
credential: credential,
|
|
301
|
+
receipt: receipt_data,
|
|
302
|
+
request: request
|
|
303
|
+
) : nil
|
|
304
|
+
|
|
305
|
+
{
|
|
306
|
+
status: 200,
|
|
307
|
+
with_receipt: ->(response = nil) {
|
|
308
|
+
if management_response
|
|
309
|
+
transport[:respond_receipt].call(
|
|
310
|
+
receipt: receipt_data,
|
|
311
|
+
response: management_response,
|
|
312
|
+
challenge_id: credential[:challenge][:id]
|
|
313
|
+
)
|
|
314
|
+
else
|
|
315
|
+
raise "with_receipt() requires a response argument" unless response
|
|
316
|
+
|
|
317
|
+
transport[:respond_receipt].call(
|
|
318
|
+
receipt: receipt_data,
|
|
319
|
+
response: response,
|
|
320
|
+
challenge_id: credential[:challenge][:id]
|
|
321
|
+
)
|
|
322
|
+
end
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
# Attach internal metadata for compose dispatch
|
|
328
|
+
configured.define_singleton_method(:_internal) do
|
|
329
|
+
{
|
|
330
|
+
name: method[:name],
|
|
331
|
+
intent: method[:intent],
|
|
332
|
+
_canonical_request: merged
|
|
333
|
+
}
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
configured.define_singleton_method(:[]) do |key|
|
|
337
|
+
return _internal if key == :_internal
|
|
338
|
+
super(key)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
configured
|
|
342
|
+
}
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Rack middleware
|
|
347
|
+
class Middleware
|
|
348
|
+
def initialize(app, handler:, method_key:, options:)
|
|
349
|
+
@app = app
|
|
350
|
+
@handler = handler
|
|
351
|
+
@method_key = method_key
|
|
352
|
+
@options = options
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def call(env)
|
|
356
|
+
fn = Handler.call_handler(@handler, @method_key, @options)
|
|
357
|
+
result = fn.call(env)
|
|
358
|
+
|
|
359
|
+
if result[:status] == 402
|
|
360
|
+
result[:challenge]
|
|
361
|
+
else
|
|
362
|
+
response = @app.call(env)
|
|
363
|
+
result[:with_receipt].call(response)
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Mppx
|
|
6
|
+
module Server
|
|
7
|
+
module Transport
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def http
|
|
11
|
+
{
|
|
12
|
+
name: "http",
|
|
13
|
+
|
|
14
|
+
get_credential: ->(env) {
|
|
15
|
+
header = env["HTTP_AUTHORIZATION"]
|
|
16
|
+
return nil unless header
|
|
17
|
+
|
|
18
|
+
payment = Credential.extract_payment_scheme(header)
|
|
19
|
+
return nil unless payment
|
|
20
|
+
|
|
21
|
+
Credential.deserialize(payment)
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
respond_challenge: ->(challenge:, error: nil, **_opts) {
|
|
25
|
+
headers = {
|
|
26
|
+
"WWW-Authenticate" => Challenge.serialize(challenge),
|
|
27
|
+
"Cache-Control" => "no-store"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
body = nil
|
|
31
|
+
if error
|
|
32
|
+
headers["Content-Type"] = "application/problem+json"
|
|
33
|
+
body = JSON.generate(error.to_problem_details(challenge[:id]))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
[error&.status || 402, headers, body ? [body] : []]
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
respond_receipt: ->(receipt:, response:, challenge_id: nil) {
|
|
40
|
+
status, headers, body = response
|
|
41
|
+
new_headers = headers.dup
|
|
42
|
+
new_headers["Payment-Receipt"] = Receipt.serialize(receipt)
|
|
43
|
+
[status, new_headers, body]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/mppx/store.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Mppx
|
|
6
|
+
module Store
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def memory
|
|
10
|
+
store = {}
|
|
11
|
+
from(
|
|
12
|
+
get: ->(key) { store.key?(key) ? JSON.parse(store[key]) : nil },
|
|
13
|
+
put: ->(key, value) { store[key] = JSON.generate(value) },
|
|
14
|
+
delete: ->(key) { store.delete(key) }
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def redis(client)
|
|
19
|
+
from(
|
|
20
|
+
get: ->(key) {
|
|
21
|
+
raw = client.get(key)
|
|
22
|
+
raw.nil? ? nil : JSON.parse(raw)
|
|
23
|
+
},
|
|
24
|
+
put: ->(key, value) { client.set(key, JSON.generate(value)) },
|
|
25
|
+
delete: ->(key) { client.del(key) }
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def from(store)
|
|
30
|
+
store
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/mppx/version.rb
ADDED
data/lib/mppx.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
require_relative "mppx/version"
|
|
6
|
+
require_relative "mppx/base64url"
|
|
7
|
+
require_relative "mppx/canonical_json"
|
|
8
|
+
require_relative "mppx/constant_time_equal"
|
|
9
|
+
require_relative "mppx/env"
|
|
10
|
+
require_relative "mppx/errors"
|
|
11
|
+
require_relative "mppx/expires"
|
|
12
|
+
require_relative "mppx/body_digest"
|
|
13
|
+
require_relative "mppx/payment_request"
|
|
14
|
+
require_relative "mppx/challenge"
|
|
15
|
+
require_relative "mppx/credential"
|
|
16
|
+
require_relative "mppx/receipt"
|
|
17
|
+
require_relative "mppx/method"
|
|
18
|
+
require_relative "mppx/store"
|
|
19
|
+
require_relative "mppx/mcp"
|
|
20
|
+
require_relative "mppx/server/transport"
|
|
21
|
+
require_relative "mppx/server/handler"
|
|
22
|
+
|
|
23
|
+
module Mppx
|
|
24
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mppx
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- CJ Avilla
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: base64
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rspec
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.12'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.12'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '13.0'
|
|
54
|
+
description: Implements the Payment HTTP Authentication Scheme (HTTP 402) for payment-gated
|
|
55
|
+
APIs.
|
|
56
|
+
email:
|
|
57
|
+
- wave@cjav.dev
|
|
58
|
+
executables: []
|
|
59
|
+
extensions: []
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
files:
|
|
62
|
+
- LICENSE
|
|
63
|
+
- README.md
|
|
64
|
+
- lib/mppx.rb
|
|
65
|
+
- lib/mppx/base64url.rb
|
|
66
|
+
- lib/mppx/body_digest.rb
|
|
67
|
+
- lib/mppx/canonical_json.rb
|
|
68
|
+
- lib/mppx/challenge.rb
|
|
69
|
+
- lib/mppx/constant_time_equal.rb
|
|
70
|
+
- lib/mppx/credential.rb
|
|
71
|
+
- lib/mppx/env.rb
|
|
72
|
+
- lib/mppx/errors.rb
|
|
73
|
+
- lib/mppx/expires.rb
|
|
74
|
+
- lib/mppx/mcp.rb
|
|
75
|
+
- lib/mppx/method.rb
|
|
76
|
+
- lib/mppx/payment_request.rb
|
|
77
|
+
- lib/mppx/receipt.rb
|
|
78
|
+
- lib/mppx/server/handler.rb
|
|
79
|
+
- lib/mppx/server/transport.rb
|
|
80
|
+
- lib/mppx/store.rb
|
|
81
|
+
- lib/mppx/version.rb
|
|
82
|
+
homepage: https://github.com/cjavdev/mppx-rb
|
|
83
|
+
licenses:
|
|
84
|
+
- MIT
|
|
85
|
+
metadata:
|
|
86
|
+
homepage_uri: https://github.com/cjavdev/mppx-rb
|
|
87
|
+
source_code_uri: https://github.com/cjavdev/mppx-rb
|
|
88
|
+
rubygems_mfa_required: 'true'
|
|
89
|
+
rdoc_options: []
|
|
90
|
+
require_paths:
|
|
91
|
+
- lib
|
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '3.1'
|
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '0'
|
|
102
|
+
requirements: []
|
|
103
|
+
rubygems_version: 3.7.2
|
|
104
|
+
specification_version: 4
|
|
105
|
+
summary: Ruby implementation of the Machine Payments Protocol SDK
|
|
106
|
+
test_files: []
|