nowpayments 0.2.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +13 -0
- data/lib/nowpayments/api/authentication.rb +44 -0
- data/lib/nowpayments/version.rb +1 -1
- data/lib/nowpayments/webhook.rb +6 -23
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 406588aa056545818f16a435a4883e7f3fc6d164f7e3c43a52755ceecf758cbc
|
|
4
|
+
data.tar.gz: 58e76a282095442a96829a653417eb30aa2d7b192cff6139e4cd24bcd8b96784
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8e60843f1683f90a08c82a010830384b6d2ab58a1292645e4aa4610276e30fd958bd665fbe11edeaad9f4f7b86363806d4b66ed1dc86995c427229acd3c94884
|
|
7
|
+
data.tar.gz: d9945be67e85976fe4b1108209c659ef82126e942655c299836700cf65a667c7789578d2841aa4aa86bcc56f79cb96a8d8024fb78b959b4898c2f96699a1f7f6
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.1] - 2025-12-09
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Critical Webhook Signature Verification Bug**: Fixed signature verification failing when NOWPayments sends numbers in scientific notation (e.g., `1e-7`)
|
|
14
|
+
- The previous implementation parsed the JSON body, sorted keys, and re-serialized it before computing the HMAC
|
|
15
|
+
- Ruby's `JSON.generate` would change number formatting (e.g., `1e-7` → `0.0000001`), breaking the signature
|
|
16
|
+
- Now computes HMAC directly on the raw body string, which is correct since NOWPayments already sends keys in sorted order
|
|
17
|
+
- Removed unused `sort_keys_recursive` and `generate_signature` private methods
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Simplified `Webhook.verify!` implementation - now a single HMAC computation on raw body
|
|
21
|
+
- Added comprehensive regression tests including scientific notation edge case
|
|
22
|
+
|
|
10
23
|
## [0.2.0] - 2025-11-01
|
|
11
24
|
|
|
12
25
|
### Added - 100% API Coverage Achievement! 🎉
|
|
@@ -10,6 +10,7 @@ module NOWPayments
|
|
|
10
10
|
# @param email [String] Your NOWPayments dashboard email (case-sensitive)
|
|
11
11
|
# @param password [String] Your NOWPayments dashboard password (case-sensitive)
|
|
12
12
|
# @return [Hash] Authentication response with JWT token
|
|
13
|
+
# @raise [AuthenticationError] if credentials are invalid or access is denied
|
|
13
14
|
# @note Email and password are case-sensitive. test@gmail.com != Test@gmail.com
|
|
14
15
|
def authenticate(email:, password:)
|
|
15
16
|
response = post("auth", body: {
|
|
@@ -17,6 +18,46 @@ module NOWPayments
|
|
|
17
18
|
password: password
|
|
18
19
|
})
|
|
19
20
|
|
|
21
|
+
# Check for authentication errors
|
|
22
|
+
if response.body.is_a?(Hash)
|
|
23
|
+
# Handle 403 ACCESS_DENIED error
|
|
24
|
+
if response.body["statusCode"] == 403 || response.body["code"] == "ACCESS_DENIED"
|
|
25
|
+
error_msg = "Authentication failed: #{response.body["message"] || "Access denied"}. "
|
|
26
|
+
|
|
27
|
+
if sandbox
|
|
28
|
+
error_msg += "You are using SANDBOX mode. "
|
|
29
|
+
error_msg += "Please verify:\n"
|
|
30
|
+
error_msg += " 1. You have a sandbox account at https://sandbox.nowpayments.io/\n"
|
|
31
|
+
error_msg += " 2. Your email and password are correct (case-sensitive)\n"
|
|
32
|
+
error_msg += " 3. Your sandbox account has API access enabled"
|
|
33
|
+
else
|
|
34
|
+
error_msg += "You are using PRODUCTION mode. "
|
|
35
|
+
error_msg += "Please verify:\n"
|
|
36
|
+
error_msg += " 1. Your NOWPayments account at https://nowpayments.io/ has API access enabled\n"
|
|
37
|
+
error_msg += " 2. Go to Settings → API → Enable API access if not already enabled\n"
|
|
38
|
+
error_msg += " 3. Your email and password are correct (case-sensitive)\n"
|
|
39
|
+
error_msg += " 4. If testing, you may need to use sandbox: true and sandbox credentials"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
raise AuthenticationError.new(
|
|
43
|
+
status: response.body["statusCode"],
|
|
44
|
+
body: { "message" => error_msg },
|
|
45
|
+
response_headers: response.headers
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Handle other error responses
|
|
50
|
+
status_code = response.body["statusCode"]&.to_i || 0
|
|
51
|
+
if response.body["status"] == false || (status_code > 0 && status_code >= 400)
|
|
52
|
+
error_msg = response.body["message"] || "Authentication failed"
|
|
53
|
+
raise AuthenticationError.new(
|
|
54
|
+
status: status_code,
|
|
55
|
+
body: { "message" => error_msg },
|
|
56
|
+
response_headers: response.headers
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
20
61
|
# Store token and expiry time (5 minutes from now)
|
|
21
62
|
if response.body["token"]
|
|
22
63
|
@jwt_token = response.body["token"]
|
|
@@ -24,6 +65,9 @@ module NOWPayments
|
|
|
24
65
|
|
|
25
66
|
# Reset connection to include new Bearer token
|
|
26
67
|
reset_connection! if respond_to?(:reset_connection!, true)
|
|
68
|
+
else
|
|
69
|
+
# No token in response - authentication failed
|
|
70
|
+
raise AuthenticationError, "Authentication failed: No token received. Check your email and password."
|
|
27
71
|
end
|
|
28
72
|
|
|
29
73
|
response.body
|
data/lib/nowpayments/version.rb
CHANGED
data/lib/nowpayments/webhook.rb
CHANGED
|
@@ -18,36 +18,19 @@ module NOWPayments
|
|
|
18
18
|
raise ArgumentError, "signature required" if signature.nil? || signature.empty?
|
|
19
19
|
raise ArgumentError, "secret required" if secret.nil? || secret.empty?
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
# Compute HMAC directly on raw body - NOWPayments already sends keys in sorted order.
|
|
22
|
+
# DO NOT parse and re-serialize! Ruby's JSON.generate may change number formatting
|
|
23
|
+
# (e.g., scientific notation "1e-7" becomes "0.0000001"), breaking the signature.
|
|
24
|
+
expected_sig = OpenSSL::HMAC.hexdigest("SHA512", secret, raw_body)
|
|
24
25
|
|
|
25
26
|
raise SecurityError, "Invalid IPN signature - webhook verification failed" unless secure_compare(expected_sig, signature)
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
# Only parse after verification succeeds
|
|
29
|
+
JSON.parse(raw_body)
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
private
|
|
31
33
|
|
|
32
|
-
# Recursively sort Hash keys (including nested hashes and arrays)
|
|
33
|
-
# This is critical for proper HMAC signature verification
|
|
34
|
-
def sort_keys_recursive(obj)
|
|
35
|
-
case obj
|
|
36
|
-
when Hash
|
|
37
|
-
obj.sort.to_h.transform_values { |v| sort_keys_recursive(v) }
|
|
38
|
-
when Array
|
|
39
|
-
obj.map { |v| sort_keys_recursive(v) }
|
|
40
|
-
else
|
|
41
|
-
obj
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Generate HMAC-SHA512 signature
|
|
46
|
-
def generate_signature(sorted_json, secret)
|
|
47
|
-
json_string = JSON.generate(sorted_json, space: "", indent: "")
|
|
48
|
-
OpenSSL::HMAC.hexdigest("SHA512", secret, json_string)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
34
|
# Constant-time comparison to prevent timing attacks
|
|
52
35
|
def secure_compare(a, b)
|
|
53
36
|
return false unless a.bytesize == b.bytesize
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nowpayments
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chayut Orapinpatipat
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-12-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|