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
data/lib/mppx/errors.rb
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mppx
|
|
4
|
+
module Errors
|
|
5
|
+
class PaymentError < StandardError
|
|
6
|
+
def type
|
|
7
|
+
raise NotImplementedError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def title
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def status
|
|
15
|
+
402
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_problem_details(challenge_id = nil)
|
|
19
|
+
result = {
|
|
20
|
+
type: type,
|
|
21
|
+
title: title,
|
|
22
|
+
status: status,
|
|
23
|
+
detail: message
|
|
24
|
+
}
|
|
25
|
+
result[:challengeId] = challenge_id if challenge_id
|
|
26
|
+
result
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class MalformedCredentialError < PaymentError
|
|
31
|
+
def initialize(reason: nil)
|
|
32
|
+
msg = reason ? "Credential is malformed: #{reason}." : "Credential is malformed."
|
|
33
|
+
super(msg)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def type = "https://paymentauth.org/problems/malformed-credential"
|
|
37
|
+
def title = "Malformed Credential"
|
|
38
|
+
def status = 402
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class InvalidChallengeError < PaymentError
|
|
42
|
+
def initialize(id: nil, reason: nil)
|
|
43
|
+
id_part = id ? " \"#{id}\"" : ""
|
|
44
|
+
reason_part = reason ? ": #{reason}" : ""
|
|
45
|
+
super("Challenge#{id_part} is invalid#{reason_part}.")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def type = "https://paymentauth.org/problems/invalid-challenge"
|
|
49
|
+
def title = "Invalid Challenge"
|
|
50
|
+
def status = 402
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class VerificationFailedError < PaymentError
|
|
54
|
+
def initialize(reason: nil)
|
|
55
|
+
msg = reason ? "Payment verification failed: #{reason}." : "Payment verification failed."
|
|
56
|
+
super(msg)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def type = "https://paymentauth.org/problems/verification-failed"
|
|
60
|
+
def title = "Verification Failed"
|
|
61
|
+
def status = 402
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class PaymentActionRequiredError < PaymentError
|
|
65
|
+
def initialize(reason: nil)
|
|
66
|
+
msg = reason ? "Payment requires action: #{reason}." : "Payment requires action."
|
|
67
|
+
super(msg)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def type = "https://paymentauth.org/problems/payment-action-required"
|
|
71
|
+
def title = "Payment Action Required"
|
|
72
|
+
def status = 402
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class PaymentExpiredError < PaymentError
|
|
76
|
+
def initialize(expires: nil)
|
|
77
|
+
msg = expires ? "Payment expired at #{expires}." : "Payment has expired."
|
|
78
|
+
super(msg)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def type = "https://paymentauth.org/problems/payment-expired"
|
|
82
|
+
def title = "Payment Expired"
|
|
83
|
+
def status = 402
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class PaymentRequiredError < PaymentError
|
|
87
|
+
def initialize(description: nil)
|
|
88
|
+
parts = ["Payment is required"]
|
|
89
|
+
parts << "(#{description})" if description
|
|
90
|
+
super("#{parts.join(" ")}.")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def type = "https://paymentauth.org/problems/payment-required"
|
|
94
|
+
def title = "Payment Required"
|
|
95
|
+
def status = 402
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
class InvalidPayloadError < PaymentError
|
|
99
|
+
def initialize(reason: nil)
|
|
100
|
+
msg = reason ? "Credential payload is invalid: #{reason}." : "Credential payload is invalid."
|
|
101
|
+
super(msg)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def type = "https://paymentauth.org/problems/invalid-payload"
|
|
105
|
+
def title = "Invalid Payload"
|
|
106
|
+
def status = 402
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class BadRequestError < PaymentError
|
|
110
|
+
def initialize(reason: nil)
|
|
111
|
+
msg = reason ? "Bad request: #{reason}." : "Bad request."
|
|
112
|
+
super(msg)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def type = "https://paymentauth.org/problems/bad-request"
|
|
116
|
+
def title = "Bad Request"
|
|
117
|
+
def status = 400
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
class PaymentInsufficientError < PaymentError
|
|
121
|
+
def initialize(reason: nil)
|
|
122
|
+
msg = reason ? "Payment insufficient: #{reason}." : "Payment amount is insufficient."
|
|
123
|
+
super(msg)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def type = "https://paymentauth.org/problems/payment-insufficient"
|
|
127
|
+
def title = "Payment Insufficient"
|
|
128
|
+
def status = 402
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class PaymentMethodUnsupportedError < PaymentError
|
|
132
|
+
def initialize(method: nil)
|
|
133
|
+
msg = method ? "Payment method \"#{method}\" is not supported." : "Payment method is not supported."
|
|
134
|
+
super(msg)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def type = "https://paymentauth.org/problems/method-unsupported"
|
|
138
|
+
def title = "Method Unsupported"
|
|
139
|
+
def status = 400
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
class InsufficientBalanceError < PaymentError
|
|
143
|
+
def initialize(reason: nil)
|
|
144
|
+
msg = reason ? "Insufficient balance: #{reason}." : "Insufficient balance."
|
|
145
|
+
super(msg)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def type = "https://paymentauth.org/problems/session/insufficient-balance"
|
|
149
|
+
def title = "Insufficient Balance"
|
|
150
|
+
def status = 402
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
class InvalidSignatureError < PaymentError
|
|
154
|
+
def initialize(reason: nil)
|
|
155
|
+
msg = reason ? "Invalid signature: #{reason}." : "Invalid signature."
|
|
156
|
+
super(msg)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def type = "https://paymentauth.org/problems/session/invalid-signature"
|
|
160
|
+
def title = "Invalid Signature"
|
|
161
|
+
def status = 402
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
class SignerMismatchError < PaymentError
|
|
165
|
+
def initialize(reason: nil)
|
|
166
|
+
msg = reason ? "Signer mismatch: #{reason}." : "Signer is not authorized for this channel."
|
|
167
|
+
super(msg)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def type = "https://paymentauth.org/problems/session/signer-mismatch"
|
|
171
|
+
def title = "Signer Mismatch"
|
|
172
|
+
def status = 402
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
class AmountExceedsDepositError < PaymentError
|
|
176
|
+
def initialize(reason: nil)
|
|
177
|
+
msg = reason ? "Amount exceeds deposit: #{reason}." : "Voucher amount exceeds channel deposit."
|
|
178
|
+
super(msg)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def type = "https://paymentauth.org/problems/session/amount-exceeds-deposit"
|
|
182
|
+
def title = "Amount Exceeds Deposit"
|
|
183
|
+
def status = 402
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
class DeltaTooSmallError < PaymentError
|
|
187
|
+
def initialize(reason: nil)
|
|
188
|
+
msg = reason ? "Delta too small: #{reason}." : "Amount increase below minimum voucher delta."
|
|
189
|
+
super(msg)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def type = "https://paymentauth.org/problems/session/delta-too-small"
|
|
193
|
+
def title = "Delta Too Small"
|
|
194
|
+
def status = 402
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
class ChannelNotFoundError < PaymentError
|
|
198
|
+
def initialize(reason: nil)
|
|
199
|
+
msg = reason ? "Channel not found: #{reason}." : "No channel with this ID exists."
|
|
200
|
+
super(msg)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def type = "https://paymentauth.org/problems/session/channel-not-found"
|
|
204
|
+
def title = "Channel Not Found"
|
|
205
|
+
def status = 410
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
class ChannelClosedError < PaymentError
|
|
209
|
+
def initialize(reason: nil)
|
|
210
|
+
msg = reason ? "Channel closed: #{reason}." : "Channel is closed."
|
|
211
|
+
super(msg)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def type = "https://paymentauth.org/problems/session/channel-finalized"
|
|
215
|
+
def title = "Channel Closed"
|
|
216
|
+
def status = 410
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
data/lib/mppx/expires.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mppx
|
|
4
|
+
module Expires
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def seconds(n)
|
|
8
|
+
format_time(Time.now.utc + n)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def minutes(n)
|
|
12
|
+
format_time(Time.now.utc + (n * 60))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def hours(n)
|
|
16
|
+
format_time(Time.now.utc + (n * 3600))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def days(n)
|
|
20
|
+
format_time(Time.now.utc + (n * 86_400))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def weeks(n)
|
|
24
|
+
format_time(Time.now.utc + (n * 7 * 86_400))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def months(n)
|
|
28
|
+
format_time(Time.now.utc + (n * 30 * 86_400))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def years(n)
|
|
32
|
+
format_time(Time.now.utc + (n * 365 * 86_400))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def format_time(time)
|
|
36
|
+
time.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/mppx/mcp.rb
ADDED
data/lib/mppx/method.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mppx
|
|
4
|
+
module Method
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def from(name:, intent:, schema:)
|
|
8
|
+
{
|
|
9
|
+
name: name,
|
|
10
|
+
intent: intent,
|
|
11
|
+
schema: schema
|
|
12
|
+
}.freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_client(method, create_credential:)
|
|
16
|
+
method.merge(create_credential: create_credential).freeze
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_server(method, verify:, defaults: nil, request: nil, respond: nil)
|
|
20
|
+
result = method.merge(verify: verify)
|
|
21
|
+
result[:defaults] = defaults if defaults
|
|
22
|
+
result[:request] = request if request
|
|
23
|
+
result[:respond] = respond if respond
|
|
24
|
+
result.freeze
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Mppx
|
|
6
|
+
module PaymentRequest
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def from(request)
|
|
10
|
+
request
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def from_method(method, request)
|
|
14
|
+
schema = method[:schema][:request]
|
|
15
|
+
schema.call(request)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def serialize(request)
|
|
19
|
+
json = CanonicalJson.canonicalize(request)
|
|
20
|
+
Base64Url.encode(json)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def deserialize(encoded)
|
|
24
|
+
json = Base64Url.decode(encoded)
|
|
25
|
+
JSON.parse(json)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/mppx/receipt.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Mppx
|
|
6
|
+
module Receipt
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def from(method:, reference:, status: "success", timestamp:, external_id: nil)
|
|
10
|
+
raise ArgumentError, "status must be 'success'" unless status == "success"
|
|
11
|
+
|
|
12
|
+
receipt = {
|
|
13
|
+
method: method,
|
|
14
|
+
reference: reference,
|
|
15
|
+
status: status,
|
|
16
|
+
timestamp: timestamp
|
|
17
|
+
}
|
|
18
|
+
receipt[:externalId] = external_id if external_id
|
|
19
|
+
receipt
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def serialize(receipt)
|
|
23
|
+
json = JSON.generate(receipt)
|
|
24
|
+
Base64Url.encode(json)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def deserialize(encoded)
|
|
28
|
+
json = Base64Url.decode(encoded)
|
|
29
|
+
parsed = JSON.parse(json)
|
|
30
|
+
{
|
|
31
|
+
method: parsed["method"],
|
|
32
|
+
reference: parsed["reference"],
|
|
33
|
+
status: parsed["status"],
|
|
34
|
+
timestamp: parsed["timestamp"]
|
|
35
|
+
}.tap do |r|
|
|
36
|
+
r[:externalId] = parsed["externalId"] if parsed["externalId"]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def from_response(headers)
|
|
41
|
+
header = headers["Payment-Receipt"] || headers["payment-receipt"]
|
|
42
|
+
raise "Missing Payment-Receipt header." unless header
|
|
43
|
+
|
|
44
|
+
deserialize(header)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|