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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +133 -0
  4. data/lib/mpp/body_digest.rb +37 -0
  5. data/lib/mpp/challenge.rb +115 -0
  6. data/lib/mpp/challenge_echo.rb +19 -0
  7. data/lib/mpp/challenge_id.rb +54 -0
  8. data/lib/mpp/client/transport.rb +137 -0
  9. data/lib/mpp/client.rb +9 -0
  10. data/lib/mpp/credential.rb +20 -0
  11. data/lib/mpp/errors.rb +190 -0
  12. data/lib/mpp/expires.rb +60 -0
  13. data/lib/mpp/extensions/mcp/capabilities.rb +23 -0
  14. data/lib/mpp/extensions/mcp/constants.rb +17 -0
  15. data/lib/mpp/extensions/mcp/decorator.rb +44 -0
  16. data/lib/mpp/extensions/mcp/errors.rb +110 -0
  17. data/lib/mpp/extensions/mcp/types.rb +205 -0
  18. data/lib/mpp/extensions/mcp/verify.rb +152 -0
  19. data/lib/mpp/extensions/mcp.rb +16 -0
  20. data/lib/mpp/json.rb +32 -0
  21. data/lib/mpp/methods/stripe/charge_intent.rb +90 -0
  22. data/lib/mpp/methods/stripe/client_method.rb +42 -0
  23. data/lib/mpp/methods/stripe/defaults.rb +14 -0
  24. data/lib/mpp/methods/stripe/stripe_method.rb +63 -0
  25. data/lib/mpp/methods/stripe.rb +14 -0
  26. data/lib/mpp/methods/tempo/account.rb +52 -0
  27. data/lib/mpp/methods/tempo/attribution.rb +112 -0
  28. data/lib/mpp/methods/tempo/client_method.rb +259 -0
  29. data/lib/mpp/methods/tempo/defaults.rb +77 -0
  30. data/lib/mpp/methods/tempo/fee_payer_envelope.rb +74 -0
  31. data/lib/mpp/methods/tempo/intents.rb +377 -0
  32. data/lib/mpp/methods/tempo/keychain.rb +31 -0
  33. data/lib/mpp/methods/tempo/proof.rb +127 -0
  34. data/lib/mpp/methods/tempo/rpc.rb +60 -0
  35. data/lib/mpp/methods/tempo/schemas.rb +96 -0
  36. data/lib/mpp/methods/tempo/transaction.rb +144 -0
  37. data/lib/mpp/methods/tempo.rb +22 -0
  38. data/lib/mpp/parsing.rb +252 -0
  39. data/lib/mpp/receipt.rb +31 -0
  40. data/lib/mpp/secure_compare.rb +25 -0
  41. data/lib/mpp/server/decorator.rb +32 -0
  42. data/lib/mpp/server/defaults.rb +45 -0
  43. data/lib/mpp/server/intent.rb +40 -0
  44. data/lib/mpp/server/method.rb +27 -0
  45. data/lib/mpp/server/middleware.rb +51 -0
  46. data/lib/mpp/server/mpp_handler.rb +97 -0
  47. data/lib/mpp/server/verify.rb +129 -0
  48. data/lib/mpp/server.rb +15 -0
  49. data/lib/mpp/store.rb +49 -0
  50. data/lib/mpp/units.rb +57 -0
  51. data/lib/mpp/version.rb +6 -0
  52. data/lib/mpp-rb.rb +3 -0
  53. data/lib/mpp.rb +68 -0
  54. metadata +111 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ca67d74a92976b39f4bb8ce28a4fc46bd87d1cc0fdfcc5978681760a05e1231b
