nostr-zap 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2dbcf7cc9306b5fe9ad608df598f848d7c121465a5fc1d5d77614a9b148566f7
4
- data.tar.gz: 178a651a19ba53a6e25c4dd2104ef70ebb4de222b61dc3df846b0913112a6482
3
+ metadata.gz: 85d3b9fa913d9e3b38f2e62f2995299878f9860f5078e396272bb2f35632da9c
4
+ data.tar.gz: 29f215e0d57f0297c8e11d09938365757829933ce756a6869e959d551df46927
5
5
  SHA512:
6
- metadata.gz: a713b3dbd1002c306a49a27d663278f1d95e38ff9e56609bed9ff599cee0dadd9372214cf45fd113dbfa9049e12f794aceea744d6c83ed597a8f68e537562bae
7
- data.tar.gz: 6d97c5fb41f2b84203b405cbf7c54e4ad61944956219dd4447f383cb6eed745bf6ecdd318e625891e3f67ee2388857aa45674c26bbc89056776aba479addeed5
6
+ metadata.gz: b64eec1720df7ad87babaf118d85544a74267e740047c066e6b9cd038a12d14435a9b128e60817b43c3e807d0c805d02b2c9d6811b7616e041f13ff8b3e8d54d
7
+ data.tar.gz: 98510efc4be72be923e8aabb5b7603191bf8dc2380c06a7f31b410e25c193f8a98356827dad49e5841b22a1d960a53c3bd89523ffb73feaa394e5313395d03b8
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sasha Zykov
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 CHANGED
@@ -78,7 +78,7 @@ if validator.valid?
78
78
  validator.amount_msats # => 1000
79
79
  validator.zapped_event_id # => "hex event id" or nil
80
80
  else
81
- validator.errors # => ["Invalid kind: expected 9734, got 1"]
81
+ validator.errors # => ["Invalid kind: expected integer 9734, got 1"]
82
82
  end
