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 +4 -4
- data/LICENSE +21 -0
- data/README.md +1 -2
- data/lib/nostr_zap/relay_client.rb +43 -10
- data/lib/nostr_zap/version.rb +1 -1
- data/lib/nostr_zap/zap/request_validator.rb +12 -8
- metadata +2 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85d3b9fa913d9e3b38f2e62f2995299878f9860f5078e396272bb2f35632da9c
|
|
4
|
+
data.tar.gz: 29f215e0d57f0297c8e11d09938365757829933ce756a6869e959d551df46927
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 '
|
|
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
|
-
|
|
47
|
-
|
|
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,
|
|
50
|
+
[relay_url, thread]
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
81
|
+
private
|
|
79
82
|
|
|
80
83
|
def create_socket(uri)
|
|
81
|
-
|
|
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:
|
|
205
|
+
message: relay_message.nil? || relay_message.empty? ? default_message : relay_message
|
|
173
206
|
}
|
|
174
207
|
rescue JSON::ParserError
|
|
175
208
|
nil
|
data/lib/nostr_zap/version.rb
CHANGED
|
@@ -69,7 +69,7 @@ module NostrZap
|
|
|
69
69
|
find_tag_value('e')
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
|
|
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
|
-
|
|
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 #{
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|