blockchain0x-x402 0.0.1.alpha.0 → 0.0.1.alpha.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be9ad30aa23e7e4d44c0dd712254925bb187f4f0c78567aec39c7ea3d4f98587
4
- data.tar.gz: 22a5ab6a9635274a08a72dfe3f6fd0f06ecadbcdaf0b1ef05b849673fe5355df
3
+ metadata.gz: ee6cf734c4809fd53f64cb3a26a1f07710bd0d502008bb07e178f8c37b83bd5f
4
+ data.tar.gz: 9dfbf939eaa222810a74cc6cf502a5af8958077147843b270a15011e73ae7297
5
5
  SHA512:
6
- metadata.gz: 35deca80a3287f50c577ac6935a83be33d39268af3fc008972c3d55154d90041a2e0bcca4082689b13b6e0c1f42e0c8d63c4f5aaba9895afe1ce2fe8fd6694b0
7
- data.tar.gz: 385941f182db7f338f0cd26b9a63ab8c90a368535f2323ee869cfc1b7d39acfdaed09e08a2cef83fa95c0712403274b15f7f6cff3e083ddd8acce7bcaa3a6592
6
+ metadata.gz: 59eb62699d8f3941e2c9edd2f40051a5e8015a4001a42ae10c8cd83419a867809b426c8baaef47100bfcee3aea15023c1e99373437461cca6b9fcb903f4ec728
7
+ data.tar.gz: fb89d30a00d8ac9fd262846d8ff3ca300408ce45bafa74927477f93e2a9688a2b7a1afa1060f1d0e178ab87a0f767ecdb6fb374ab2dd216cf03d8b85a0a49a95
@@ -21,6 +21,14 @@
21
21
  # wrapper propagates that response unchanged so the caller can
22
22
  # decide whether to loop or surface to the user.
23
23
  #
24
+ # Spend policy (sub-plan 27.1 A6): the 402 challenge is
25
+ # ATTACKER-CONTROLLED input. Callers MUST set `max_amount_wei`
26
+ # (and should set `allowed_pay_to`) so a malicious or compromised
27
+ # seller cannot drain the session allowance by quoting an
28
+ # arbitrary amount/recipient. The wrapper also enforces the
29
+ # requirement's maxAgeSeconds and refuses 402s that match no
30
+ # known network instead of guessing.
31
+ #
24
32
  # The `sdk` is duck-typed: any object responding to
25
33
  # `network`, `payments_create(args)`, and `transactions_get(id)`
26
34
  # works. Tests mock the surface; production passes a
@@ -39,24 +47,40 @@ module Blockchain0xX402
39
47
 
40
48
  # @param sdk [#network, #payments_create, #transactions_get]
41
49
  # @param agent_id [String] wallet that funds the on-chain payment
50
+ # @param max_amount_wei [String, nil] spend ceiling in 6-dp USDC base
51
+ # units (27.1 A6). SET THIS - a 402 quoting more is refused with
52
+ # ClientError 'amount_over_cap' before any payment is created.
53
+ # @param allowed_pay_to [Array<String>, nil] optional recipient
54
+ # allowlist (27.1 A6); compared case-insensitively.
42
55
  # @param confirm_timeout_seconds [Integer]
43
56
  # @param confirm_poll_seconds [Float]
44
57
  # @param connection [Faraday::Connection, nil] test seam
45
58
  # @param sleep_proc [#call, nil] test seam for the poll loop
59
+ # @param now_proc [#call, nil] test seam for the maxAgeSeconds clock
46
60
  def initialize(
47
61
  sdk:,
48
62
  agent_id:,
63
+ max_amount_wei: nil,
64
+ allowed_pay_to: nil,
49
65
  confirm_timeout_seconds: DEFAULT_CONFIRM_TIMEOUT_SECONDS,
50
66
  confirm_poll_seconds: DEFAULT_CONFIRM_POLL_SECONDS,
51
67
  connection: nil,
52
- sleep_proc: nil
68
+ sleep_proc: nil,
69
+ now_proc: nil
53
70
  )
71
+ if !max_amount_wei.nil? && !max_amount_wei.to_s.match?(/\A[0-9]+\z/)
72
+ raise ArgumentError, 'max_amount_wei must be an integer string of 6-dp USDC base units'
73
+ end
74
+
54
75
  @sdk = sdk
55
76
  @agent_id = agent_id
77
+ @max_amount_wei = max_amount_wei&.to_s
78
+ @allowed_pay_to = allowed_pay_to&.map(&:downcase)
56
79
  @confirm_timeout = confirm_timeout_seconds
57
80
  @confirm_poll = confirm_poll_seconds
58
81
  @conn = connection || Faraday.new
59
82
  @sleep = sleep_proc || Kernel.method(:sleep)
83
+ @now = now_proc || method(:monotonic_now)
60
84
  end
61
85
 
62
86
  # Perform an HTTP request that handles 402 automatically.
@@ -70,7 +94,11 @@ module Blockchain0xX402
70
94
  return first unless first.status == 402
71
95
 
72
96
  spec = Wire.parse_402_response(first)