83
83
  ```
84
84
 
@@ -148,7 +148,6 @@ NostrZap::Error # base
148
148
  ## Dependencies
149
149
 
150
150
  - [bip-schnorr](https://rubygems.org/gems/bip-schnorr) ~> 0.7 -- BIP-340 Schnorr signatures
151
- - [concurrent-ruby](https://rubygems.org/gems/concurrent-ruby) ~> 1.0 -- concurrent relay publishing
152
151
  - [websocket](https://rubygems.org/gems/websocket) ~> 1.0 -- WebSocket handshake and framing
153
152
 
154
153
  ## Development
@@ -5,7 +5,7 @@ require 'websocket'
5
5
  require 'openssl'
6
6
  require 'uri'
7
7
  require 'json'
8
- require 'concurrent'
8
+ require 'resolv'
9
9
 
10
10
  module NostrZap
11
11
  # Client for publishing NOSTR events to relay servers via WebSocket.
@@ -43,15 +43,18 @@ module NostrZap
43
43
  # @param relay_urls [Array<String>] List of relay WebSocket URLs
44
44
  # @return [Hash] Map of relay URL to result { success: bool, message: String }
45
45
  def publish_event(event:, relay_urls:)
46
- futures = relay_urls.to_h do |relay_url|
47
- future = Concurrent::Promises.future do
46
+ threads = relay_urls.to_h do |relay_url|
47
+ thread = Thread.new do
48
48
  publish_to_relay(event: event, relay_url: relay_url)
49
49
  end
50
- [relay_url, future]
50
+ [relay_url, thread]
51
51
  end
52
52
 
53
- futures.transform_values do |future|
54
- future.value(@timeout) || { success: false, message: 'Connection timeout' }
53
+ threads.transform_values do |thread|
54
+ next thread.value if thread.join(@timeout)
55
+
56
+ thread.kill
57
+ { success: false, message: 'Connection timeout' }
55
58
  end
56
59
  end
57
60
 
@@ -68,17 +71,31 @@ module NostrZap
68
71
  perform_handshake(socket, uri)
69
72
  send_event(socket, event)
70
73
  wait_for_ok_response(socket, event_id)
71
- rescue => e
74
+ rescue StandardError => e
72
75
  @logger&.error("NostrZap::RelayClient error for #{relay_url}: #{e.message}")
73
76
  { success: false, message: e.message }
74
77
  ensure
75
78
  socket&.close
76
79
  end
77
80
 
78
- private
81
+ private
79
82
 
80
83
  def create_socket(uri)
81
- tcp_socket = TCPSocket.new(uri.host, uri.port || ((uri.scheme == 'wss') ? 443 : 80))
84
+ connect_hosts = resolve_connect_hosts(uri.host)
85
+ port = uri.port || (uri.scheme == 'wss' ? 443 : 80)
86
+ tcp_socket = nil
87
+ last_error = nil
88
+
89
+ connect_hosts.each do |connect_host|
90
+ tcp_socket = TCPSocket.new(connect_host, port)
91
+ break
92
+ rescue StandardError => e
93
+ last_error = e
94
+ end
95
+
96
+ if tcp_socket.nil?
97
+ raise PublishError, "Could not connect to relay host #{uri.host}: #{last_error&.message || 'unknown error'}"
98
+ end
82
99
 
83
100
  if uri.scheme == 'wss'
84
101
  ssl_context = OpenSSL::SSL::SSLContext.new
@@ -93,6 +110,22 @@ module NostrZap
93
110
  end
94
111
  end
95
112
 
113
+ def resolve_connect_hosts(host)
114
+ addresses = Resolv.getaddresses(host)
115
+ raise PublishError, "Relay host could not be resolved: #{host}" if addresses.empty?
116
+
117
+ public_addresses = addresses.select { |addr| public_ip_address?(addr) }
118
+ return public_addresses if public_addresses.any?
119
+
120
+ raise PublishError, "Relay host resolves only to private/reserved addresses: #{host}"
121
+ rescue Resolv::ResolvError
122
+ raise PublishError, "Relay host could not be resolved: #{host}"
123
+ end
124
+
125
+ def public_ip_address?(address)
126
+ RelayUrlValidator::PRIVATE_IP_RANGES.none? { |range| range.include?(address) }
127
+ end
128
+
96
129
  def perform_handshake(socket, uri)
97
130
  handshake = WebSocket::Handshake::Client.new(url: uri.to_s)
98
131
  socket.write(handshake.to_s)
@@ -169,7 +202,7 @@ module NostrZap
169
202
 
170
203
  {
171
204
  success: parsed[2] == true,
172
- message: (relay_message.nil? || relay_message.empty?) ? default_message : relay_message,
205
+ message: relay_message.nil? || relay_message.empty? ? default_message : relay_message
173
206
  }
174
207
  rescue JSON::ParserError
175
208
  nil
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NostrZap
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
@@ -69,7 +69,7 @@ module NostrZap
69
69
  find_tag_value('e')
70
70
  end
71
71
 
72
- private
72
+ private
73
73
 
74
74
  def find_tag_value(tag_name)
75
75
  return nil unless @parsed_event
@@ -113,9 +113,10 @@ module NostrZap
113
113
  end
114
114
 
115
115
  def kind_valid?
116
- return true if @parsed_event['kind'] == ZAP_REQUEST_KIND
116
+ kind = @parsed_event['kind']
117
+ return true if kind.is_a?(Integer) && kind == ZAP_REQUEST_KIND
117
118
 
118
- fail_with("Invalid kind: expected #{ZAP_REQUEST_KIND}, got #{@parsed_event['kind']}")
119
+ fail_with("Invalid kind: expected integer #{ZAP_REQUEST_KIND}, got #{kind.inspect}")
119
120
  end
120
121
 
121
122
  def tags_valid?
@@ -124,9 +125,12 @@ module NostrZap
124
125
 
125
126
  def p_tag_valid?
126
127
  p_tag = @parsed_event['tags']&.find { |tag| tag[0] == 'p' }
127
- return true if p_tag && p_tag[1] && !p_tag[1].empty?
128
+ value = p_tag&.[](1)
129
+ return fail_with("Missing required 'p' tag (recipient pubkey)") unless value.is_a?(String) && !value.empty?
128
130
 
129
- fail_with("Missing required 'p' tag (recipient pubkey)")
131
+ return true if value.is_a?(String) && value.match?(/\A[0-9a-f]{64}\z/i)
132
+
133
+ fail_with("Invalid 'p' tag: expected 64-character hex pubkey")
130
134
  end
131
135
 
132
136
  def relays_tag_valid?
@@ -145,7 +149,7 @@ module NostrZap
145
149
  created_at: @parsed_event['created_at'],
146
150
  kind: @parsed_event['kind'],
147
151
  tags: @parsed_event['tags'],
148
- content: @parsed_event['content'],
152
+ content: @parsed_event['content']
149
153
  )
150
154
  return true if @parsed_event['id'] == computed
151
155
 
@@ -158,12 +162,12 @@ module NostrZap
158
162
  valid = Crypto.verify_signature?(
159
163
  @parsed_event['id'],
160
164
  @parsed_event['pubkey'],
161
- @parsed_event['sig'],
165
+ @parsed_event['sig']
162
166
  )
163
167
  return true if valid
164
168
 
165
169
  fail_with('Invalid signature')
166
- rescue => e
170
+ rescue StandardError => e
167
171
  fail_with("Signature validation error: #{e.message}")
168
172
  end
169
173
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nostr-zap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Zykov
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0.7'
27
- - !ruby/object:Gem::Dependency
28
- name: concurrent-ruby
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '1.0'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '1.0'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: websocket
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -62,6 +48,7 @@ executables: []
62
48
  extensions: []
63
49
  extra_rdoc_files: []
64
50
  files:
51
+ - LICENSE
65
52
  - README.md
66
53
  - lib/nostr_zap.rb
67
54
  - lib/nostr_zap/crypto.rb