4
+ data.tar.gz: 63526c4eff0144dfe35c1a722161a23173ecb8a94cc087efb98f8cb7e8f4f8b9
5
+ SHA512:
6
+ metadata.gz: 37d08450e62ad4b411f52caddd71d403b0c78d861a596f4081b6e45705881612fc81ad085675a4a2cdde67174283af1b86eddce7fa1dd6a5f70827bede94749d
7
+ data.tar.gz: 8f0e2042ca49bc3c7ae54d6e79ff6276ac807d5e65111d83f2de012ee0680c75bac1452c3c70afdcfe2dd02d73f20fe229bc633cb892021a6bb4be0798edec19
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stripe, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # mpp-rb
2
+
3
+ Ruby SDK for the [**Machine Payments Protocol**](https://mpp.dev)
4
+
5
+ [![Gem Version](https://img.shields.io/gem/v/mpp.svg)](https://rubygems.org/gems/mpp-rb)
6
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+
8
+ ## Documentation
9
+
10
+ Full documentation, API reference, and guides are available at **[mpp.dev/sdk/ruby](https://mpp.dev/sdk/ruby)**.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ gem install mpp-rb
16
+ ```
17
+
18
+ Or add to your Gemfile:
19
+
20
+ ```ruby
21
+ gem "mpp-rb"
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ### Server
27
+
28
+ ```ruby
29
+ require "mpp-rb"
30
+
31
+ server = Mpp.create(
32
+ method: Mpp::Methods::Tempo.tempo(
33
+ intents: {"charge" => Mpp::Methods::Tempo::ChargeIntent.new},
34
+ recipient: "0x0000000000000000000000000000000000000001",
35
+ ),
36
+ )
37
+
38
+ # In your request handler (Sinatra, Rails, Rack, etc.)
39
+ result = server.charge(authorization_header, "0.50", description: "Paid endpoint")
40
+
41
+ if result.is_a?(Mpp::Challenge)
42
+ # Return 402 with WWW-Authenticate header
43
+ resp = Mpp::Server::Decorator.make_challenge_response(result, server.realm)
44
+ # resp["status"], resp["headers"], resp["body"]
45
+ else
46
+ credential, receipt = result
47
+ # credential.source — payer address
48
+ # receipt.to_payment_receipt — Payment-Receipt header value
49
+ end
50
+ ```
51
+
52
+ ### Client
53
+
54
+ ```ruby
55
+ require "mpp-rb"
56
+
57
+ account = Mpp::Methods::Tempo::Account.from_key("0x...")
58
+
59
+ transport = Mpp::Client::Transport.new(
60
+ methods: [
61
+ Mpp::Methods::Tempo.tempo(
62
+ account: account,
63
+ intents: {"charge" => Mpp::Methods::Tempo::ChargeIntent.new},
64
+ ),
65
+ ],
66
+ )
67
+
68
+ response = transport.request(:get, "https://mpp.dev/api/ping/paid")
69
+ ```
70
+
71
+ ### Rack Middleware
72
+
73
+ ```ruby
74
+ require "mpp-rb"
75
+
76
+ handler = Mpp.create(
77
+ method: Mpp::Methods::Tempo.tempo(
78
+ intents: {"charge" => Mpp::Methods::Tempo::ChargeIntent.new},
79
+ recipient: "0x0000000000000000000000000000000000000001",
80
+ ),
81
+ )
82
+
83
+ # In your config.ru or Rails middleware stack:
84
+ use Mpp::Server::Middleware, handler: handler
85
+
86
+ # In your app, signal that payment is required:
87
+ env["mpp.charge"] = { amount: "0.50", description: "Paid endpoint" }
88
+ ```
89
+
90
+ ## Examples
91
+
92
+ | Example | Description |
93
+ |---------|-------------|
94
+ | [tempo_charge](./examples/tempo_charge/) | Tempo testnet payments via Sinatra |
95
+ | [stripe_charge](./examples/stripe_charge/) | Stripe payments via Shared Payment Tokens |
96
+
97
+ Each example is a standalone Sinatra app with `/free` and `/paid` endpoints. To run one:
98
+
99
+ ```sh
100
+ cd examples/tempo_charge
101
+ bundle install
102
+ ruby app.rb
103
+ ```
104
+
105
+ Then test with [mppx](https://www.npmjs.com/package/mppx), a CLI that handles the full 402 challenge/credential flow:
106
+
107
+ ```sh
108
+ npx mppx http://localhost:4567/paid
109
+ ```
110
+
111
+ ## Support Matrix
112
+
113
+ | Method | Charge Client | Charge Server |
114
+ |--------|---------------|---------------|
115
+ | Tempo | Yes | Yes |
116
+ | Stripe | Yes | Yes |
117
+
118
+ Tempo charge transaction construction is implemented directly in Ruby. Optional dependencies: `eth` (account signing) and `rlp` (fee payer envelope).
119
+
120
+ ## Protocol
121
+
122
+ Built on the ["Payment" HTTP Authentication Scheme](https://datatracker.ietf.org/doc/draft-ryan-httpauth-payment/). See [mpp-specs](https://tempoxyz.github.io/mpp-specs/) for the full specification.
123
+
124
+ ## Releasing
125
+
126
+ 1. Update the version in `lib/mpp/version.rb`
127
+ 2. Commit: `git commit -am "v0.x.x"`
128
+ 3. Tag: `git tag v0.x.x`
129
+ 4. Push: `git push origin main --tags`
130
+
131
+ ## License
132
+
133
+ MIT
@@ -0,0 +1,37 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "openssl"
5
+ require "base64"
6
+ require "json"
7
+
8
+ module Mpp
9
+ module BodyDigest
10
+ extend T::Sig
11
+
12
+ module_function
13
+
14
+ # Compute a SHA-256 digest of a request body.
15
+ # Returns: "sha-256=<base64>"
16
+ sig { params(body: T.untyped).returns(String) }
17
+ def compute(body)
18
+ case body
19
+ when Hash
20
+ body = Mpp::Json.compact_encode(body)
21
+ when String
22
+ # use as-is
23
+ end
24
+ body = body.encode(Encoding::UTF_8) if body.is_a?(String)
25
+ digest = OpenSSL::Digest::SHA256.digest(body)
26
+ encoded = Base64.strict_encode64(digest)
27
+ "sha-256=#{encoded}"
28
+ end
29
+
30
+ # Verify a body digest matches the expected value.
31
+ sig { params(digest: String, body: T.untyped).returns(T::Boolean) }
32
+ def verify(digest, body)
33
+ expected = compute(body)
34
+ Mpp.secure_compare(expected, digest)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,115 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "challenge_id"
5
+ require_relative "secure_compare"
6
+ require_relative "json"
7
+
8
+ module Mpp
9
+ Challenge = Data.define(
10
+ :id,
11
+ :method,
12
+ :intent,
13
+ :request,
14
+ :realm,
15
+ :request_b64,
16
+ :digest,
17
+ :expires,
18
+ :description,
19
+ :opaque
20
+ ) do
21
+ def initialize(id:, method:, intent:, request:, realm: "", request_b64: "", digest: nil, expires: nil,
22
+ description: nil, opaque: nil)
23
+ super
24
+ end
25
+
26
+ # Create a Challenge with an HMAC-bound ID.
27
+ def self.create(secret_key:, realm:, method:, intent:, request:, expires: nil, digest: nil, description: nil,
28
+ meta: nil)
29
+ challenge_id = Mpp.generate_challenge_id(
30
+ secret_key: secret_key,
31
+ realm: realm,
32
+ method: method,
33
+ intent: intent,
34
+ request: request,
35
+ expires: expires,
36
+ digest: digest,
37
+ opaque: meta
38
+ )
39
+ request_json = Mpp::Json.compact_encode(request)
40
+ request_b64 = Mpp.b64url_encode(request_json)
41
+
42
+ new(
43
+ id: challenge_id,
44
+ method: method,
45
+ intent: intent,
46
+ request: request,
47
+ realm: realm,
48
+ request_b64: request_b64,
49
+ digest: digest,
50
+ expires: expires,
51
+ description: description,
52
+ opaque: meta
53
+ )
54
+ end
55
+
56
+ # Parse a Challenge from a WWW-Authenticate header value.
57
+ def self.from_www_authenticate(header)
58
+ Mpp::Parsing.parse_www_authenticate(header)
59
+ end
60
+
61
+ # Parse multiple Payment challenges from a merged WWW-Authenticate header.
62
+ # Handles RFC 9110 §11.6.1 comma-separated authentication schemes.
63
+ def self.from_www_authenticate_list(header)
64
+ indices = []
65
+ header.scan(/Payment\s+/i) { indices << T.must(Regexp.last_match).begin(0) }
66
+ return [] if indices.empty?
67
+
68
+ indices.each_with_index.map do |start_idx, i|
69
+ end_idx = (i + 1 < indices.length) ? indices[i + 1] : header.length
70
+ chunk = T.must(header[start_idx...end_idx]).sub(/,\s*$/, "")
71
+ from_www_authenticate(chunk)
72
+ end
73
+ end
74
+
75
+ # Serialize to a WWW-Authenticate header value.
76
+ def to_www_authenticate(realm)
77
+ Mpp::Parsing.format_www_authenticate(self, realm)
78
+ end
79
+
80
+ # Verify the challenge ID matches the expected HMAC.
81
+ def verify(secret_key, realm)
82
+ expected_id = Mpp.generate_challenge_id(
83
+ secret_key: secret_key,
84
+ realm: realm,
85
+ method: method,
86
+ intent: intent,
87
+ request: request,
88
+ expires: expires,
89
+ digest: digest,
90
+ opaque: opaque
91
+ )
92
+ Mpp.secure_compare(id, expected_id)
93
+ end
94
+
95
+ # Create a ChallengeEcho for use in credentials.
96
+ def to_echo
97
+ opaque_b64 = nil
98
+ if opaque
99
+ opaque_json = Mpp::Json.compact_encode(opaque)
100
+ opaque_b64 = Mpp.b64url_encode(opaque_json)
101
+ end
102
+
103
+ ChallengeEcho.new(
104
+ id: id,
105
+ realm: realm,
106
+ method: method,
107
+ intent: intent,
108
+ request: request_b64,
109
+ expires: expires,
110
+ digest: digest,
111
+ opaque: opaque_b64
112
+ )
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,19 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module Mpp
5
+ ChallengeEcho = Data.define(
6
+ :id,
7
+ :realm,
8
+ :method,
9
+ :intent,
10
+ :request,
11
+ :expires,
12
+ :digest,
13
+ :opaque
14
+ ) do
15
+ def initialize(id:, realm:, method:, intent:, request:, expires: nil, digest: nil, opaque: nil)
16
+ super
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,54 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "openssl"
5
+ require "base64"
6
+
7
+ module Mpp
8
+ extend T::Sig
9
+
10
+ module_function
11
+
12
+ # Generate HMAC-SHA256 challenge ID per spec.
13
+ #
14
+ # HMAC input format: realm|method|intent|request_b64|expires|digest|opaque_b64
15
+ # All fields always included; absent optional fields use empty string.
16
+ # Output: base64url(HMAC-SHA256(secret_key, input))
17
+ sig { params(secret_key: T.untyped, realm: T.untyped, method: T.untyped, intent: T.untyped, request: T.untyped, expires: T.untyped, digest: T.untyped, opaque: T.untyped).returns(String) }
18
+ def generate_challenge_id(secret_key:, realm:, method:, intent:, request:, expires: nil, digest: nil, opaque: nil)
19
+ request_json = Json.compact_encode(request)
20
+ request_b64 = b64url_encode(request_json)
21
+
22
+ opaque_b64 = if opaque
23
+ opaque_json = Json.compact_encode(opaque)
24
+ b64url_encode(opaque_json)
25
+ else
26
+ ""
27
+ end
28
+
29
+ hmac_input = [
30
+ realm,
31
+ method,
32
+ intent,
33
+ request_b64,
34
+ expires || "",
35
+ digest || "",
36
+ opaque_b64
37
+ ].join("|")
38
+
39
+ mac = OpenSSL::HMAC.digest("SHA256", secret_key.encode(Encoding::UTF_8), hmac_input.encode(Encoding::UTF_8))
40
+ Base64.urlsafe_encode64(mac, padding: false)
41
+ end
42
+
43
+ # Encode string to base64url without padding.
44
+ sig { params(data: T.untyped).returns(String) }
45
+ def b64url_encode(data)
46
+ Base64.urlsafe_encode64(data.encode(Encoding::UTF_8), padding: false)
47
+ end
48
+
49
+ # Encode bytes to base64url without padding.
50
+ sig { params(data: String).returns(String) }
51
+ def b64url_encode_bytes(data)
52
+ Base64.urlsafe_encode64(data, padding: false)
53
+ end
54
+ end
@@ -0,0 +1,137 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "net/http"
5
+ require "uri"
6
+ require "json"
7
+ require "time"
8
+
9
+ module Mpp
10
+ module Client
11
+ # Payment-aware HTTP client that handles 402 Payment Required responses.
12
+ #
13
+ # Wraps Net::HTTP and automatically:
14
+ # 1. Detects 402 responses with WWW-Authenticate: Payment headers
15
+ # 2. Parses the challenge and finds a matching payment method
16
+ # 3. Creates credentials and retries the request
17
+ # 4. Returns the final response
18
+ class Transport
19
+ extend T::Sig
20
+
21
+ sig { params(methods: T::Array[T.untyped]).void }
22
+ def initialize(methods:)
23
+ @methods = T.let(methods.to_h { |m| [m.name, m] }, T::Hash[String, T.untyped])
24
+ end
25
+
26
+ # Send an HTTP request with automatic 402 payment handling.
27
+ # Returns [Net::HTTPResponse, body_string].
28
+ sig { params(method: T.untyped, url: T.any(URI::Generic, String), headers: T.untyped, body: T.untyped).returns(T.untyped) }
29
+ def request(method, url, headers: {}, body: nil)
30
+ uri = URI(url)
31
+ response = send_request(uri, method, headers, body)
32
+
33
+ return response unless response.code.to_i == 402
34
+
35
+ # Parse WWW-Authenticate headers
36
+ www_auth_headers = response.get_fields("www-authenticate") || []
37
+ challenge, matched_method = find_matching_challenge(www_auth_headers)
38
+ return response unless challenge && matched_method
39
+
40
+ # Check expiry before paying (client-side guardrail)
41
+ if challenge.expires
42
+ begin
43
+ expires_dt = Time.iso8601(challenge.expires.gsub("Z", "+00:00"))
44
+ return response if expires_dt < Time.now.utc
45
+ rescue ArgumentError
46
+ # If we can't parse, let server validate
47
+ end
48
+ end
49
+
50
+ credential = matched_method.create_credential(challenge)
51
+ auth_header = credential.to_authorization
52
+
53
+ retry_headers = headers.merge("Authorization" => auth_header)
54
+ send_request(uri, method, retry_headers, body)
55
+ end
56
+
57
+ sig { params(url: T.any(URI::Generic, String), kwargs: T.untyped).returns(T.untyped) }
58
+ def get(url, **kwargs)
59
+ request("GET", url, **kwargs)
60
+ end
61
+
62
+ sig { params(url: T.any(URI::Generic, String), kwargs: T.untyped).returns(T.untyped) }
63
+ def post(url, **kwargs)
64
+ request("POST", url, **kwargs)
65
+ end
66
+
67
+ sig { params(url: T.any(URI::Generic, String), kwargs: T.untyped).returns(T.untyped) }
68
+ def put(url, **kwargs)
69
+ request("PUT", url, **kwargs)
70
+ end
71
+
72
+ sig { params(url: T.any(URI::Generic, String), kwargs: T.untyped).returns(T.untyped) }
73
+ def delete(url, **kwargs)
74
+ request("DELETE", url, **kwargs)
75
+ end
76
+
77
+ private
78
+
79
+ sig { params(uri: URI::Generic, method: T.untyped, headers: T::Hash[String, String], body: T.nilable(String)).returns(Net::HTTPResponse) }
80
+ def send_request(uri, method, headers, body)
81
+ http = Net::HTTP.new(uri.host, uri.port)
82
+ http.use_ssl = uri.scheme == "https"
83
+
84
+ request_class = case method.to_s.upcase
85
+ when "GET" then Net::HTTP::Get
86
+ when "POST" then Net::HTTP::Post
87
+ when "PUT" then Net::HTTP::Put
88
+ when "DELETE" then Net::HTTP::Delete
89
+ when "PATCH" then Net::HTTP::Patch
90
+ else raise ArgumentError, "Unsupported HTTP method: #{method}"
91
+ end
92
+
93
+ req = request_class.new(uri)
94
+ headers.each { |k, v| req[k] = v }
95
+ req.body = body if body
96
+
97
+ http.request(req)
98
+ end
99
+
100
+ sig { params(www_auth_headers: T.untyped).returns(T::Array[T.untyped]) }
101
+ def find_matching_challenge(www_auth_headers)
102
+ www_auth_headers.each do |header|
103
+ next unless header.downcase.start_with?("payment ")
104
+
105
+ begin
106
+ parsed = Mpp::Challenge.from_www_authenticate(header)
107
+ return [parsed, @methods[parsed.method]] if @methods.key?(parsed.method)
108
+ rescue Mpp::ParseError
109
+ next
110
+ end
111
+ end
112
+ [nil, nil]
113
+ end
114
+ end
115
+
116
+ # Module-level convenience methods
117
+ extend T::Sig
118
+
119
+ module_function
120
+
121
+ sig { params(method: T.untyped, url: T.untyped, methods: T::Array[T.untyped], kwargs: T.untyped).returns(T.untyped) }
122
+ def request(method, url, methods:, **kwargs)
123
+ transport = Transport.new(methods: methods)
124
+ transport.request(method, url, **kwargs)
125
+ end
126
+
127
+ sig { params(url: T.untyped, methods: T::Array[T.untyped], kwargs: T.untyped).returns(T.untyped) }
128
+ def get(url, methods:, **kwargs)
129
+ request("GET", url, **T.unsafe({methods: methods, **kwargs}))
130
+ end
131
+
132
+ sig { params(url: T.untyped, methods: T::Array[T.untyped], kwargs: T.untyped).returns(T.untyped) }
133
+ def post(url, methods:, **kwargs)
134
+ request("POST", url, **T.unsafe({methods: methods, **kwargs}))
135
+ end
136
+ end
137
+ end
data/lib/mpp/client.rb ADDED
@@ -0,0 +1,9 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "client/transport"
5
+
6
+ module Mpp
7
+ module Client
8
+ end
9
+ end
@@ -0,0 +1,20 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Mpp
5
+ Credential = Data.define(:challenge, :payload, :source) do
6
+ def initialize(challenge:, payload:, source: nil)
7
+ super
8
+ end
9
+
10
+ # Parse a Credential from an Authorization header value.
11
+ def self.from_authorization(header)
12
+ Mpp::Parsing.parse_authorization(header)
13
+ end
14
+
15
+ # Serialize to an Authorization header value.
16
+ def to_authorization
17
+ Mpp::Parsing.format_authorization(self)
18
+ end
19
+ end
20
+ end