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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97ac8b5b89917ed020bf1fc5c887cc02ab236a65778433696826adbcf55298ab
4
- data.tar.gz: 1faa8a8ef8abdb85c17e33c6f5ddfe7eb17f434a062784095c32627b924eac94
3
+ metadata.gz: 406588aa056545818f16a435a4883e7f3fc6d164f7e3c43a52755ceecf758cbc
4
+ data.tar.gz: 58e76a282095442a96829a653417eb30aa2d7b192cff6139e4cd24bcd8b96784
5
5
  SHA512:
6
- metadata.gz: 41100692b84c8888da5c11292d1759c0b17c48027ab72fa15b4b755675402ad39fb36c441cd13cb12a526eb969126700e3c636e3df01de54f3d8e8e5bbe8e85b
7
- data.tar.gz: 3e66866c39e02084cbfa8156d6b79160eeb962955cbafa1900d298321cb99759da60c43728dc1336b359c32bfbd69ef5f8827e40bbe67d96038268b6373eb2f7
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NOWPayments
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
@@ -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
- parsed = JSON.parse(raw_body)
22
- sorted_json = sort_keys_recursive(parsed)
23
- expected_sig = generate_signature(sorted_json, secret)
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
- parsed
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.0
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-01 00:00:00.000000000 Z
11
+ date: 2025-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday