x402-rack 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/.claude/plans/20260325-bsv-module.md +384 -0
- data/.claude/plans/20260326-rack-stack-architecture.md +304 -0
- data/.rspec +3 -0
- data/.rubocop.yml +33 -0
- data/CLAUDE.md +47 -0
- data/DESIGN.md +216 -0
- data/LICENSE.txt +56 -0
- data/README.md +74 -0
- data/Rakefile +18 -0
- data/docs/process-flow/pay_gateway.md +69 -0
- data/lib/x402/bsv/gateway.rb +148 -0
- data/lib/x402/bsv/pay_gateway.rb +160 -0
- data/lib/x402/bsv/proof_gateway.rb +145 -0
- data/lib/x402/bsv.rb +5 -0
- data/lib/x402/configuration.rb +89 -0
- data/lib/x402/errors.rb +17 -0
- data/lib/x402/middleware.rb +86 -0
- data/lib/x402/protocol/base64url.rb +19 -0
- data/lib/x402/protocol/challenge.rb +63 -0
- data/lib/x402/protocol/proof.rb +36 -0
- data/lib/x402/protocol/request_binding.rb +32 -0
- data/lib/x402/settlement_result.rb +14 -0
- data/lib/x402/verification/protocol_checks.rb +43 -0
- data/lib/x402/version.rb +5 -0
- data/lib/x402.rb +24 -0
- data/sig/x402.rbs +4 -0
- metadata +140 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module X402
|
|
4
|
+
class Configuration
|
|
5
|
+
Route = Struct.new(:http_method, :path, :amount_sats, keyword_init: true)
|
|
6
|
+
|
|
7
|
+
GATEWAY_METHODS = %i[challenge_headers proof_header_names settle!].freeze
|
|
8
|
+
|
|
9
|
+
attr_accessor :domain, :payee_locking_script_hex, :gateways
|
|
10
|
+
attr_reader :routes
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@routes = []
|
|
14
|
+
@gateways = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Register a protected route.
|
|
18
|
+
#
|
|
19
|
+
# @param method [String] HTTP method or "*" for any
|
|
20
|
+
# @param path [String, Regexp] exact path or pattern
|
|
21
|
+
# @param amount_sats [Integer] required payment in satoshis
|
|
22
|
+
def protect(method:, path:, amount_sats:)
|
|
23
|
+
@routes << Route.new(http_method: method.upcase, path: path, amount_sats: amount_sats)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Find the matching route for a request method and path.
|
|
27
|
+
#
|
|
28
|
+
# @return [Route, nil]
|
|
29
|
+
def find_route(request_method, request_path)
|
|
30
|
+
@routes.find do |route|
|
|
31
|
+
method_matches?(route.http_method, request_method) &&
|
|
32
|
+
path_matches?(route.path, request_path)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def validate!
|
|
37
|
+
raise ConfigurationError, "domain is required" if domain.nil? || domain.empty?
|
|
38
|
+
|
|
39
|
+
if payee_locking_script_hex.nil? || payee_locking_script_hex.empty?
|
|
40
|
+
raise ConfigurationError,
|
|
41
|
+
"payee_locking_script_hex is required"
|
|
42
|
+
end
|
|
43
|
+
validate_gateways!
|
|
44
|
+
raise ConfigurationError, "at least one route must be protected" if routes.empty?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def validate_gateways!
|
|
50
|
+
raise ConfigurationError, "at least one gateway is required" if gateways.nil? || gateways.empty?
|
|
51
|
+
|
|
52
|
+
gateways.each_with_index do |gw, i|
|
|
53
|
+
GATEWAY_METHODS.each do |method|
|
|
54
|
+
unless gw.respond_to?(method)
|
|
55
|
+
raise ConfigurationError,
|
|
56
|
+
"gateway at index #{i} does not respond to ##{method}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
validate_no_duplicate_proof_headers!
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def validate_no_duplicate_proof_headers!
|
|
65
|
+
seen = {}
|
|
66
|
+
gateways.each_with_index do |gw, i|
|
|
67
|
+
gw.proof_header_names.each do |name|
|
|
68
|
+
if seen.key?(name)
|
|
69
|
+
raise ConfigurationError,
|
|
70
|
+
"duplicate proof header \"#{name}\" claimed by gateways at indices #{seen[name]} and #{i}"
|
|
71
|
+
end
|
|
72
|
+
seen[name] = i
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def method_matches?(route_method, request_method)
|
|
78
|
+
route_method == "*" || route_method == request_method.upcase
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def path_matches?(route_path, request_path)
|
|
82
|
+
case route_path
|
|
83
|
+
when Regexp then route_path.match?(request_path)
|
|
84
|
+
when String then route_path == request_path
|
|
85
|
+
else false
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
data/lib/x402/errors.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module X402
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class ConfigurationError < Error; end
|
|
7
|
+
|
|
8
|
+
class VerificationError < Error
|
|
9
|
+
attr_reader :reason, :status
|
|
10
|
+
|
|
11
|
+
def initialize(reason, status: 400)
|
|
12
|
+
@reason = reason
|
|
13
|
+
@status = status
|
|
14
|
+
super(reason)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module X402
|
|
7
|
+
class Middleware
|
|
8
|
+
def initialize(app)
|
|
9
|
+
@app = app
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(env)
|
|
13
|
+
request = Rack::Request.new(env)
|
|
14
|
+
config = X402.configuration
|
|
15
|
+
route = config.find_route(request.request_method, request.path_info)
|
|
16
|
+
|
|
17
|
+
# Unprotected route — pass through
|
|
18
|
+
return @app.call(env) unless route
|
|
19
|
+
|
|
20
|
+
# Check for a proof/payment header from any gateway
|
|
21
|
+
gateway, header_name, proof_payload = detect_proof(env, config)
|
|
22
|
+
|
|
23
|
+
if gateway
|
|
24
|
+
settle_and_forward(env, gateway, header_name, proof_payload, request, route)
|
|
25
|
+
else
|
|
26
|
+
issue_challenge(request, route, config)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def detect_proof(env, config)
|
|
33
|
+
config.gateways.each do |gw|
|
|
34
|
+
gw.proof_header_names.each do |name|
|
|
35
|
+
rack_key = rack_header_key(name)
|
|
36
|
+
value = env[rack_key]
|
|
37
|
+
return [gw, name, value] if value && !value.empty?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def issue_challenge(request, route, config)
|
|
44
|
+
headers = { "content-type" => "application/json" }
|
|
45
|
+
|
|
46
|
+
config.gateways.each do |gw|
|
|
47
|
+
gw.challenge_headers(request, route).each do |name, value|
|
|
48
|
+
headers[name.downcase] = value
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
body = JSON.generate({ error: "Payment Required" })
|
|
53
|
+
[402, headers, [body]]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def settle_and_forward(env, gateway, header_name, proof_payload, request, route)
|
|
57
|
+
result = gateway.settle!(header_name, proof_payload, request, route)
|
|
58
|
+
|
|
59
|
+
status, headers, body = @app.call(env)
|
|
60
|
+
|
|
61
|
+
# Merge any receipt headers from the gateway
|
|
62
|
+
if result.respond_to?(:receipt_headers) && result.receipt_headers
|
|
63
|
+
result.receipt_headers.each do |name, value|
|
|
64
|
+
headers[name.downcase] = value
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
[status, headers, body]
|
|
69
|
+
rescue X402::VerificationError => e
|
|
70
|
+
error_response(e.status, e.reason)
|
|
71
|
+
rescue X402::Error => e
|
|
72
|
+
error_response(400, e.message)
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
error_response(500, "internal error: #{e.message}")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def error_response(status, reason)
|
|
78
|
+
body = JSON.generate({ error: reason })
|
|
79
|
+
[status, { "content-type" => "application/json" }, [body]]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def rack_header_key(http_header_name)
|
|
83
|
+
"HTTP_#{http_header_name.upcase.tr("-", "_")}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
|
|
5
|
+
module X402
|
|
6
|
+
module Base64Url
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def encode(data)
|
|
10
|
+
Base64.urlsafe_encode64(data, padding: false)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def decode(data)
|
|
14
|
+
Base64.urlsafe_decode64(data)
|
|
15
|
+
rescue ArgumentError => e
|
|
16
|
+
raise X402::Error, "invalid base64url: #{e.message}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
module X402
|
|
7
|
+
class Challenge
|
|
8
|
+
CURRENT_VERSION = 1
|
|
9
|
+
SUPPORTED_SCHEMES = ["bsv-tx-v1"].freeze
|
|
10
|
+
DEFAULT_TTL = 300 # 5 minutes
|
|
11
|
+
|
|
12
|
+
attr_reader :version, :scheme, :domain, :method, :path, :query,
|
|
13
|
+
:req_headers_sha256, :req_body_sha256,
|
|
14
|
+
:amount_sats, :payee_locking_script_hex,
|
|
15
|
+
:nonce_txid, :nonce_vout, :nonce_satoshis, :nonce_locking_script_hex,
|
|
16
|
+
:expires_at
|
|
17
|
+
|
|
18
|
+
def initialize(attrs = {})
|
|
19
|
+
attrs.each { |k, v| instance_variable_set(:"@#{k}", v) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_h
|
|
23
|
+
{
|
|
24
|
+
version: @version,
|
|
25
|
+
scheme: @scheme,
|
|
26
|
+
domain: @domain,
|
|
27
|
+
method: @method,
|
|
28
|
+
path: @path,
|
|
29
|
+
query: @query,
|
|
30
|
+
req_headers_sha256: @req_headers_sha256,
|
|
31
|
+
req_body_sha256: @req_body_sha256,
|
|
32
|
+
amount_sats: @amount_sats,
|
|
33
|
+
payee_locking_script_hex: @payee_locking_script_hex,
|
|
34
|
+
nonce_txid: @nonce_txid,
|
|
35
|
+
nonce_vout: @nonce_vout,
|
|
36
|
+
nonce_satoshis: @nonce_satoshis,
|
|
37
|
+
nonce_locking_script_hex: @nonce_locking_script_hex,
|
|
38
|
+
expires_at: @expires_at
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_canonical_json
|
|
43
|
+
to_h.to_json_c14n
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def sha256_hex
|
|
47
|
+
OpenSSL::Digest::SHA256.hexdigest(to_canonical_json)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_header
|
|
51
|
+
Base64Url.encode(to_canonical_json)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Reconstruct a challenge from the echoed X402-Challenge header.
|
|
55
|
+
def self.from_header(header_value)
|
|
56
|
+
json = Base64Url.decode(header_value)
|
|
57
|
+
data = JSON.parse(json, symbolize_names: true)
|
|
58
|
+
new(data)
|
|
59
|
+
rescue JSON::ParserError => e
|
|
60
|
+
raise X402::Error, "invalid challenge JSON: #{e.message}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module X402
|
|
6
|
+
class Proof
|
|
7
|
+
attr_reader :challenge_sha256, :payment
|
|
8
|
+
|
|
9
|
+
def initialize(challenge_sha256:, payment:)
|
|
10
|
+
@challenge_sha256 = challenge_sha256
|
|
11
|
+
@payment = payment
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Parse a proof from the X402-Proof header (base64url-encoded JSON).
|
|
15
|
+
def self.from_header(header_value)
|
|
16
|
+
json = Base64Url.decode(header_value)
|
|
17
|
+
data = JSON.parse(json, symbolize_names: true)
|
|
18
|
+
|
|
19
|
+
new(
|
|
20
|
+
challenge_sha256: data[:challenge_sha256],
|
|
21
|
+
payment: data[:payment]
|
|
22
|
+
)
|
|
23
|
+
rescue JSON::ParserError => e
|
|
24
|
+
raise X402::Error, "invalid proof JSON: #{e.message}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Convenience accessors for payment fields.
|
|
28
|
+
def rawtx_b64
|
|
29
|
+
@payment&.fetch(:rawtx_b64, nil)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def txid
|
|
33
|
+
@payment&.fetch(:txid, nil)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module X402
|
|
6
|
+
# Computes canonical hashes of request headers and body for binding
|
|
7
|
+
# challenges and proofs to the actual HTTP request.
|
|
8
|
+
module RequestBinding
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Compute SHA-256 hex digest of the request body.
|
|
12
|
+
# Returns the hash of an empty string if no body is present.
|
|
13
|
+
def body_sha256(rack_request)
|
|
14
|
+
return sha256_hex("") unless rack_request.body
|
|
15
|
+
|
|
16
|
+
rack_request.body.rewind if rack_request.body.respond_to?(:rewind)
|
|
17
|
+
body = rack_request.body.read || ""
|
|
18
|
+
rack_request.body.rewind if rack_request.body.respond_to?(:rewind)
|
|
19
|
+
sha256_hex(body)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Compute SHA-256 hex digest of canonical request headers.
|
|
23
|
+
# For v1, this is the empty string hash (no headers bound).
|
|
24
|
+
def headers_sha256(_rack_request)
|
|
25
|
+
sha256_hex("")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def sha256_hex(data)
|
|
29
|
+
OpenSSL::Digest::SHA256.hexdigest(data)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module X402
|
|
4
|
+
# Returned by Gateway#settle! on successful settlement.
|
|
5
|
+
#
|
|
6
|
+
# @attr receipt_headers [Hash] HTTP headers to include in the 200 response
|
|
7
|
+
# @attr txid [String, nil] transaction identifier
|
|
8
|
+
# @attr network [String, nil] CAIP-2 network identifier (e.g. "bsv:mainnet")
|
|
9
|
+
SettlementResult = Struct.new(:receipt_headers, :txid, :network, keyword_init: true) do
|
|
10
|
+
def initialize(receipt_headers: {}, txid: nil, network: nil)
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module X402
|
|
4
|
+
module Verification
|
|
5
|
+
module ProtocolChecks
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def check_version!(challenge)
|
|
9
|
+
return if challenge.version == Challenge::CURRENT_VERSION
|
|
10
|
+
|
|
11
|
+
raise VerificationError, "unsupported version: #{challenge.version}"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def check_scheme!(challenge)
|
|
15
|
+
return if Challenge::SUPPORTED_SCHEMES.include?(challenge.scheme)
|
|
16
|
+
|
|
17
|
+
raise VerificationError, "unsupported scheme: #{challenge.scheme}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def check_challenge_hash!(challenge, proof)
|
|
21
|
+
raise VerificationError, "challenge hash mismatch" unless challenge.sha256_hex == proof.challenge_sha256
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def check_request_binding!(challenge, rack_request)
|
|
25
|
+
raise VerificationError, "request method mismatch" unless challenge.method == rack_request.request_method
|
|
26
|
+
raise VerificationError, "request path mismatch" unless challenge.path == rack_request.path_info
|
|
27
|
+
raise VerificationError, "request query mismatch" unless challenge.query == rack_request.query_string
|
|
28
|
+
|
|
29
|
+
expected_body = RequestBinding.body_sha256(rack_request)
|
|
30
|
+
raise VerificationError, "request body hash mismatch" unless challenge.req_body_sha256 == expected_body
|
|
31
|
+
|
|
32
|
+
expected_headers = RequestBinding.headers_sha256(rack_request)
|
|
33
|
+
return if challenge.req_headers_sha256 == expected_headers
|
|
34
|
+
|
|
35
|
+
raise VerificationError, "request headers hash mismatch"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def check_expiry!(challenge)
|
|
39
|
+
raise VerificationError.new("challenge expired", status: 402) if challenge.expires_at <= Time.now.to_i
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/x402/version.rb
ADDED
data/lib/x402.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "x402/version"
|
|
4
|
+
require_relative "x402/errors"
|
|
5
|
+
require_relative "x402/settlement_result"
|
|
6
|
+
require_relative "x402/configuration"
|
|
7
|
+
require_relative "x402/middleware"
|
|
8
|
+
|
|
9
|
+
module X402
|
|
10
|
+
class << self
|
|
11
|
+
def configure
|
|
12
|
+
yield(configuration)
|
|
13
|
+
configuration.validate!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def configuration
|
|
17
|
+
@configuration ||= Configuration.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def reset_configuration!
|
|
21
|
+
@configuration = Configuration.new
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/sig/x402.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: x402-rack
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Simon Bettison
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-03-27 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.2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.2'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: bsv-sdk
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.3'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.3'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: bsv-wallet
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.1'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.1'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: json-canonicalization
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rack
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.0'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.0'
|
|
82
|
+
description: Rack middleware implementing the x402 stateless settlement-gated HTTP
|
|
83
|
+
protocol using BSV (Bitcoin SV) payments.
|
|
84
|
+
email:
|
|
85
|
+
- simon@bettison.org
|
|
86
|
+
executables: []
|
|
87
|
+
extensions: []
|
|
88
|
+
extra_rdoc_files: []
|
|
89
|
+
files:
|
|
90
|
+
- ".claude/plans/20260325-bsv-module.md"
|
|
91
|
+
- ".claude/plans/20260326-rack-stack-architecture.md"
|
|
92
|
+
- ".rspec"
|
|
93
|
+
- ".rubocop.yml"
|
|
94
|
+
- CLAUDE.md
|
|
95
|
+
- DESIGN.md
|
|
96
|
+
- LICENSE.txt
|
|
97
|
+
- README.md
|
|
98
|
+
- Rakefile
|
|
99
|
+
- docs/process-flow/pay_gateway.md
|
|
100
|
+
- lib/x402.rb
|
|
101
|
+
- lib/x402/bsv.rb
|
|
102
|
+
- lib/x402/bsv/gateway.rb
|
|
103
|
+
- lib/x402/bsv/pay_gateway.rb
|
|
104
|
+
- lib/x402/bsv/proof_gateway.rb
|
|
105
|
+
- lib/x402/configuration.rb
|
|
106
|
+
- lib/x402/errors.rb
|
|
107
|
+
- lib/x402/middleware.rb
|
|
108
|
+
- lib/x402/protocol/base64url.rb
|
|
109
|
+
- lib/x402/protocol/challenge.rb
|
|
110
|
+
- lib/x402/protocol/proof.rb
|
|
111
|
+
- lib/x402/protocol/request_binding.rb
|
|
112
|
+
- lib/x402/settlement_result.rb
|
|
113
|
+
- lib/x402/verification/protocol_checks.rb
|
|
114
|
+
- lib/x402/version.rb
|
|
115
|
+
- sig/x402.rbs
|
|
116
|
+
homepage: https://github.com/sgbett/x402-rack
|
|
117
|
+
licenses:
|
|
118
|
+
- LicenseRef-OpenBSV
|
|
119
|
+
metadata:
|
|
120
|
+
source_code_uri: https://github.com/sgbett/x402-rack
|
|
121
|
+
changelog_uri: https://github.com/sgbett/x402-rack/blob/master/CHANGELOG.md
|
|
122
|
+
rubygems_mfa_required: 'true'
|
|
123
|
+
rdoc_options: []
|
|
124
|
+
require_paths:
|
|
125
|
+
- lib
|
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: 3.1.0
|
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
132
|
+
requirements:
|
|
133
|
+
- - ">="
|
|
134
|
+
- !ruby/object:Gem::Version
|
|
135
|
+
version: '0'
|
|
136
|
+
requirements: []
|
|
137
|
+
rubygems_version: 3.6.2
|
|
138
|
+
specification_version: 4
|
|
139
|
+
summary: x402 protocol server middleware for BSV micropayments
|
|
140
|
+
test_files: []
|