remitmd 0.1.7 → 0.1.8

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: b29ac02ca95fe1053bed848cca89111698f1744bbeb145681a76a3f9ed2d8e66
4
- data.tar.gz: d55c4f33eaaa686fa0036e3a0566ee6382a0a544c01093a3d9887e745b2eaa01
3
+ metadata.gz: 8c2b03f3cb2f1a05e9a33845448154e4d34fda42f65af31823be2f30fb14b6fe
4
+ data.tar.gz: 3d6dc094058152593779b5430899400d7b9d6543693ee3dd35dfd08b88f4c131
5
5
  SHA512:
6
- metadata.gz: 9c00d48fc571e6975d9dfd32c679d5b9cf46205742a8a725022907eb997d42de7a61ae040c22dfafb7d5a994277d25f5dafd2e589611e849f94a70c692d71102
7
- data.tar.gz: dff3c27cf317ce994999efd38d57f1173f0a1136da0bf4dbcdbf1ff5879a9f737bef96875802108ca130b4e46b3b94eec1cc0f52cace3152c89357018d9f9621
6
+ metadata.gz: deca4edd8beb6ac2d798102a83838762d746f4508bf5e852bb1b43d74aed1fa24a7b328bc4880560fc5ab60f5bd2c937277123183c3c45cc430bf02407f5c5c8
7
+ data.tar.gz: c8b3bd0dc87584d00abe0b3acb37a52fa16f1f9af337780dd6aecc861f2cd542d2d684d636b28bef6b0227c13002c3c70388809d2e38a2874255bde0dd1c3932
data/README.md CHANGED
@@ -24,7 +24,7 @@ gem install remitmd
24
24
  ```ruby
25
25
  require "remitmd"
26
26
 
27
- wallet = Remitmd::RemitWallet.new(private_key: ENV["REMITMD_PRIVATE_KEY"])
27
+ wallet = Remitmd::RemitWallet.new(private_key: ENV["REMITMD_KEY"])
28
28
 
29
29
  # Direct payment
30
30
  tx = wallet.pay("0xRecipient0000000000000000000000000000001", 1.50)
@@ -39,12 +39,32 @@ Or from environment variables:
39
39
 
40
40
  ```ruby
41
41
  wallet = Remitmd::RemitWallet.from_env
42
- # Requires: REMITMD_PRIVATE_KEY
42
+ # Requires: REMITMD_KEY (or REMIT_SIGNER_URL + REMIT_SIGNER_TOKEN)
43
43
  # Optional: REMITMD_CHAIN (default: "base"), REMITMD_API_URL
