x402-rack 0.5.1 → 0.6.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 +4 -4
- data/.yardopts +6 -0
- data/CHANGELOG.md +23 -0
- data/CLAUDE.md +3 -14
- data/Rakefile +46 -0
- data/docs/index.md +21 -0
- data/docs/requirements.txt +3 -0
- data/docs/schemes/brc-105.md +4 -0
- data/docs/schemes/bsv-pay.md +5 -1
- data/docs/schemes/bsv-proof.md +11 -0
- data/lib/x402/bsv/brc105_gateway.rb +93 -17
- data/lib/x402/bsv/pay_gateway.rb +14 -0
- data/lib/x402/bsv/prefix_store.rb +8 -0
- data/lib/x402/bsv/proof_gateway.rb +15 -0
- data/lib/x402/configuration.rb +37 -7
- data/lib/x402/middleware.rb +70 -4
- data/lib/x402/payment_observer.rb +11 -1
- data/lib/x402/settlement_worker.rb +4 -0
- data/lib/x402/version.rb +1 -1
- data/mkdocs.yml +68 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f3b7c632a276b60aa31022985a02b8b9e1722cef5e11fbe0ee626f8d5c2fac98
|
|
4
|
+
data.tar.gz: 791f68f8b62750f1e3741404e3ed551b6a7f97f8160f42eeb76be1db551dc4c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8d3df96ff711728a23d5e8bc6ef2b62085e19904ec7810e0dfd26af47114b258e23d0064aa952ea19a0a0479c04398ac682b4f0b596a7597ef31721e2b1c02c6
|
|
7
|
+
data.tar.gz: f270706245222660c689860335f8d204d484819ddb8d9dc176fe10d51633d339d137c8a53de0d71f805cbd3eb8605dd6c3f1c1836f2251b7b2ad67bb26403b74
|
data/.yardopts
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.6.0] - 2026-04-05
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Configurable structured logging** — pluggable `logger` on configuration with structured request lifecycle messages (route match, identity key, proof dispatch, settlement outcome). (#102)
|
|
13
|
+
- **BRC-105 settlement logging** — structured log output for derivation, key ID, locking script verification, and settlement result in `BRC105Gateway`. (#96)
|
|
14
|
+
- **BRC-105 §6.2 response body** — settlement success and error responses now include spec-conformant JSON body with receipt details. Spec-anchored tests. (#91)
|
|
15
|
+
- **BRC-105 response headers** — `x-bsv-payment-version` and `x-bsv-payment-satoshis-paid` headers on successful settlement. (#91)
|
|
16
|
+
- **API documentation** — YARD-style docs for public interfaces. (#89)
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **Client identity key required for BRC-105** — `x-bsv-auth-identity-key` header is now mandatory for BRC-105 settlement (§7.1). Requests without it receive a 401 error. (#99)
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **ARC broadcast error details** — error messages from ARC are now logged and surfaced in the 502 response rather than swallowed silently.
|
|
25
|
+
- **Base64 `derivationSuffix` accepted** — client-generated suffixes may be base64-encoded (not just hex). Validation updated to accept both formats. (#93)
|
|
26
|
+
|
|
27
|
+
### Build
|
|
28
|
+
|
|
29
|
+
- **Dependency updates** — bsv-sdk 0.6.0 → 0.6.1, rack 3.2.6.
|
|
30
|
+
|
|
8
31
|
## [0.5.1] - 2026-04-04
|
|
9
32
|
|
|
10
33
|
### Fixed
|
data/CLAUDE.md
CHANGED
|
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
4
4
|
|
|
5
5
|
## Project
|
|
6
6
|
|
|
7
|
-
x402-rack is a Ruby gem providing Rack middleware for the x402 protocol (BSV settlement-gated HTTP).
|
|
7
|
+
x402-rack is a Ruby gem providing Rack middleware for the x402 protocol (BSV settlement-gated HTTP). Requires Ruby >= 3.1.
|
|
8
8
|
|
|
9
9
|
## Commands
|
|
10
10
|
|
|
@@ -31,20 +31,9 @@ bundle exec rubocop -A
|
|
|
31
31
|
bundle exec rake
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
##
|
|
34
|
+
## Specifications
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
- `lib/x402.rb` — main entry point, defines `X402` module
|
|
39
|
-
- `lib/x402/version.rb` — version constant
|
|
40
|
-
- `lib/x402/middleware.rb` — pure dispatcher (no blockchain knowledge)
|
|
41
|
-
- `lib/x402/bsv/gateway.rb` — base class for template-based gateways
|
|
42
|
-
- `lib/x402/bsv/pay_gateway.rb` — Coinbase v2 headers, server broadcasts
|
|
43
|
-
- `lib/x402/bsv/proof_gateway.rb` — merkleworks headers, client broadcasts
|
|
44
|
-
- `lib/x402/bsv/brc105_gateway.rb` — BSV Association BRC-105, BRC-29 derivation (no inheritance from Gateway)
|
|
45
|
-
- `lib/x402/bsv/prefix_store.rb` — pluggable replay protection for BRC-105 derivation prefixes
|
|
46
|
-
- `sig/x402.rbs` — RBS type signatures
|
|
47
|
-
- `x402-rack.gemspec` — gem specification (dependencies defined here, not in Gemfile)
|
|
36
|
+
This project implements published protocol specifications (BRC-105, BRC-29, etc.). When writing or modifying code that implements a spec, consult the spec directly (via `bsv-protocol-docs` MCP) and verify conformance — including optional features unless there is a documented reason to omit them. Tests should be anchored to spec requirements, not just implementation behaviour.
|
|
48
37
|
|
|
49
38
|
## Code Style
|
|
50
39
|
|
data/Rakefile
CHANGED
|
@@ -16,3 +16,49 @@ require "rubocop/rake_task"
|
|
|
16
16
|
RuboCop::RakeTask.new
|
|
17
17
|
|
|
18
18
|
task default: %i[spec rubocop]
|
|
19
|
+
|
|
20
|
+
def generate_reference_index(output_dir)
|
|
21
|
+
require "csv"
|
|
22
|
+
csv_path = File.join(output_dir, "index.csv")
|
|
23
|
+
return unless File.exist?(csv_path)
|
|
24
|
+
|
|
25
|
+
modules = []
|
|
26
|
+
classes = []
|
|
27
|
+
CSV.foreach(csv_path, headers: true) do |row|
|
|
28
|
+
next unless %w[Module Class].include?(row["type"])
|
|
29
|
+
|
|
30
|
+
entry = { name: row["name"], path: row["path"] }
|
|
31
|
+
row["type"] == "Module" ? modules << entry : classes << entry
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
File.open(File.join(output_dir, "index.md"), "w") do |f|
|
|
35
|
+
f.puts "# API Reference"
|
|
36
|
+
f.puts
|
|
37
|
+
f.puts "Auto-generated from source using [YARD](https://yardoc.org/)."
|
|
38
|
+
f.puts
|
|
39
|
+
f.puts "## Modules"
|
|
40
|
+
f.puts
|
|
41
|
+
modules.sort_by { |e| e[:name] }.each { |e| f.puts "- [#{e[:name]}](#{e[:path]})" }
|
|
42
|
+
f.puts
|
|
43
|
+
f.puts "## Classes"
|
|
44
|
+
f.puts
|
|
45
|
+
classes.sort_by { |e| e[:name] }.each { |e| f.puts "- [#{e[:name]}](#{e[:path]})" }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
namespace :docs do
|
|
50
|
+
desc "Generate YARD markdown into docs/reference/"
|
|
51
|
+
task :generate do
|
|
52
|
+
require "fileutils"
|
|
53
|
+
output_dir = "docs/reference"
|
|
54
|
+
FileUtils.rm_rf(output_dir)
|
|
55
|
+
FileUtils.mkdir_p(output_dir)
|
|
56
|
+
sh "bundle exec yardoc --plugin markdown --format markdown --output-dir docs/reference lib/**/*.rb"
|
|
57
|
+
generate_reference_index(output_dir)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
desc "Generate docs and serve locally with MkDocs"
|
|
61
|
+
task serve: :generate do
|
|
62
|
+
sh "mkdocs serve"
|
|
63
|
+
end
|
|
64
|
+
end
|
data/docs/index.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# x402-rack
|
|
2
|
+
|
|
3
|
+
Rack middleware for payment-gated HTTP using BSV and the [x402 protocol](https://docs.x402.org/).
|
|
4
|
+
|
|
5
|
+
The middleware is a pure dispatcher — it matches routes, issues payment challenges, and routes proofs to pluggable gateway backends for settlement. It has no blockchain knowledge and holds no keys.
|
|
6
|
+
|
|
7
|
+
## Three BSV settlement schemes
|
|
8
|
+
|
|
9
|
+
- **BSV-pay** (Coinbase v2 headers) — server broadcasts via ARC. No nonces, minimal infrastructure.
|
|
10
|
+
- **BSV-proof** (merkleworks x402) — client broadcasts, server checks mempool. Nonce-bound, request-binding.
|
|
11
|
+
- **BRC-105** (BSV Association `x-bsv-*` headers) — BRC-29 key derivation for unique payment addresses. Works standalone or composes with BRC-103 mutual authentication.
|
|
12
|
+
|
|
13
|
+
## Getting started
|
|
14
|
+
|
|
15
|
+
Add to your Gemfile:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "x402-rack", git: "https://github.com/sgbett/x402-rack.git"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
See the [Architecture](architecture.md) guide for how the pieces fit together, or jump to the [API Reference](reference/index.md) for class and method documentation.
|
data/docs/schemes/brc-105.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# BRC-105 Scheme (BSV Association)
|
|
2
2
|
|
|
3
|
+
The official BSV Association [BRC-105](https://github.com/bitcoin-sv/BRCs/blob/master/payments/0105.md) payment protocol. Currently standalone (no-auth) mode — BRC-103 mutual authentication coming soon.
|
|
4
|
+
|
|
5
|
+
## Description
|
|
6
|
+
|
|
3
7
|
BRC-29 derived payment addresses with AtomicBEEF transactions. BSV Association `x-bsv-*` headers. Optional BRC-103 mutual authentication.
|
|
4
8
|
|
|
5
9
|
Unlike BSV-pay and BSV-proof, this scheme does not use partial transaction templates. The client builds the entire transaction using a derived payment address.
|
data/docs/schemes/bsv-pay.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# BSV-pay Scheme
|
|
2
2
|
|
|
3
|
+
The default gateway. Direct peer-to-peer payment — client pays server, server broadcasts to the network.
|
|
4
|
+
|
|
5
|
+
## Description
|
|
6
|
+
|
|
3
7
|
Server broadcasts via ARC. Uses Coinbase v2 header spec. Minimal infrastructure — ARC only, no treasury, no nonces.
|
|
4
8
|
|
|
5
9
|
## Headers
|
|
@@ -70,4 +74,4 @@ See [operations/performance.md](../operations/performance.md) for scaling consid
|
|
|
70
74
|
|
|
71
75
|
## Process Flow
|
|
72
76
|
|
|
73
|
-
See [process-flow/
|
|
77
|
+
See [process-flow/pay-gateway.md](../process-flow/pay-gateway.md) for sequence diagrams.
|
data/docs/schemes/bsv-proof.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# BSV-proof Scheme (merkleworks x402)
|
|
2
2
|
|
|
3
|
+
The [merkleworks x402](https://x402.merkleworks.io) implementation. Client broadcasts and proves payment via mempool.
|
|
4
|
+
|
|
5
|
+
!!! danger "Experimental — not production-ready"
|
|
6
|
+
The ProofGateway implementation is incomplete and under active development.
|
|
7
|
+
The nonce provider interface, Profile B provenance verification, and mempool
|
|
8
|
+
checking behaviour may change without notice. **Do not use in production.**
|
|
9
|
+
For production BSV payments, use [BSV-pay](bsv-pay.md) (PayGateway) or
|
|
10
|
+
[BRC-105](brc-105.md) (BRC105Gateway).
|
|
11
|
+
|
|
12
|
+
## Description
|
|
13
|
+
|
|
3
14
|
Client broadcasts, server checks mempool. Proof-of-payment model. Nonce-bound with request binding.
|
|
4
15
|
|
|
5
16
|
## Headers
|
|
@@ -22,8 +22,9 @@ module X402
|
|
|
22
22
|
PROTOCOL_ID = [2, "3241645161d8"].freeze
|
|
23
23
|
PROOF_HEADER = "x-bsv-payment"
|
|
24
24
|
NETWORK = "bsv:mainnet"
|
|
25
|
-
COMPRESSED_PUBKEY_HEX = /\A0[23][0-9a-
|
|
25
|
+
COMPRESSED_PUBKEY_HEX = /\A0[23][0-9a-f]{64}\z/
|
|
26
26
|
MAX_DERIVATION_BYTES = 64
|
|
27
|
+
PRINTABLE_ASCII = /\A[\x20-\x7E]+\z/
|
|
27
28
|
|
|
28
29
|
# @param key_deriver [BSV::Wallet::KeyDeriver] provides identity key + BRC-42 derivation
|
|
29
30
|
# @param prefix_store [#store!, #valid?, #consume!] replay protection for derivation prefixes
|
|
@@ -48,11 +49,15 @@ module X402
|
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
headers = {
|
|
52
|
+
"x-bsv-payment-version" => "1.0",
|
|
51
53
|
"x-bsv-payment-satoshis-required" => route.resolve_amount_sats.to_s,
|
|
52
54
|
"x-bsv-payment-derivation-prefix" => prefix
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
#
|
|
57
|
+
# The 402 challenge is issued before the client authenticates (no
|
|
58
|
+
# x-bsv-auth-identity-key yet). Include the server's identity key so
|
|
59
|
+
# the client knows who to derive the payment address for. When BRC-103
|
|
60
|
+
# mutual auth is already established, the client already has this key.
|
|
56
61
|
headers["x-bsv-payment-identity-key"] = @key_deriver.identity_key unless validated_brc103_key(rack_request)
|
|
57
62
|
|
|
58
63
|
headers
|
|
@@ -73,17 +78,23 @@ module X402
|
|
|
73
78
|
# @param route [X402::Configuration::Route]
|
|
74
79
|
# @return [SettlementResult]
|
|
75
80
|
def settle!(_header_name, proof_payload, rack_request, route)
|
|
81
|
+
# §7.1: fail fast if unauthenticated — before parsing untrusted payload
|
|
82
|
+
counterparty = resolve_counterparty(rack_request)
|
|
76
83
|
required_sats = route.resolve_amount_sats
|
|
77
84
|
payment = parse_payment(proof_payload)
|
|
78
85
|
prefix = payment["derivationPrefix"]
|
|
79
86
|
suffix = payment["derivationSuffix"]
|
|
80
87
|
validate_prefix_and_suffix!(prefix, suffix)
|
|
81
88
|
subject_tx = parse_beef_transaction(payment["transaction"])
|
|
89
|
+
log_derivation_inputs(prefix, suffix, counterparty)
|
|
82
90
|
expected_script = derive_payment_script(prefix, suffix, rack_request)
|
|
83
|
-
|
|
91
|
+
log_expected_script(expected_script)
|
|
92
|
+
log_tx_outputs(subject_tx, required_sats, expected_script)
|
|
93
|
+
paid_sats = verify_payment_output!(subject_tx, required_sats, expected_script)
|
|
84
94
|
consume_prefix!(prefix)
|
|
85
95
|
broadcast!(subject_tx)
|
|
86
|
-
|
|
96
|
+
log_settlement_success(subject_tx, paid_sats, required_sats)
|
|
97
|
+
build_settlement_result(subject_tx, paid_sats)
|
|
87
98
|
end
|
|
88
99
|
|
|
89
100
|
private
|
|
@@ -95,11 +106,12 @@ module X402
|
|
|
95
106
|
end
|
|
96
107
|
|
|
97
108
|
def validate_prefix_and_suffix!(prefix, suffix)
|
|
98
|
-
|
|
99
|
-
|
|
109
|
+
validate_hex!("derivationPrefix", prefix)
|
|
110
|
+
validate_suffix!("derivationSuffix", suffix)
|
|
100
111
|
end
|
|
101
112
|
|
|
102
|
-
|
|
113
|
+
# Prefix is server-generated hex (SecureRandom.hex).
|
|
114
|
+
def validate_hex!(name, value)
|
|
103
115
|
raise VerificationError.new("missing #{name}", status: 400) if value.nil?
|
|
104
116
|
unless value.is_a?(String) && !value.empty? &&
|
|
105
117
|
value.bytesize <= MAX_DERIVATION_BYTES && value.match?(/\A[0-9a-f]+\z/)
|
|
@@ -107,6 +119,17 @@ module X402
|
|
|
107
119
|
end
|
|
108
120
|
end
|
|
109
121
|
|
|
122
|
+
# Suffix is client-generated — the BRC-105 spec does not constrain the
|
|
123
|
+
# format. Reference implementations (BRC-121, bsv-x402) use base64.
|
|
124
|
+
# We accept any printable ASCII string within the size limit.
|
|
125
|
+
def validate_suffix!(name, value)
|
|
126
|
+
raise VerificationError.new("missing #{name}", status: 400) if value.nil?
|
|
127
|
+
unless value.is_a?(String) && !value.empty? &&
|
|
128
|
+
value.bytesize <= MAX_DERIVATION_BYTES && value.match?(PRINTABLE_ASCII)
|
|
129
|
+
raise VerificationError.new("invalid #{name} format", status: 400)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
110
133
|
def consume_prefix!(prefix)
|
|
111
134
|
return if @prefix_store.consume!(prefix)
|
|
112
135
|
|
|
@@ -138,42 +161,95 @@ module X402
|
|
|
138
161
|
::BSV::Script::Script.from_hex("76a914#{h160}88ac")
|
|
139
162
|
end
|
|
140
163
|
|
|
164
|
+
# BRC-105 §7.1: "If not authenticated, respond 401 Unauthorized."
|
|
165
|
+
# The client's identity key is required for BRC-42 key derivation.
|
|
141
166
|
def resolve_counterparty(rack_request)
|
|
142
|
-
|
|
167
|
+
key = rack_request.env["brc103.identity_key"]
|
|
168
|
+
return key if key.is_a?(String) && key.match?(COMPRESSED_PUBKEY_HEX)
|
|
169
|
+
|
|
170
|
+
if key.nil? || (key.is_a?(String) && key.empty?)
|
|
171
|
+
raise VerificationError.new("missing client identity key (x-bsv-auth-identity-key)", status: 401)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
raise VerificationError.new("invalid client identity key (x-bsv-auth-identity-key)", status: 401)
|
|
143
175
|
end
|
|
144
176
|
|
|
145
177
|
# Returns the validated BRC-103 identity key from the Rack env, or nil
|
|
146
178
|
# if absent or not a valid compressed public key hex.
|
|
179
|
+
# Used by challenge_headers to check for BRC-103 presence.
|
|
147
180
|
def validated_brc103_key(rack_request)
|
|
148
181
|
key = rack_request.env["brc103.identity_key"]
|
|
149
182
|
key if key.is_a?(String) && key.match?(COMPRESSED_PUBKEY_HEX)
|
|
150
183
|
end
|
|
151
184
|
|
|
152
185
|
def verify_payment_output!(transaction, required_sats, expected_script)
|
|
153
|
-
|
|
186
|
+
matching = transaction.outputs.find do |output|
|
|
154
187
|
output.locking_script == expected_script && output.satoshis >= required_sats
|
|
155
188
|
end
|
|
156
|
-
return if found
|
|
157
189
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
190
|
+
unless matching
|
|
191
|
+
raise VerificationError.new(
|
|
192
|
+
"no output pays >= #{required_sats} sats to derived address", status: 402
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
matching.satoshis
|
|
161
197
|
end
|
|
162
198
|
|
|
163
199
|
def broadcast!(transaction)
|
|
164
200
|
@arc_client.broadcast(transaction)
|
|
165
|
-
rescue StandardError
|
|
166
|
-
|
|
201
|
+
rescue StandardError => e
|
|
202
|
+
logger.error "[brc105] ARC broadcast error: #{e.class}: #{e.message}"
|
|
203
|
+
raise VerificationError.new("ARC broadcast failed: #{e.message}", status: 502)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# NOTE: x-bsv-payment-satoshis-paid reflects the amount claimed in the
|
|
207
|
+
# BEEF transaction, verified against the derived payment script but set
|
|
208
|
+
# before ARC broadcast confirmation. It is not an on-chain-confirmed
|
|
209
|
+
# value. Downstream consumers requiring confirmed amounts should use
|
|
210
|
+
# the txid to query chain state independently.
|
|
211
|
+
# --- Settlement logging (tagged [brc105]) ---
|
|
212
|
+
|
|
213
|
+
def logger
|
|
214
|
+
X402.configuration.logger
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def log_derivation_inputs(prefix, suffix, counterparty)
|
|
218
|
+
logger.info "[brc105] Derivation: prefix=#{prefix} suffix=#{suffix} counterparty=#{counterparty}"
|
|
219
|
+
logger.debug "[brc105] Key ID: #{prefix} #{suffix}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def log_expected_script(script)
|
|
223
|
+
logger.info "[brc105] Expected locking script: #{script.to_hex}"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def log_tx_outputs(transaction, required_sats, expected_script)
|
|
227
|
+
logger.info "[brc105] Verifying #{transaction.outputs.length} output(s) against #{required_sats} sats required"
|
|
228
|
+
transaction.outputs.each_with_index do |output, i|
|
|
229
|
+
script_match = output.locking_script == expected_script
|
|
230
|
+
sats_match = output.satoshis >= required_sats
|
|
231
|
+
logger.info "[brc105] output[#{i}]: #{output.satoshis} sats, " \
|
|
232
|
+
"script=#{output.locking_script.to_hex[0..15]}... " \
|
|
233
|
+
"script_match=#{script_match} sats_match=#{sats_match}"
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def log_settlement_success(transaction, paid_sats, required_sats)
|
|
238
|
+
logger.info "[brc105] Settlement OK: txid=#{transaction.txid_hex} " \
|
|
239
|
+
"paid=#{paid_sats} required=#{required_sats}"
|
|
167
240
|
end
|
|
168
241
|
|
|
169
|
-
def build_settlement_result(transaction)
|
|
242
|
+
def build_settlement_result(transaction, paid_sats)
|
|
170
243
|
receipt = {
|
|
171
244
|
"success" => true,
|
|
172
245
|
"transaction" => transaction.txid_hex,
|
|
173
246
|
"network" => NETWORK
|
|
174
247
|
}
|
|
175
248
|
SettlementResult.new(
|
|
176
|
-
receipt_headers: {
|
|
249
|
+
receipt_headers: {
|
|
250
|
+
"x-bsv-payment-satoshis-paid" => paid_sats.to_s,
|
|
251
|
+
"x-bsv-payment-result" => Base64.strict_encode64(JSON.generate(receipt))
|
|
252
|
+
},
|
|
177
253
|
txid: transaction.txid_hex,
|
|
178
254
|
network: NETWORK
|
|
179
255
|
)
|
data/lib/x402/bsv/pay_gateway.rb
CHANGED
|
@@ -44,15 +44,29 @@ module X402
|
|
|
44
44
|
@txid_store = txid_store
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# Build a 402 challenge with Coinbase v2 +Payment-Required+ header.
|
|
48
|
+
#
|
|
49
|
+
# @param rack_request [Rack::Request]
|
|
50
|
+
# @param route [X402::Configuration::Route]
|
|
51
|
+
# @return [Hash] challenge headers
|
|
47
52
|
def challenge_headers(rack_request, route)
|
|
48
53
|
challenge = build_challenge(rack_request, route)
|
|
49
54
|
{ "Payment-Required" => Base64.strict_encode64(JSON.generate(challenge)) }
|
|
50
55
|
end
|
|
51
56
|
|
|
57
|
+
# @return [Array<String>] proof header names this gateway responds to
|
|
52
58
|
def proof_header_names
|
|
53
59
|
["Payment-Signature"]
|
|
54
60
|
end
|
|
55
61
|
|
|
62
|
+
# Verify and broadcast a Coinbase v2 payment.
|
|
63
|
+
#
|
|
64
|
+
# @param _header_name [String] which proof header matched
|
|
65
|
+
# @param proof_payload [String] base64-encoded payment payload
|
|
66
|
+
# @param rack_request [Rack::Request]
|
|
67
|
+
# @param route [X402::Configuration::Route]
|
|
68
|
+
# @return [SettlementResult]
|
|
69
|
+
# @raise [VerificationError] on invalid payment, insufficient amount, or broadcast failure
|
|
56
70
|
def settle!(_header_name, proof_payload, rack_request, route)
|
|
57
71
|
required_sats = route.resolve_amount_sats
|
|
58
72
|
payload = decode_payment_payload(proof_payload)
|
|
@@ -41,6 +41,8 @@ module X402
|
|
|
41
41
|
|
|
42
42
|
# Record a prefix as issued.
|
|
43
43
|
#
|
|
44
|
+
# @param prefix [String] hex derivation prefix
|
|
45
|
+
# @return [void]
|
|
44
46
|
# @raise [StoreFullError] if max_issued unconsumed prefixes are held
|
|
45
47
|
def store!(prefix)
|
|
46
48
|
@monitor.synchronize do
|
|
@@ -53,6 +55,9 @@ module X402
|
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
# Non-binding read: returns true if the prefix was issued and not yet consumed.
|
|
58
|
+
#
|
|
59
|
+
# @param prefix [String] hex derivation prefix
|
|
60
|
+
# @return [Boolean]
|
|
56
61
|
def valid?(prefix)
|
|
57
62
|
@monitor.synchronize do
|
|
58
63
|
entry = @prefixes[prefix]
|
|
@@ -62,6 +67,9 @@ module X402
|
|
|
62
67
|
|
|
63
68
|
# Atomically mark a prefix as consumed. Returns false if already consumed,
|
|
64
69
|
# unknown, or expired.
|
|
70
|
+
#
|
|
71
|
+
# @param prefix [String] hex derivation prefix
|
|
72
|
+
# @return [Boolean] true if successfully consumed, false if replay/unknown/expired
|
|
65
73
|
def consume!(prefix)
|
|
66
74
|
@monitor.synchronize do
|
|
67
75
|
entry = @prefixes[prefix]
|
|
@@ -35,6 +35,12 @@ module X402
|
|
|
35
35
|
@arc_client = arc_client
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# Build a 402 challenge with merkleworks +X402-Challenge+ header.
|
|
39
|
+
#
|
|
40
|
+
# @param rack_request [Rack::Request]
|
|
41
|
+
# @param route [X402::Configuration::Route]
|
|
42
|
+
# @return [Hash] challenge headers
|
|
43
|
+
# @raise [ConfigurationError] if route uses callable amount_sats (fiat pricing)
|
|
38
44
|
def challenge_headers(rack_request, route)
|
|
39
45
|
if route.amount_sats.respond_to?(:call)
|
|
40
46
|
raise ConfigurationError,
|
|
@@ -44,10 +50,19 @@ module X402
|
|
|
44
50
|
{ "X402-Challenge" => challenge.to_header }
|
|
45
51
|
end
|
|
46
52
|
|
|
53
|
+
# @return [Array<String>] proof header names this gateway responds to
|
|
47
54
|
def proof_header_names
|
|
48
55
|
["X402-Proof"]
|
|
49
56
|
end
|
|
50
57
|
|
|
58
|
+
# Verify a merkleworks x402 proof against the challenge and check mempool.
|
|
59
|
+
#
|
|
60
|
+
# @param _header_name [String] which proof header matched
|
|
61
|
+
# @param proof_payload [String] base64url-encoded proof
|
|
62
|
+
# @param rack_request [Rack::Request]
|
|
63
|
+
# @param route [X402::Configuration::Route]
|
|
64
|
+
# @return [SettlementResult]
|
|
65
|
+
# @raise [VerificationError] on invalid proof, binding mismatch, or mempool failure
|
|
51
66
|
def settle!(_header_name, proof_payload, rack_request, route)
|
|
52
67
|
required_sats = route.resolve_amount_sats
|
|
53
68
|
proof = Proof.from_header(proof_payload)
|
data/lib/x402/configuration.rb
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module X402
|
|
4
|
+
# DSL for configuring X402 middleware, gateways, and protected routes.
|
|
5
|
+
#
|
|
6
|
+
# @example Minimal configuration
|
|
7
|
+
# X402.configure do |config|
|
|
8
|
+
# config.domain = "api.example.com"
|
|
9
|
+
# config.server_wif = ENV["SERVER_WIF"]
|
|
10
|
+
# config.arc_url = "https://arc.taal.com"
|
|
11
|
+
# config.enable :pay_gateway
|
|
12
|
+
# config.protect method: :GET, path: "/api/expensive", amount_sats: 100
|
|
13
|
+
# end
|
|
4
14
|
class Configuration
|
|
5
15
|
# Route holds a raw +amount_sats+ that may be an Integer or a callable.
|
|
6
16
|
# The +resolve_amount_sats+ method evaluates callables at access time,
|
|
@@ -37,15 +47,28 @@ module X402
|
|
|
37
47
|
|
|
38
48
|
attr_accessor :domain, :payee_locking_script_hex, :gateways,
|
|
39
49
|
:arc_url, :arc_api_key, :arc_client, :server_wif,
|
|
40
|
-
:exchange_rate_provider
|
|
50
|
+
:exchange_rate_provider, :logger
|
|
41
51
|
attr_reader :routes, :gateway_specs
|
|
42
52
|
|
|
43
53
|
def initialize
|
|
44
54
|
@routes = []
|
|
45
55
|
@gateways = []
|
|
46
56
|
@gateway_specs = []
|
|
57
|
+
@logger = default_logger
|
|
47
58
|
end
|
|
48
59
|
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def default_logger
|
|
63
|
+
require "logger"
|
|
64
|
+
logger = ::Logger.new($stderr)
|
|
65
|
+
logger.formatter = proc { |_sev, _time, _prog, msg| "#{msg}\n" }
|
|
66
|
+
logger.level = ::Logger::DEBUG
|
|
67
|
+
logger
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
public
|
|
71
|
+
|
|
49
72
|
# Record a gateway to be constructed later during +validate!+.
|
|
50
73
|
#
|
|
51
74
|
# @param name [Symbol] registered gateway name (e.g. +:pay_gateway+)
|
|
@@ -114,6 +137,13 @@ module X402
|
|
|
114
137
|
end
|
|
115
138
|
end
|
|
116
139
|
|
|
140
|
+
# Validate the configuration and construct gateways from specs.
|
|
141
|
+
#
|
|
142
|
+
# Called automatically at the end of +X402.configure+. Builds gateways
|
|
143
|
+
# from +enable+ specs, validates all constraints, and emits operational
|
|
144
|
+
# warnings for development defaults.
|
|
145
|
+
#
|
|
146
|
+
# @raise [ConfigurationError] if required fields are missing or invalid
|
|
117
147
|
def validate!
|
|
118
148
|
raise ConfigurationError, "domain is required" if domain.nil? || domain.empty?
|
|
119
149
|
|
|
@@ -140,9 +170,9 @@ module X402
|
|
|
140
170
|
end
|
|
141
171
|
return unless needs_warning
|
|
142
172
|
|
|
143
|
-
warn "[x402] challenge_secret is auto-generated. " \
|
|
144
|
-
|
|
145
|
-
|
|
173
|
+
logger.warn "[x402] challenge_secret is auto-generated. " \
|
|
174
|
+
"In-flight challenges will fail after process restart. " \
|
|
175
|
+
"Set challenge_secret: explicitly for production."
|
|
146
176
|
end
|
|
147
177
|
|
|
148
178
|
def warn_in_memory_prefix_store!
|
|
@@ -151,9 +181,9 @@ module X402
|
|
|
151
181
|
end
|
|
152
182
|
return unless needs_warning
|
|
153
183
|
|
|
154
|
-
warn "[x402] BRC105Gateway using in-memory PrefixStore. " \
|
|
155
|
-
|
|
156
|
-
|
|
184
|
+
logger.warn "[x402] BRC105Gateway using in-memory PrefixStore. " \
|
|
185
|
+
"No replay protection across processes. " \
|
|
186
|
+
"Use a shared backend (Redis) for multi-process deployments."
|
|
157
187
|
end
|
|
158
188
|
|
|
159
189
|
def validate_gateways!
|
data/lib/x402/middleware.rb
CHANGED
|
@@ -4,11 +4,30 @@ require "rack"
|
|
|
4
4
|
require "json"
|
|
5
5
|
|
|
6
6
|
module X402
|
|
7
|
+
# Pure Rack dispatcher for payment-gated HTTP.
|
|
8
|
+
#
|
|
9
|
+
# The middleware has no blockchain knowledge — it matches routes, issues
|
|
10
|
+
# payment challenges by polling configured gateways, and dispatches proofs
|
|
11
|
+
# to the matching gateway for settlement.
|
|
12
|
+
#
|
|
13
|
+
# @example config.ru
|
|
14
|
+
# X402.configure do |config|
|
|
15
|
+
# config.domain = "api.example.com"
|
|
16
|
+
# config.server_wif = ENV["SERVER_WIF"]
|
|
17
|
+
# config.arc_url = "https://arc.taal.com"
|
|
18
|
+
# config.enable :pay_gateway
|
|
19
|
+
# config.protect method: :GET, path: "/api/expensive", amount_sats: 100
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# use X402::Middleware
|
|
7
23
|
class Middleware
|
|
24
|
+
# @param app [#call] next Rack app in the middleware stack
|
|
8
25
|
def initialize(app)
|
|
9
26
|
@app = app
|
|
10
27
|
end
|
|
11
28
|
|
|
29
|
+
# @param env [Hash] Rack environment
|
|
30
|
+
# @return [Array(Integer, Hash, Array)] Rack response triple
|
|
12
31
|
def call(env)
|
|
13
32
|
request = Rack::Request.new(env)
|
|
14
33
|
config = X402.configuration
|
|
@@ -17,12 +36,21 @@ module X402
|
|
|
17
36
|
# Unprotected route — pass through
|
|
18
37
|
return @app.call(env) unless route
|
|
19
38
|
|
|
39
|
+
log = config.logger
|
|
40
|
+
log.info "[x402] #{request.request_method} #{request.path_info} " \
|
|
41
|
+
"— protected route, #{route.resolve_amount_sats} sats"
|
|
42
|
+
|
|
43
|
+
# BRC-104 §6.2: extract client identity key from x-bsv-auth-identity-key
|
|
44
|
+
extract_brc103_identity_key!(env, log)
|
|
45
|
+
|
|
20
46
|
# Check for a proof/payment header from any gateway
|
|
21
47
|
gateway, header_name, proof_payload = detect_proof(env, config)
|
|
22
48
|
|
|
23
49
|
if gateway
|
|
24
|
-
|
|
50
|
+
log.info "[x402] Proof header #{header_name} detected — dispatching to #{gateway.class.name}"
|
|
51
|
+
settle_and_forward(env, gateway, header_name, proof_payload, request, route, log)
|
|
25
52
|
else
|
|
53
|
+
log.info "[x402] No proof header — issuing 402 challenge"
|
|
26
54
|
issue_challenge(request, route, config)
|
|
27
55
|
end
|
|
28
56
|
end
|
|
@@ -40,6 +68,11 @@ module X402
|
|
|
40
68
|
nil
|
|
41
69
|
end
|
|
42
70
|
|
|
71
|
+
# Challenge response body follows BRC-105 §6.2 format.
|
|
72
|
+
# Settlement errors (e.g. underpayment 402, bad request 400) use the
|
|
73
|
+
# generic {"error": reason} shape via error_response — these are
|
|
74
|
+
# distinct: the challenge tells the client what to pay, whereas a
|
|
75
|
+
# settlement error explains why a submitted payment was rejected.
|
|
43
76
|
def issue_challenge(request, route, config)
|
|
44
77
|
headers = { "content-type" => "application/json" }
|
|
45
78
|
|
|
@@ -49,13 +82,20 @@ module X402
|
|
|
49
82
|
end
|
|
50
83
|
end
|
|
51
84
|
|
|
52
|
-
body = JSON.generate(
|
|
85
|
+
body = JSON.generate(
|
|
86
|
+
status: "error",
|
|
87
|
+
code: "ERR_PAYMENT_REQUIRED",
|
|
88
|
+
satoshisRequired: route.resolve_amount_sats,
|
|
89
|
+
description: "A BSV payment is required to access this resource."
|
|
90
|
+
)
|
|
53
91
|
[402, headers, [body]]
|
|
54
92
|
end
|
|
55
93
|
|
|
56
|
-
def settle_and_forward(env, gateway, header_name, proof_payload, request, route)
|
|
94
|
+
def settle_and_forward(env, gateway, header_name, proof_payload, request, route, log)
|
|
57
95
|
result = gateway.settle!(header_name, proof_payload, request, route)
|
|
58
96
|
|
|
97
|
+
log.info "[x402] Settlement OK — txid=#{result.txid}"
|
|
98
|
+
|
|
59
99
|
status, headers, body = @app.call(env)
|
|
60
100
|
|
|
61
101
|
# Merge any receipt headers from the gateway
|
|
@@ -67,10 +107,13 @@ module X402
|
|
|
67
107
|
|
|
68
108
|
[status, headers, body]
|
|
69
109
|
rescue X402::VerificationError => e
|
|
110
|
+
log.warn "[x402] Settlement failed: #{e.status} #{e.reason}"
|
|
70
111
|
error_response(e.status, e.reason)
|
|
71
112
|
rescue X402::Error => e
|
|
113
|
+
log.warn "[x402] Error: #{e.message}"
|
|
72
114
|
error_response(400, e.message)
|
|
73
|
-
rescue StandardError
|
|
115
|
+
rescue StandardError => e
|
|
116
|
+
log.error "[x402] Unexpected error: #{e.class}: #{e.message}"
|
|
74
117
|
error_response(500, "internal error")
|
|
75
118
|
end
|
|
76
119
|
|
|
@@ -79,6 +122,29 @@ module X402
|
|
|
79
122
|
[status, { "content-type" => "application/json" }, [body]]
|
|
80
123
|
end
|
|
81
124
|
|
|
125
|
+
# BRC-104 §6.2: x-bsv-auth-identity-key carries the client's public
|
|
126
|
+
# identity key (33-byte compressed secp256k1 pubkey, hex). Populate
|
|
127
|
+
# brc103.identity_key in the Rack env so gateways can use it as the
|
|
128
|
+
# counterparty in BRC-42 key derivation.
|
|
129
|
+
#
|
|
130
|
+
# NOTE: This is the CLAIMED identity key — not authenticated. BRC-103
|
|
131
|
+
# signature verification must occur in a separate middleware if identity
|
|
132
|
+
# assertions are required for authorisation decisions.
|
|
133
|
+
#
|
|
134
|
+
# Does not overwrite an identity key already set by upstream middleware
|
|
135
|
+
# (e.g. a BRC-103/104 auth layer that has verified the signature).
|
|
136
|
+
def extract_brc103_identity_key!(env, log)
|
|
137
|
+
return if env["brc103.identity_key"].is_a?(String) && !env["brc103.identity_key"].empty?
|
|
138
|
+
|
|
139
|
+
key = env["HTTP_X_BSV_AUTH_IDENTITY_KEY"]
|
|
140
|
+
if key && !key.empty?
|
|
141
|
+
log.info "[x402] Client identity key: #{key[0..15]}..."
|
|
142
|
+
env["brc103.identity_key"] = key.downcase
|
|
143
|
+
else
|
|
144
|
+
log.debug "[x402] No x-bsv-auth-identity-key header"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
82
148
|
def rack_header_key(http_header_name)
|
|
83
149
|
"HTTP_#{http_header_name.upcase.tr("-", "_")}"
|
|
84
150
|
end
|
|
@@ -55,6 +55,8 @@ module X402
|
|
|
55
55
|
@on_payment = on_payment
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
# @param env [Hash] Rack environment
|
|
59
|
+
# @return [Array(Integer, Hash, Array)] Rack response triple (always passes through)
|
|
58
60
|
def call(env)
|
|
59
61
|
observe_payment(env)
|
|
60
62
|
@app.call(env)
|
|
@@ -129,8 +131,11 @@ module X402
|
|
|
129
131
|
|
|
130
132
|
# Built-in extractor for the Coinbase v2 envelope format.
|
|
131
133
|
# Decodes +Base64(JSON({ payload: { rawtx: "hex" } }))+.
|
|
132
|
-
#
|
|
134
|
+
#
|
|
135
|
+
# @see PaymentObserver extractor: parameter
|
|
133
136
|
class CoinbaseV2Extractor
|
|
137
|
+
# @param proof_payload [String] raw proof header value
|
|
138
|
+
# @return [BSV::Transaction::Transaction, nil] parsed transaction or nil on failure
|
|
134
139
|
def call(proof_payload)
|
|
135
140
|
json = Base64.strict_decode64(proof_payload)
|
|
136
141
|
payload = JSON.parse(json)
|
|
@@ -146,11 +151,16 @@ module X402
|
|
|
146
151
|
|
|
147
152
|
# Built-in recogniser for a single static payee address.
|
|
148
153
|
# Used when +payee_locking_script_hex+ is provided without a custom recogniser.
|
|
154
|
+
#
|
|
155
|
+
# @see PaymentObserver recogniser: parameter
|
|
149
156
|
class StaticRecogniser
|
|
157
|
+
# @param payee_locking_script_hex [String] hex-encoded locking script to match
|
|
150
158
|
def initialize(payee_locking_script_hex)
|
|
151
159
|
@payee_hex = ::BSV::Script::Script.from_hex(payee_locking_script_hex).to_hex
|
|
152
160
|
end
|
|
153
161
|
|
|
162
|
+
# @param locking_script_hex [String] hex-encoded locking script to test
|
|
163
|
+
# @return [Boolean] true if the script matches the configured payee
|
|
154
164
|
def ours?(locking_script_hex)
|
|
155
165
|
locking_script_hex == @payee_hex
|
|
156
166
|
end
|
data/lib/x402/version.rb
CHANGED
data/mkdocs.yml
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
site_name: x402-rack
|
|
2
|
+
site_description: Rack middleware for payment-gated HTTP using BSV
|
|
3
|
+
site_url: https://sgbett.github.io/x402-rack/
|
|
4
|
+
repo_url: https://github.com/sgbett/x402-rack
|
|
5
|
+
repo_name: sgbett/x402-rack
|
|
6
|
+
|
|
7
|
+
theme:
|
|
8
|
+
name: material
|
|
9
|
+
palette:
|
|
10
|
+
- media: '(prefers-color-scheme: light)'
|
|
11
|
+
scheme: default
|
|
12
|
+
primary: deep purple
|
|
13
|
+
accent: amber
|
|
14
|
+
toggle:
|
|
15
|
+
icon: material/brightness-7
|
|
16
|
+
name: Switch to dark mode
|
|
17
|
+
- media: '(prefers-color-scheme: dark)'
|
|
18
|
+
scheme: slate
|
|
19
|
+
primary: deep purple
|
|
20
|
+
accent: amber
|
|
21
|
+
toggle:
|
|
22
|
+
icon: material/brightness-4
|
|
23
|
+
name: Switch to light mode
|
|
24
|
+
features:
|
|
25
|
+
- navigation.sections
|
|
26
|
+
- navigation.expand
|
|
27
|
+
- navigation.top
|
|
28
|
+
- search.suggest
|
|
29
|
+
- content.code.copy
|
|
30
|
+
|
|
31
|
+
markdown_extensions:
|
|
32
|
+
- admonition
|
|
33
|
+
- pymdownx.details
|
|
34
|
+
- pymdownx.superfences:
|
|
35
|
+
custom_fences:
|
|
36
|
+
- name: mermaid
|
|
37
|
+
class: mermaid
|
|
38
|
+
format: !!python/name:pymdownx.superfences.fence_code_format
|
|
39
|
+
- pymdownx.highlight:
|
|
40
|
+
anchor_linenums: true
|
|
41
|
+
- pymdownx.inlinehilite
|
|
42
|
+
- toc:
|
|
43
|
+
permalink: true
|
|
44
|
+
|
|
45
|
+
plugins:
|
|
46
|
+
- search
|
|
47
|
+
- minify:
|
|
48
|
+
minify_html: true
|
|
49
|
+
|
|
50
|
+
nav:
|
|
51
|
+
- Home: index.md
|
|
52
|
+
- Architecture: architecture.md
|
|
53
|
+
- Ecosystem: ecosystem.md
|
|
54
|
+
- Schemes:
|
|
55
|
+
- BSV-pay: schemes/bsv-pay.md
|
|
56
|
+
- BSV-proof: schemes/bsv-proof.md
|
|
57
|
+
- BRC-105: schemes/brc-105.md
|
|
58
|
+
- Process Flows:
|
|
59
|
+
- PayGateway: process-flow/pay-gateway.md
|
|
60
|
+
- ProofGateway: process-flow/proof-gateway.md
|
|
61
|
+
- BRC105Gateway: process-flow/brc105-gateway.md
|
|
62
|
+
- Operations:
|
|
63
|
+
- Deployment: operations/deployment.md
|
|
64
|
+
- Performance: operations/performance.md
|
|
65
|
+
- Treasury: operations/treasury.md
|
|
66
|
+
- Client Integration: client-integration.md
|
|
67
|
+
- Security: security.md
|
|
68
|
+
- API Reference: reference/index.md
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: x402-rack
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Simon Bettison
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-04-
|
|
10
|
+
date: 2026-04-05 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: base64
|
|
@@ -91,6 +91,7 @@ files:
|
|
|
91
91
|
- ".claude/plans/20260326-rack-stack-architecture.md"
|
|
92
92
|
- ".rspec"
|
|
93
93
|
- ".rubocop.yml"
|
|
94
|
+
- ".yardopts"
|
|
94
95
|
- CHANGELOG.md
|
|
95
96
|
- CLAUDE.md
|
|
96
97
|
- DESIGN.md
|
|
@@ -100,12 +101,14 @@ files:
|
|
|
100
101
|
- docs/architecture.md
|
|
101
102
|
- docs/client-integration.md
|
|
102
103
|
- docs/ecosystem.md
|
|
104
|
+
- docs/index.md
|
|
103
105
|
- docs/operations/deployment.md
|
|
104
106
|
- docs/operations/performance.md
|
|
105
107
|
- docs/operations/treasury.md
|
|
106
108
|
- docs/process-flow/brc105-gateway.md
|
|
107
109
|
- docs/process-flow/pay-gateway.md
|
|
108
110
|
- docs/process-flow/proof-gateway.md
|
|
111
|
+
- docs/requirements.txt
|
|
109
112
|
- docs/schemes/brc-105.md
|
|
110
113
|
- docs/schemes/bsv-pay.md
|
|
111
114
|
- docs/schemes/bsv-proof.md
|
|
@@ -130,6 +133,7 @@ files:
|
|
|
130
133
|
- lib/x402/settlement_worker.rb
|
|
131
134
|
- lib/x402/verification/protocol_checks.rb
|
|
132
135
|
- lib/x402/version.rb
|
|
136
|
+
- mkdocs.yml
|
|
133
137
|
- sig/x402.rbs
|
|
134
138
|
homepage: https://github.com/sgbett/x402-rack
|
|
135
139
|
licenses:
|