97
+ challenge_at = @now.call
73
98
  requirement = pick_requirement(spec.accepts)
99
+ # 27.1 A6: the 402 is attacker-controlled input - enforce the
100
+ # caller's spend policy BEFORE any payment is created.
101
+ enforce_spend_policy(requirement)
74
102
  payment = @sdk.payments_create(
75
103
  agent_id: @agent_id,
76
104
  to: requirement.pay_to_address,
@@ -80,6 +108,18 @@ module Blockchain0xX402
80
108
  raise ClientError.new('chain_failed', 'payments_create did not return an id.') if payment_id.nil?
81
109
 
82
110
  confirmed = wait_for_confirmation(payment_id)
111
+ # 27.1 A6: a proof that confirmed after the requirement's
112
+ # maxAgeSeconds window is refused client-side, not presented.
113
+ if requirement.max_age_seconds
114
+ elapsed = @now.call - challenge_at
115
+ if elapsed > requirement.max_age_seconds
116
+ raise ClientError.new(
117
+ 'stale_challenge',
118
+ "challenge expired: confirmation took #{elapsed.round}s, " \
119
+ "maxAgeSeconds is #{requirement.max_age_seconds}.",
120
+ )
121
+ end
122
+ end
83
123
 
84
124
  header = Wire.build_payment_header(
85
125
  Wire::ExactUsdcPayment.new(
@@ -122,6 +162,24 @@ module Blockchain0xX402
122
162
  )
123
163
  end
124
164
 
165
+ # 27.1 A6: spend-policy gate, evaluated before any payment exists.
166
+ def enforce_spend_policy(requirement)
167
+ if @max_amount_wei && Integer(requirement.amount_wei_usdc, 10) > Integer(@max_amount_wei, 10)
168
+ raise ClientError.new(
169
+ 'amount_over_cap',
170
+ "402 quotes #{requirement.amount_wei_usdc} wei USDC, above the " \
171
+ "configured max_amount_wei #{@max_amount_wei}.",
172
+ )
173
+ end
174
+ return unless @allowed_pay_to && !@allowed_pay_to.include?(requirement.pay_to_address.downcase)
175
+
176
+ raise ClientError.new(
177
+ 'recipient_not_allowed',
178
+ "402 pay-to address #{requirement.pay_to_address} is not in the " \
179
+ 'configured allowed_pay_to list.',
180
+ )
181
+ end
182
+
125
183
  def wait_for_confirmation(payment_id)
126
184
  deadline = monotonic_now + @confirm_timeout
127
185
  last = nil
@@ -35,8 +35,14 @@ module Blockchain0xX402
35
35
  # challenge. Codes:
36
36
  #
37
37
  # no_matching_requirement - no accepts entry matched the SDK network
38
+ # (or the network is unknown; sub-plan 27.1
39
+ # A6 - a refusal, never an accepts[0] guess)
38
40
  # settlement_timeout - on-chain payment did not confirm in budget
39
41
  # chain_failed - payment status flipped to failed
42
+ # amount_over_cap - 402 quoted above max_amount_wei (27.1 A6)
43
+ # recipient_not_allowed - payToAddress outside allowed_pay_to (27.1 A6)
44
+ # stale_challenge - confirmation landed after the requirement's
45
+ # maxAgeSeconds window (27.1 A6)
40
46
  class ClientError < Error
41
47
  end
42
48
  end
@@ -5,5 +5,5 @@
5
5
  # frozen_string_literal: true
6
6
 
7
7
  module Blockchain0xX402
8
- VERSION = '0.0.1.alpha.0'
8
+ VERSION = '0.0.1.alpha.1'
9
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blockchain0x-x402
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1.alpha.0
4
+ version: 0.0.1.alpha.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Blockchain0x
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-30 00:00:00.000000000 Z
11
+ date: 2026-06-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -30,6 +30,20 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '3.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: base64
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0.2'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0.2'
33
47
  - !ruby/object:Gem::Dependency
34
48
  name: rspec
35
49
  requirement: !ruby/object:Gem::Requirement
@@ -65,10 +79,10 @@ licenses:
65
79
  - Apache-2.0
66
80
  metadata:
67
81
  homepage_uri: https://blockchain0x.com
68
- source_code_uri: https://github.com/Tosh-Labs/blockchain0x-app/tree/dev/packages/sdk-ruby-x402
69
- bug_tracker_uri: https://github.com/Tosh-Labs/blockchain0x-app/issues
82
+ source_code_uri: https://github.com/tosh-labs/blockchain0x-app/tree/dev/packages/sdk-ruby-x402
83
+ bug_tracker_uri: https://github.com/tosh-labs/blockchain0x-app/issues
70
84
  documentation_uri: https://docs.blockchain0x.com
71
- changelog_uri: https://github.com/Tosh-Labs/blockchain0x-x402-ruby/blob/main/CHANGELOG.md
85
+ changelog_uri: https://github.com/tosh-labs/blockchain0x-x402-ruby/blob/main/CHANGELOG.md
72
86
  rubygems_mfa_required: 'true'
73
87
  post_install_message:
74
88
  rdoc_options: []