44
44
  ```
45
45
 
46
46
  Permits are auto-signed. Every payment method fetches the on-chain USDC nonce, signs an EIP-2612 permit, and includes it automatically.
47
47
 
48
+ ## Local Signer (Recommended)
49
+
50
+ The local signer delegates key management to `remit signer`, a localhost HTTP server that holds your encrypted key. Your agent only needs a URL and token — no private key in the environment.
51
+
52
+ ```bash
53
+ export REMIT_SIGNER_URL=http://127.0.0.1:7402
54
+ export REMIT_SIGNER_TOKEN=rmit_sk_...
55
+ ```
56
+
57
+ ```ruby
58
+ # Explicit
59
+ signer = Remitmd::HttpSigner.new(url: "http://127.0.0.1:7402", token: "rmit_sk_...")
60
+ wallet = Remitmd::RemitWallet.new(signer: signer)
61
+
62
+ # Or auto-detect from env (recommended)
63
+ wallet = Remitmd::RemitWallet.from_env # detects REMIT_SIGNER_URL automatically
64
+ ```
65
+
66
+ `RemitWallet.from_env` detects signer credentials automatically. Priority: `REMIT_SIGNER_URL` > `REMITMD_KEY`.
67
+
48
68
  ## Payment Models
49
69
 
50
70
  ### Direct Payment
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
7
+ module Remitmd
8
+ # Signer backed by a local HTTP signing server.
9
+ #
10
+ # Delegates digest signing to an HTTP server (typically
11
+ # `http://127.0.0.1:7402`). The signer server holds the encrypted key;
12
+ # this adapter only needs a bearer token and URL.
13
+ #
14
+ # @example
15
+ # signer = Remitmd::HttpSigner.new(url: "http://127.0.0.1:7402", token: "rmit_sk_...")
16
+ # wallet = Remitmd::RemitWallet.new(signer: signer, chain: "base")
17
+ #
18
+ class HttpSigner
19
+ include Signer
20
+
21
+ # Create an HttpSigner, fetching and caching the wallet address.
22
+ #
23
+ # @param url [String] signer server URL (e.g. "http://127.0.0.1:7402")
24
+ # @param token [String] bearer token for authentication
25
+ # @raise [RemitError] if the server is unreachable, returns an error, or returns no address
26
+ def initialize(url:, token:)
27
+ @url = url.chomp("/")
28
+ @token = token
29
+ @address = fetch_address
30
+ end
31
+
32
+ # Sign a 32-byte digest (raw binary bytes).
33
+ # Posts to /sign/digest with the hex-encoded digest.
34
+ # Returns a 0x-prefixed 65-byte hex signature.
35
+ #
36
+ # @param digest_bytes [String] 32-byte binary digest
37
+ # @return [String] 0x-prefixed 65-byte hex signature
38
+ # @raise [RemitError] on network, auth, policy, or server errors
39
+ def sign(digest_bytes)
40
+ hex = "0x#{digest_bytes.unpack1("H*")}"
41
+ uri = URI("#{@url}/sign/digest")
42
+ http = build_http(uri)
43
+
44
+ req = Net::HTTP::Post.new(uri.path)
45
+ req["Content-Type"] = "application/json"
46
+ req["Authorization"] = "Bearer #{@token}"
47
+ req.body = { digest: hex }.to_json
48
+
49
+ resp = begin
50
+ http.request(req)
51
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError => e
52
+ raise RemitError.new(
53
+ RemitError::NETWORK_ERROR,
54
+ "HttpSigner: cannot reach signer server at #{@url}: #{e.message}"
55
+ )
56
+ end
57
+
58
+ handle_sign_response(resp)
59
+ end
60
+
61
+ # The cached Ethereum address (0x-prefixed).
62
+ # @return [String]
63
+ attr_reader :address
64
+
65
+ # Never expose the bearer token in inspect/to_s output.
66
+ def inspect
67
+ "#<Remitmd::HttpSigner address=#{@address}>"
68
+ end
69
+
70
+ alias to_s inspect
71
+
72
+ private
73
+
74
+ # Fetch the wallet address from GET /address during construction.
75
+ # @return [String] the 0x-prefixed Ethereum address
76
+ # @raise [RemitError] on any failure
77
+ def fetch_address
78
+ uri = URI("#{@url}/address")
79
+ http = build_http(uri)
80
+
81
+ req = Net::HTTP::Get.new(uri.path)
82
+ req["Authorization"] = "Bearer #{@token}"
83
+
84
+ resp = begin
85
+ http.request(req)
86
+ rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError => e
87
+ raise RemitError.new(
88
+ RemitError::NETWORK_ERROR,
89
+ "HttpSigner: cannot reach signer server at #{@url}: #{e.message}"
90
+ )
91
+ end
92
+
93
+ status = resp.code.to_i
94
+
95
+ if status == 401
96
+ raise RemitError.new(
97
+ RemitError::UNAUTHORIZED,
98
+ "HttpSigner: unauthorized -- check your REMIT_SIGNER_TOKEN"
99
+ )
100
+ end
101
+
102
+ unless (200..299).cover?(status)
103
+ raise RemitError.new(
104
+ RemitError::SERVER_ERROR,
105
+ "HttpSigner: GET /address failed (#{status})"
106
+ )
107
+ end
108
+
109
+ body = begin
110
+ JSON.parse(resp.body.to_s)
111
+ rescue JSON::ParserError
112
+ raise RemitError.new(
113
+ RemitError::SERVER_ERROR,
114
+ "HttpSigner: GET /address returned malformed JSON"
115
+ )
116
+ end
117
+
118
+ addr = body["address"]
119
+ if addr.nil? || addr.to_s.empty?
120
+ raise RemitError.new(
121
+ RemitError::SERVER_ERROR,
122
+ "HttpSigner: GET /address returned no address"
123
+ )
124
+ end
125
+
126
+ addr.to_s
127
+ end
128
+
129
+ # Handle the response from POST /sign/digest.
130
+ # @param resp [Net::HTTPResponse]
131
+ # @return [String] the 0x-prefixed hex signature
132
+ # @raise [RemitError] on any error
133
+ def handle_sign_response(resp)
134
+ status = resp.code.to_i
135
+
136
+ if status == 401
137
+ raise RemitError.new(
138
+ RemitError::UNAUTHORIZED,
139
+ "HttpSigner: unauthorized -- check your REMIT_SIGNER_TOKEN"
140
+ )
141
+ end
142
+
143
+ if status == 403
144
+ reason = begin
145
+ data = JSON.parse(resp.body.to_s)
146
+ data["reason"] || "unknown"
147
+ rescue JSON::ParserError
148
+ "unknown"
149
+ end
150
+ raise RemitError.new(
151
+ RemitError::UNAUTHORIZED,
152
+ "HttpSigner: policy denied -- #{reason}"
153
+ )
154
+ end
155
+
156
+ unless (200..299).cover?(status)
157
+ detail = begin
158
+ data = JSON.parse(resp.body.to_s)
159
+ data["reason"] || data["error"] || "server error"
160
+ rescue JSON::ParserError
161
+ "server error"
162
+ end
163
+ raise RemitError.new(
164
+ RemitError::SERVER_ERROR,
165
+ "HttpSigner: sign failed (#{status}): #{detail}"
166
+ )
167
+ end
168
+
169
+ body = begin
170
+ JSON.parse(resp.body.to_s)
171
+ rescue JSON::ParserError
172
+ raise RemitError.new(
173
+ RemitError::SERVER_ERROR,
174
+ "HttpSigner: POST /sign/digest returned malformed JSON"
175
+ )
176
+ end
177
+
178
+ sig = body["signature"]
179
+ if sig.nil? || sig.to_s.empty?
180
+ raise RemitError.new(
181
+ RemitError::SERVER_ERROR,
182
+ "HttpSigner: server returned no signature"
183
+ )
184
+ end
185
+
186
+ sig.to_s
187
+ end
188
+
189
+ # Build a Net::HTTP client for the given URI.
190
+ # @param uri [URI] the target URI
191
+ # @return [Net::HTTP]
192
+ def build_http(uri)
193
+ http = Net::HTTP.new(uri.host, uri.port)
194
+ http.use_ssl = uri.scheme == "https"
195
+ http.open_timeout = 5
196
+ http.read_timeout = 10
197
+ http
198
+ end
199
+ end
200
+ end
@@ -67,18 +67,31 @@ module Remitmd
67
67
  end
