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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +133 -0
- data/lib/mpp/body_digest.rb +37 -0
- data/lib/mpp/challenge.rb +115 -0
- data/lib/mpp/challenge_echo.rb +19 -0
- data/lib/mpp/challenge_id.rb +54 -0
- data/lib/mpp/client/transport.rb +137 -0
- data/lib/mpp/client.rb +9 -0
- data/lib/mpp/credential.rb +20 -0
- data/lib/mpp/errors.rb +190 -0
- data/lib/mpp/expires.rb +60 -0
- data/lib/mpp/extensions/mcp/capabilities.rb +23 -0
- data/lib/mpp/extensions/mcp/constants.rb +17 -0
- data/lib/mpp/extensions/mcp/decorator.rb +44 -0
- data/lib/mpp/extensions/mcp/errors.rb +110 -0
- data/lib/mpp/extensions/mcp/types.rb +205 -0
- data/lib/mpp/extensions/mcp/verify.rb +152 -0
- data/lib/mpp/extensions/mcp.rb +16 -0
- data/lib/mpp/json.rb +32 -0
- data/lib/mpp/methods/stripe/charge_intent.rb +90 -0
- data/lib/mpp/methods/stripe/client_method.rb +42 -0
- data/lib/mpp/methods/stripe/defaults.rb +14 -0
- data/lib/mpp/methods/stripe/stripe_method.rb +63 -0
- data/lib/mpp/methods/stripe.rb +14 -0
- data/lib/mpp/methods/tempo/account.rb +52 -0
- data/lib/mpp/methods/tempo/attribution.rb +112 -0
- data/lib/mpp/methods/tempo/client_method.rb +259 -0
- data/lib/mpp/methods/tempo/defaults.rb +77 -0
- data/lib/mpp/methods/tempo/fee_payer_envelope.rb +74 -0
- data/lib/mpp/methods/tempo/intents.rb +377 -0
- data/lib/mpp/methods/tempo/keychain.rb +31 -0
- data/lib/mpp/methods/tempo/proof.rb +127 -0
- data/lib/mpp/methods/tempo/rpc.rb +60 -0
- data/lib/mpp/methods/tempo/schemas.rb +96 -0
- data/lib/mpp/methods/tempo/transaction.rb +144 -0
- data/lib/mpp/methods/tempo.rb +22 -0
- data/lib/mpp/parsing.rb +252 -0
- data/lib/mpp/receipt.rb +31 -0
- data/lib/mpp/secure_compare.rb +25 -0
- data/lib/mpp/server/decorator.rb +32 -0
- data/lib/mpp/server/defaults.rb +45 -0
- data/lib/mpp/server/intent.rb +40 -0
- data/lib/mpp/server/method.rb +27 -0
- data/lib/mpp/server/middleware.rb +51 -0
- data/lib/mpp/server/mpp_handler.rb +97 -0
- data/lib/mpp/server/verify.rb +129 -0
- data/lib/mpp/server.rb +15 -0
- data/lib/mpp/store.rb +49 -0
- data/lib/mpp/units.rb +57 -0
- data/lib/mpp/version.rb +6 -0
- data/lib/mpp-rb.rb +3 -0
- data/lib/mpp.rb +68 -0
- metadata +111 -0
data/lib/mpp/errors.rb
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Mpp
|
|
5
|
+
BASE_URI = "https://paymentauth.org/problems"
|
|
6
|
+
|
|
7
|
+
# Parse error for malformed payment headers.
|
|
8
|
+
class ParseError < StandardError; end
|
|
9
|
+
|
|
10
|
+
# Base verification error.
|
|
11
|
+
class VerificationError < StandardError; end
|
|
12
|
+
|
|
13
|
+
# Base class for all payment-related errors with RFC 9457 support.
|
|
14
|
+
class PaymentError < StandardError
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
sig { params(subclass: T::Class[T.anything]).returns(T.untyped) }
|
|
18
|
+
def self.inherited(subclass)
|
|
19
|
+
super
|
|
20
|
+
return if subclass.instance_variable_defined?(:@_mpp_configured)
|
|
21
|
+
|
|
22
|
+
subclass.instance_variable_set(:@_mpp_configured, true)
|
|
23
|
+
name = subclass.name&.split("::")&.last || "PaymentError"
|
|
24
|
+
unless subclass.instance_variable_defined?(:@type)
|
|
25
|
+
subclass.instance_variable_set(:@type,
|
|
26
|
+
"#{BASE_URI}/#{to_slug(name)}")
|
|
27
|
+
end
|
|
28
|
+
subclass.instance_variable_set(:@title, to_title(name)) unless subclass.instance_variable_defined?(:@title)
|
|
29
|
+
subclass.instance_variable_set(:@status, 402) unless subclass.instance_variable_defined?(:@status)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
extend T::Sig
|
|
34
|
+
|
|
35
|
+
sig { returns(T.nilable(Integer)) }
|
|
36
|
+
attr_reader :status
|
|
37
|
+
|
|
38
|
+
sig { returns(T.nilable(String)) }
|
|
39
|
+
attr_reader :type
|
|
40
|
+
|
|
41
|
+
sig { returns(T.nilable(String)) }
|
|
42
|
+
attr_reader :title
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
sig { params(name: String).returns(String) }
|
|
47
|
+
def to_slug(name)
|
|
48
|
+
name.sub(/Error$/, "").gsub(/(?<=[a-z0-9])(?=[A-Z])/, "-").downcase
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
sig { params(name: String).returns(String) }
|
|
52
|
+
def to_title(name)
|
|
53
|
+
name.sub(/Error$/, "").gsub(/(?<=[a-z0-9])(?=[A-Z])/, " ")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@status = T.let(402, Integer)
|
|
58
|
+
@type = T.let("#{BASE_URI}/payment-error", String)
|
|
59
|
+
@title = T.let("Payment Error", String)
|
|
60
|
+
|
|
61
|
+
sig { returns(T.untyped) }
|
|
62
|
+
def status = self.class.status
|
|
63
|
+
sig { returns(T.untyped) }
|
|
64
|
+
def type = self.class.type
|
|
65
|
+
sig { returns(T.untyped) }
|
|
66
|
+
def title = self.class.title
|
|
67
|
+
|
|
68
|
+
# Convert to RFC 9457 Problem Details format.
|
|
69
|
+
sig { params(challenge_id: T.untyped).returns(T::Hash[T.untyped, T.untyped]) }
|
|
70
|
+
def to_problem_details(challenge_id: nil)
|
|
71
|
+
details = {
|
|
72
|
+
"type" => type,
|
|
73
|
+
"title" => title,
|
|
74
|
+
"status" => status,
|
|
75
|
+
"detail" => message
|
|
76
|
+
}
|
|
77
|
+
details["challengeId"] = challenge_id if challenge_id
|
|
78
|
+
details
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class PaymentRequiredError < PaymentError
|
|
83
|
+
extend T::Sig
|
|
84
|
+
|
|
85
|
+
sig { params(realm: T.untyped, description: T.untyped).void }
|
|
86
|
+
def initialize(realm: nil, description: nil)
|
|
87
|
+
parts = ["Payment is required"]
|
|
88
|
+
parts << "for \"#{realm}\"" if realm
|
|
89
|
+
parts << "(#{description})" if description
|
|
90
|
+
super("#{parts.join(" ")}.")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
class MalformedCredentialError < PaymentError
|
|
95
|
+
extend T::Sig
|
|
96
|
+
|
|
97
|
+
sig { params(reason: T.untyped).void }
|
|
98
|
+
def initialize(reason: nil)
|
|
99
|
+
msg = reason ? "Credential is malformed: #{reason}." : "Credential is malformed."
|
|
100
|
+
super(msg)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class InvalidChallengeError < PaymentError
|
|
105
|
+
extend T::Sig
|
|
106
|
+
|
|
107
|
+
sig { params(challenge_id: T.untyped, reason: T.untyped).void }
|
|
108
|
+
def initialize(challenge_id: nil, reason: nil)
|
|
109
|
+
id_part = challenge_id ? " \"#{challenge_id}\"" : ""
|
|
110
|
+
reason_part = reason ? ": #{reason}" : ""
|
|
111
|
+
super("Challenge#{id_part} is invalid#{reason_part}.")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
class VerificationFailedError < PaymentError
|
|
116
|
+
extend T::Sig
|
|
117
|
+
|
|
118
|
+
sig { params(reason: T.untyped).void }
|
|
119
|
+
def initialize(reason: nil)
|
|
120
|
+
msg = reason ? "Payment verification failed: #{reason}." : "Payment verification failed."
|
|
121
|
+
super(msg)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
class PaymentExpiredError < PaymentError
|
|
126
|
+
extend T::Sig
|
|
127
|
+
|
|
128
|
+
sig { params(expires: T.untyped).void }
|
|
129
|
+
def initialize(expires: nil)
|
|
130
|
+
msg = expires ? "Payment expired at #{expires}." : "Payment has expired."
|
|
131
|
+
super(msg)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
class InvalidPayloadError < PaymentError
|
|
136
|
+
extend T::Sig
|
|
137
|
+
|
|
138
|
+
sig { params(reason: T.untyped).void }
|
|
139
|
+
def initialize(reason: nil)
|
|
140
|
+
msg = reason ? "Credential payload is invalid: #{reason}." : "Credential payload is invalid."
|
|
141
|
+
super(msg)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
class BadRequestError < PaymentError
|
|
146
|
+
extend T::Sig
|
|
147
|
+
|
|
148
|
+
@status = T.let(400, Integer)
|
|
149
|
+
|
|
150
|
+
sig { params(reason: T.untyped).void }
|
|
151
|
+
def initialize(reason: nil)
|
|
152
|
+
msg = reason ? "Bad request: #{reason}." : "Bad request."
|
|
153
|
+
super(msg)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
class PaymentInsufficientError < PaymentError
|
|
158
|
+
extend T::Sig
|
|
159
|
+
|
|
160
|
+
sig { params(reason: T.untyped).void }
|
|
161
|
+
def initialize(reason: nil)
|
|
162
|
+
msg = reason ? "Payment insufficient: #{reason}." : "Payment amount is insufficient."
|
|
163
|
+
super(msg)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
class PaymentMethodUnsupportedError < PaymentError
|
|
168
|
+
extend T::Sig
|
|
169
|
+
|
|
170
|
+
@status = T.let(400, Integer)
|
|
171
|
+
@type = T.let("#{BASE_URI}/method-unsupported", String)
|
|
172
|
+
@title = T.let("Method Unsupported", String)
|
|
173
|
+
|
|
174
|
+
sig { params(method: T.untyped).void }
|
|
175
|
+
def initialize(method: nil)
|
|
176
|
+
msg = method ? "Payment method \"#{method}\" is not supported." : "Payment method is not supported."
|
|
177
|
+
super(msg)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
class PaymentActionRequiredError < PaymentError
|
|
182
|
+
extend T::Sig
|
|
183
|
+
|
|
184
|
+
sig { params(reason: T.untyped).void }
|
|
185
|
+
def initialize(reason: nil)
|
|
186
|
+
msg = reason ? "Payment requires action: #{reason}." : "Payment requires action."
|
|
187
|
+
super(msg)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
data/lib/mpp/expires.rb
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Mpp
|
|
7
|
+
module Expires
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Format a Time as ISO 8601 with Z suffix and millisecond precision.
|
|
13
|
+
sig { params(time: Time).returns(String) }
|
|
14
|
+
def to_iso(time)
|
|
15
|
+
time.utc.strftime("%Y-%m-%dT%H:%M:%S.%LZ")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Returns an ISO 8601 datetime string n seconds from now.
|
|
19
|
+
sig { params(n: BasicObject).returns(T.untyped) }
|
|
20
|
+
def seconds(n)
|
|
21
|
+
to_iso(Time.now.utc + n)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Returns an ISO 8601 datetime string n minutes from now.
|
|
25
|
+
sig { params(n: Numeric).returns(String) }
|
|
26
|
+
def minutes(n)
|
|
27
|
+
to_iso(Time.now.utc + (n * 60))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns an ISO 8601 datetime string n hours from now.
|
|
31
|
+
sig { params(n: Numeric).returns(String) }
|
|
32
|
+
def hours(n)
|
|
33
|
+
to_iso(Time.now.utc + (n * 3600))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns an ISO 8601 datetime string n days from now.
|
|
37
|
+
sig { params(n: Numeric).returns(String) }
|
|
38
|
+
def days(n)
|
|
39
|
+
to_iso(Time.now.utc + (n * 86_400))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns an ISO 8601 datetime string n weeks from now.
|
|
43
|
+
sig { params(n: Numeric).returns(String) }
|
|
44
|
+
def weeks(n)
|
|
45
|
+
to_iso(Time.now.utc + (n * 7 * 86_400))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns an ISO 8601 datetime string n months (30 days) from now.
|
|
49
|
+
sig { params(n: Numeric).returns(String) }
|
|
50
|
+
def months(n)
|
|
51
|
+
to_iso(Time.now.utc + (n * 30 * 86_400))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns an ISO 8601 datetime string n years (365 days) from now.
|
|
55
|
+
sig { params(n: Numeric).returns(String) }
|
|
56
|
+
def years(n)
|
|
57
|
+
to_iso(Time.now.utc + (n * 365 * 86_400))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Mpp
|
|
5
|
+
module Extensions
|
|
6
|
+
module MCP
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Build payment capabilities object for MCP.
|
|
12
|
+
sig { params(methods: T.untyped, intents: T.untyped).returns(T::Hash[T.untyped, T.untyped]) }
|
|
13
|
+
def payment_capabilities(methods, intents)
|
|
14
|
+
{
|
|
15
|
+
"payment" => {
|
|
16
|
+
"methods" => methods,
|
|
17
|
+
"intents" => intents
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Mpp
|
|
5
|
+
module Extensions
|
|
6
|
+
module MCP
|
|
7
|
+
META_CREDENTIAL = "org.paymentauth/credential"
|
|
8
|
+
META_RECEIPT = "org.paymentauth/receipt"
|
|
9
|
+
|
|
10
|
+
CODE_PAYMENT_REQUIRED = -32_042
|
|
11
|
+
CODE_PAYMENT_VERIFICATION_FAILED = -32_043
|
|
12
|
+
CODE_MALFORMED_CREDENTIAL = -32_602
|
|
13
|
+
|
|
14
|
+
HTTP_STATUS_PAYMENT_REQUIRED = 402
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Mpp
|
|
5
|
+
module Extensions
|
|
6
|
+
module MCP
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Wrapper for MCP tool handlers with payment verification.
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# result = Mpp::Extensions::MCP.pay(mpp_handler, meta: params["_meta"],
|
|
15
|
+
# request: { "amount" => "1000" }, realm: "api.example.com") do |credential, receipt|
|
|
16
|
+
# # execute tool
|
|
17
|
+
# end
|
|
18
|
+
sig { params(intent: T.untyped, request: T.untyped, meta: T.untyped, realm: T.nilable(String), secret_key: T.nilable(String), method: T.nilable(String), expires_in: Integer, description: T.nilable(String), blk: T.untyped).returns(T.untyped) }
|
|
19
|
+
def pay_tool(intent:, request:, meta:, realm: nil, secret_key: nil,
|
|
20
|
+
method: nil, expires_in: DEFAULT_CHALLENGE_TTL, description: nil, &blk)
|
|
21
|
+
resolved_realm = realm || Mpp::Server::Defaults.detect_realm
|
|
22
|
+
resolved_secret_key = secret_key || Mpp::Server::Defaults.detect_secret_key
|
|
23
|
+
|
|
24
|
+
request_params = request.respond_to?(:call) ? request.call : request
|
|
25
|
+
|
|
26
|
+
result = verify_or_challenge(
|
|
27
|
+
meta: meta,
|
|
28
|
+
intent: intent,
|
|
29
|
+
request: request_params,
|
|
30
|
+
realm: resolved_realm,
|
|
31
|
+
secret_key: resolved_secret_key,
|
|
32
|
+
method: method,
|
|
33
|
+
expires_in: expires_in,
|
|
34
|
+
description: description
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
Kernel.raise PaymentRequiredError.new(challenges: [result]) if result.is_a?(MCPChallenge)
|
|
38
|
+
|
|
39
|
+
credential, receipt = result
|
|
40
|
+
yield credential, receipt
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Mpp
|
|
5
|
+
module Extensions
|
|
6
|
+
module MCP
|
|
7
|
+
class PaymentRequiredError < StandardError
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig { returns(T.untyped) }
|
|
11
|
+
attr_reader :challenges
|
|
12
|
+
|
|
13
|
+
sig { returns(Integer) }
|
|
14
|
+
attr_reader :code
|
|
15
|
+
|
|
16
|
+
sig { params(challenges: T.untyped, message: BasicObject).void }
|
|
17
|
+
def initialize(challenges:, message: "Payment Required")
|
|
18
|
+
@challenges = T.let(challenges, T.untyped)
|
|
19
|
+
@code = T.let(CODE_PAYMENT_REQUIRED, Integer)
|
|
20
|
+
super(message)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
sig { returns(T::Hash[T.untyped, T.untyped]) }
|
|
24
|
+
def to_jsonrpc_error
|
|
25
|
+
{
|
|
26
|
+
"code" => CODE_PAYMENT_REQUIRED,
|
|
27
|
+
"message" => message,
|
|
28
|
+
"data" => {
|
|
29
|
+
"httpStatus" => HTTP_STATUS_PAYMENT_REQUIRED,
|
|
30
|
+
"challenges" => @challenges.map(&:to_dict)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class PaymentVerificationError < StandardError
|
|
37
|
+
extend T::Sig
|
|
38
|
+
|
|
39
|
+
sig { returns(T.untyped) }
|
|
40
|
+
attr_reader :challenges
|
|
41
|
+
|
|
42
|
+
sig { returns(T.untyped) }
|
|
43
|
+
attr_reader :reason
|
|
44
|
+
|
|
45
|
+
sig { returns(T.untyped) }
|
|
46
|
+
attr_reader :detail
|
|
47
|
+
|
|
48
|
+
sig { returns(Integer) }
|
|
49
|
+
attr_reader :code
|
|
50
|
+
|
|
51
|
+
sig { params(challenges: T.untyped, reason: T.untyped, detail: T.untyped, message: BasicObject).void }
|
|
52
|
+
def initialize(challenges:, reason: nil, detail: nil, message: "Payment Verification Failed")
|
|
53
|
+
@challenges = T.let(challenges, T.untyped)
|
|
54
|
+
@reason = T.let(reason, T.untyped)
|
|
55
|
+
@detail = T.let(detail, T.untyped)
|
|
56
|
+
@code = T.let(CODE_PAYMENT_VERIFICATION_FAILED, Integer)
|
|
57
|
+
super(message)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
sig { returns(T::Hash[T.untyped, T.untyped]) }
|
|
61
|
+
def to_jsonrpc_error
|
|
62
|
+
data = {
|
|
63
|
+
"httpStatus" => HTTP_STATUS_PAYMENT_REQUIRED,
|
|
64
|
+
"challenges" => @challenges.map(&:to_dict)
|
|
65
|
+
}
|
|
66
|
+
if @reason || @detail
|
|
67
|
+
failure = {}
|
|
68
|
+
failure["reason"] = @reason if @reason
|
|
69
|
+
failure["detail"] = @detail if @detail
|
|
70
|
+
data["failure"] = failure
|
|
71
|
+
end
|
|
72
|
+
{
|
|
73
|
+
"code" => CODE_PAYMENT_VERIFICATION_FAILED,
|
|
74
|
+
"message" => message,
|
|
75
|
+
"data" => data
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class MalformedCredentialError < StandardError
|
|
81
|
+
extend T::Sig
|
|
82
|
+
|
|
83
|
+
sig { returns(T.untyped) }
|
|
84
|
+
attr_reader :detail
|
|
85
|
+
|
|
86
|
+
sig { returns(Integer) }
|
|
87
|
+
attr_reader :code
|
|
88
|
+
|
|
89
|
+
sig { params(detail: T.untyped, message: BasicObject).void }
|
|
90
|
+
def initialize(detail:, message: "Invalid params")
|
|
91
|
+
@detail = T.let(detail, T.untyped)
|
|
92
|
+
@code = T.let(CODE_MALFORMED_CREDENTIAL, Integer)
|
|
93
|
+
super(message)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
sig { returns(T::Hash[T.untyped, T.untyped]) }
|
|
97
|
+
def to_jsonrpc_error
|
|
98
|
+
{
|
|
99
|
+
"code" => CODE_MALFORMED_CREDENTIAL,
|
|
100
|
+
"message" => message,
|
|
101
|
+
"data" => {
|
|
102
|
+
"httpStatus" => HTTP_STATUS_PAYMENT_REQUIRED,
|
|
103
|
+
"detail" => @detail
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "base64"
|
|
5
|
+
require "json"
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
module Mpp
|
|
9
|
+
module Extensions
|
|
10
|
+
module MCP
|
|
11
|
+
MCPChallenge = Data.define(:id, :realm, :method, :intent, :request,
|
|
12
|
+
:expires, :description, :digest, :opaque) do
|
|
13
|
+
def initialize(id:, realm:, method:, intent:, request:,
|
|
14
|
+
expires: nil, description: nil, digest: nil, opaque: nil)
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_dict
|
|
19
|
+
result = {
|
|
20
|
+
"id" => id,
|
|
21
|
+
"realm" => realm,
|
|
22
|
+
"method" => method,
|
|
23
|
+
"intent" => intent,
|
|
24
|
+
"request" => request
|
|
25
|
+
}
|
|
26
|
+
result["expires"] = expires if expires
|
|
27
|
+
result["description"] = description if description
|
|
28
|
+
result["digest"] = digest if digest
|
|
29
|
+
result["opaque"] = opaque if opaque
|
|
30
|
+
result
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.from_dict(data)
|
|
34
|
+
new(
|
|
35
|
+
id: data["id"],
|
|
36
|
+
realm: data["realm"],
|
|
37
|
+
method: data["method"],
|
|
38
|
+
intent: data["intent"],
|
|
39
|
+
request: data["request"],
|
|
40
|
+
expires: data["expires"],
|
|
41
|
+
description: data["description"],
|
|
42
|
+
digest: data["digest"],
|
|
43
|
+
opaque: data["opaque"]
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_core
|
|
48
|
+
Mpp::Challenge.new(
|
|
49
|
+
id: id,
|
|
50
|
+
method: method,
|
|
51
|
+
intent: intent,
|
|
52
|
+
request: request,
|
|
53
|
+
digest: digest,
|
|
54
|
+
opaque: opaque
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.from_core(challenge, realm, expires: nil, description: nil)
|
|
59
|
+
new(
|
|
60
|
+
id: challenge.id,
|
|
61
|
+
realm: realm,
|
|
62
|
+
method: challenge.method,
|
|
63
|
+
intent: challenge.intent,
|
|
64
|
+
request: challenge.request,
|
|
65
|
+
expires: expires,
|
|
66
|
+
description: description,
|
|
67
|
+
digest: challenge.digest,
|
|
68
|
+
opaque: challenge.opaque
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
MCPCredential = Data.define(:challenge, :payload, :source) do
|
|
74
|
+
def initialize(challenge:, payload:, source: nil)
|
|
75
|
+
super
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def to_dict
|
|
79
|
+
result = {
|
|
80
|
+
"challenge" => challenge.to_dict,
|
|
81
|
+
"payload" => payload
|
|
82
|
+
}
|
|
83
|
+
result["source"] = source if source
|
|
84
|
+
result
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def to_meta
|
|
88
|
+
{META_CREDENTIAL => to_dict}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.from_dict(data)
|
|
92
|
+
new(
|
|
93
|
+
challenge: MCPChallenge.from_dict(data["challenge"]),
|
|
94
|
+
payload: data["payload"],
|
|
95
|
+
source: data["source"]
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.from_meta(meta)
|
|
100
|
+
return nil unless meta.key?(META_CREDENTIAL)
|
|
101
|
+
|
|
102
|
+
from_dict(meta[META_CREDENTIAL])
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def to_core
|
|
106
|
+
request_json = Mpp::Json.compact_encode(challenge.request)
|
|
107
|
+
request_b64 = Base64.urlsafe_encode64(request_json, padding: false)
|
|
108
|
+
|
|
109
|
+
opaque_b64 = nil
|
|
110
|
+
if challenge.opaque
|
|
111
|
+
opaque_json = Mpp::Json.compact_encode(challenge.opaque)
|
|
112
|
+
opaque_b64 = Base64.urlsafe_encode64(opaque_json, padding: false)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
echo = Mpp::ChallengeEcho.new(
|
|
116
|
+
id: challenge.id,
|
|
117
|
+
realm: challenge.realm,
|
|
118
|
+
method: challenge.method,
|
|
119
|
+
intent: challenge.intent,
|
|
120
|
+
request: request_b64,
|
|
121
|
+
expires: challenge.expires,
|
|
122
|
+
digest: challenge.digest,
|
|
123
|
+
opaque: opaque_b64
|
|
124
|
+
)
|
|
125
|
+
Mpp::Credential.new(
|
|
126
|
+
challenge: echo,
|
|
127
|
+
payload: payload,
|
|
128
|
+
source: source
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.from_core(credential, challenge)
|
|
133
|
+
new(
|
|
134
|
+
challenge: challenge,
|
|
135
|
+
payload: credential.payload,
|
|
136
|
+
source: credential.source
|
|
137
|
+
)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
MCPReceipt = Data.define(:status, :challenge_id, :method, :timestamp,
|
|
142
|
+
:reference, :settlement) do
|
|
143
|
+
def initialize(status:, challenge_id:, method:, timestamp:,
|
|
144
|
+
reference: nil, settlement: nil)
|
|
145
|
+
super
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def to_dict
|
|
149
|
+
result = {
|
|
150
|
+
"status" => status,
|
|
151
|
+
"challengeId" => challenge_id,
|
|
152
|
+
"method" => method,
|
|
153
|
+
"timestamp" => timestamp
|
|
154
|
+
}
|
|
155
|
+
result["reference"] = reference if reference
|
|
156
|
+
result["settlement"] = settlement if settlement
|
|
157
|
+
result
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def to_meta
|
|
161
|
+
{META_RECEIPT => to_dict}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def self.from_dict(data)
|
|
165
|
+
new(
|
|
166
|
+
status: data["status"],
|
|
167
|
+
challenge_id: data["challengeId"],
|
|
168
|
+
method: data["method"],
|
|
169
|
+
timestamp: data["timestamp"],
|
|
170
|
+
reference: data["reference"],
|
|
171
|
+
settlement: data["settlement"]
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def self.from_meta(meta)
|
|
176
|
+
return nil unless meta.key?(META_RECEIPT)
|
|
177
|
+
|
|
178
|
+
from_dict(meta[META_RECEIPT])
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def to_core
|
|
182
|
+
Mpp::Receipt.new(
|
|
183
|
+
status: status,
|
|
184
|
+
timestamp: Time.iso8601(timestamp.gsub("Z", "+00:00")),
|
|
185
|
+
reference: reference || ""
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def self.from_core(receipt, challenge_id:, method:, settlement: nil)
|
|
190
|
+
ts = receipt.timestamp.utc.iso8601
|
|
191
|
+
ts = ts.sub(/\+00:00$/, "Z")
|
|
192
|
+
|
|
193
|
+
new(
|
|
194
|
+
status: receipt.status,
|
|
195
|
+
challenge_id: challenge_id,
|
|
196
|
+
method: method,
|
|
197
|
+
timestamp: ts,
|
|
198
|
+
reference: receipt.reference.empty? ? nil : receipt.reference,
|
|
199
|
+
settlement: settlement
|
|
200
|
+
)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|