68
68
 
69
69
  # Build a RemitWallet from environment variables.
70
- # Reads: REMITMD_KEY (primary) or REMITMD_PRIVATE_KEY (deprecated fallback),
71
- # REMITMD_CHAIN, REMITMD_API_URL, REMITMD_ROUTER_ADDRESS.
70
+ # Reads: REMIT_SIGNER_URL + REMIT_SIGNER_TOKEN (preferred, uses HttpSigner),
71
+ # or REMITMD_KEY (primary) / REMITMD_PRIVATE_KEY (deprecated fallback).
72
+ # Also reads: REMITMD_CHAIN, REMITMD_API_URL, REMITMD_ROUTER_ADDRESS.
72
73
  def self.from_env
74
+ chain = ENV.fetch("REMITMD_CHAIN", "base")
75
+ api_url = ENV["REMITMD_API_URL"]
76
+ router_address = ENV["REMITMD_ROUTER_ADDRESS"]
77
+
78
+ # Priority 1: HTTP signer server
79
+ signer_url = ENV["REMIT_SIGNER_URL"]
80
+ if signer_url
81
+ signer_token = ENV["REMIT_SIGNER_TOKEN"]
82
+ raise ArgumentError, "REMIT_SIGNER_TOKEN must be set when REMIT_SIGNER_URL is set" unless signer_token
83
+
84
+ signer = HttpSigner.new(url: signer_url, token: signer_token)
85
+ return new(signer: signer, chain: chain, api_url: api_url, router_address: router_address)
86
+ end
87
+
88
+ # Priority 2: raw private key
73
89
  key = ENV["REMITMD_KEY"] || ENV["REMITMD_PRIVATE_KEY"]
74
90
  if ENV["REMITMD_PRIVATE_KEY"] && !ENV["REMITMD_KEY"]
75
91
  warn "[remitmd] REMITMD_PRIVATE_KEY is deprecated, use REMITMD_KEY instead"
76
92
  end
77
- raise ArgumentError, "REMITMD_KEY not set" unless key
93
+ raise ArgumentError, "REMITMD_KEY not set (or set REMIT_SIGNER_URL + REMIT_SIGNER_TOKEN)" unless key
78
94
 
79
- chain = ENV.fetch("REMITMD_CHAIN", "base")
80
- api_url = ENV["REMITMD_API_URL"]
81
- router_address = ENV["REMITMD_ROUTER_ADDRESS"]
82
95
  new(private_key: key, chain: chain, api_url: api_url, router_address: router_address)
83
96
  end
84
97
 
data/lib/remitmd.rb CHANGED
@@ -4,6 +4,7 @@ require_relative "remitmd/errors"
4
4
  require_relative "remitmd/models"
5
5
  require_relative "remitmd/keccak"
6
6
  require_relative "remitmd/signer"
7
+ require_relative "remitmd/http_signer"
7
8
  require_relative "remitmd/http"
8
9
  require_relative "remitmd/wallet"
9
10
  require_relative "remitmd/mock"
@@ -25,5 +26,5 @@ require_relative "remitmd/x402_paywall"
25
26
  # mock.was_paid?("0x0000000000000000000000000000000000000001", 1.00) # => true
26
27
  #
27
28
  module Remitmd
28
- VERSION = "0.1.7"
29
+ VERSION = "0.1.8"
29
30
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: remitmd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - remit.md
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-25 00:00:00.000000000 Z
11
+ date: 2026-03-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -53,6 +53,7 @@ files:
53
53
  - lib/remitmd/a2a.rb
54
54
  - lib/remitmd/errors.rb
55
55
  - lib/remitmd/http.rb
56
+ - lib/remitmd/http_signer.rb
56
57
  - lib/remitmd/keccak.rb
57
58
  - lib/remitmd/mock.rb
58
59
  - lib/remitmd/models.rb