DhanHQ 2.2.0 → 2.2.2

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: 51941a246763b26e7a08508c3bb5349079ed6312e1ef5bf7593dadb36808e13c
4
- data.tar.gz: 12c7d276dbe39ee14df02feff9c61914ac91d049a10b644ac7e184778eea1a68
3
+ metadata.gz: '09510cd95013213714d5ba4878e87dfa6bd5968cb90ea4e0a539ddb0d05bc2b0'
4
+ data.tar.gz: 459c316d0f2c68ad8b0556518dfd70762e460e5c68ef063439e73f68ca33ba02
5
5
  SHA512:
6
- metadata.gz: cfc4cfd03d5d722d4bc8451d71d7189404fc38f931d1ab8772bc796191934b72d82239d5096e272b9aa27aea1a974bb2c6a469cd670dbb7ba85f205d6c291ab8
7
- data.tar.gz: f7a17fafd835d9ceb339b255f17f1e408692af094c2658ee9aedf13f013e8558a9170771a7acf2b03b35e39412d9dcaf308ac9e76db168cb39e7b1f745ee1a35
6
+ metadata.gz: d54cf0cc58e82d1566f525303ea25fddd051b1c838152da39f12509435a16e4005f55417d5516c52f6222b4ba6286c421202d0a148f743be7ca4127b7ef03bae
7
+ data.tar.gz: ea522dfeb54054d8162d00ead357efb4c444c3dd0c4c182c4d688a2bd9a484d44af7b2bab221476c93fded44511bfff7e8cf4b75d6e962fc8191486bee5440b1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.2.2] - 2026-01-31
4
+
5
+ ### Contracts (date validation)
6
+
7
+ - **from_date / to_date**: `from_date` must be strictly before `to_date` and must be a valid trading date (no weekend). `to_date` may be any date after `from_date` (format YYYY-MM-DD). Applied in `HistoricalDataContract`, `TradeHistoryContract`, and `ExpiredOptionsDataContract`.
8
+ - **HistoricalDataContract**: Added trading-day check for `from_date` and `from_date < to_date`; inherits `BaseContract`.
9
+
10
+ ### Specs & tooling
11
+
12
+ - **Specs**: Base model, expired options, trade, and historical data specs updated to use weekday dates and expect `from_date must be before to_date`; VCR cassette `trade_history.yml` updated for new date.
13
+ - **RuboCop**: RSpec/ExampleLength in expired options contract spec fixed via `next_weekday` helper.
14
+
15
+ ---
16
+
17
+ ## [2.2.1] - 2026-01-31
18
+
19
+ ### Authentication
20
+
21
+ - **RenewToken API**: Added `DhanHQ::Auth.renew_token(access_token, client_id, base_url: nil)` to refresh web-generated access tokens (24h validity). Calls GET `/v2/RenewToken` with `access-token` and `dhanClientId` headers; returns response hash with indifferent access (e.g. `accessToken`, `expiryTime`). Use in `access_token_provider` or `on_token_expired` to refresh and store the new token. Only valid for tokens generated from Dhan Web (not API key flow).
22
+ - **Dhan auth scope**: Documented that the gem does **not** implement API key/secret consent or Partner consent flows; apps obtain tokens via Dhan Web, API key OAuth, or Partner flow and pass them to the gem. See [docs/AUTHENTICATION.md](docs/AUTHENTICATION.md).
23
+
24
+ ### Documentation
25
+
26
+ - **docs/AUTHENTICATION.md**: Added “How you get the token (Dhan’s responsibility)” (Individual: Web token, RenewToken, API key; Partner: consent flow) and “RenewToken (web-generated tokens only)” with `DhanHQ::Auth.renew_token` usage and example. “See also” updated for GUIDE, rails_integration, TESTING_GUIDE, CHANGELOG 2.2.0/2.2.1.
27
+ - **README.md**: Note under Dynamic access token for RenewToken via `DhanHQ::Auth.renew_token` and that API key/Partner flows are implemented in the app.
28
+ - **GUIDE.md**: “Dynamic access token” section extended with RenewToken (`DhanHQ::Auth.renew_token`) and note that API key/Partner flows are in the app.
29
+ - **docs/TESTING_GUIDE.md**: Optional config comment for RenewToken and pointer to AUTHENTICATION.md (API key/Partner in app).
30
+ - **docs/rails_integration.md**: “Dynamic access token” section extended with RenewToken (web-generated tokens) and link to AUTHENTICATION.md.
31
+ - **docs/websocket_integration.md**, **docs/live_order_updates.md**: Notes updated for dynamic token, RenewToken, and API key/Partner in app.
32
+ - **docs/standalone_ruby_websocket_integration.md**, **docs/rails_websocket_integration.md**: Configuration section updated with RenewToken and AUTHENTICATION.md link.
33
+
34
+ ### CI / Release
35
+
36
+ - **Release workflow**: Aligned with ollama-client: tag-based release (`on: push: tags: v*`), validate tag vs gem version, use `GEM_HOST_API_KEY` for RubyGems push (no credentials file), single retry with OTP. Removed GitHub Release step.
37
+ - **RELEASING.md**, **docs/RELEASE_GUIDE.md**: Updated to describe tag-only publish and `GEM_HOST_API_KEY`; removed references to “Create GitHub Release” and “Run tests” in release job.
38
+
39
+ ### Fixes
40
+
41
+ - **RuboCop**: Layout/EmptyLineAfterGuardClause — added blank line after guard clauses in Configuration, WS client, market depth client, orders connection. Style/NilLambda — `-> { nil }` → `-> {}` in configuration_spec. RSpec/InstanceVariable — replaced `@token_call_count` and `@hook_called`/`@hook_error` with `let(:token_call_count)`, `let(:token_provider)`, `let(:hook_state)` in client_spec auth-failure examples.
42
+ - **CI**: Gemfile.lock updated for path gem version (DhanHQ 2.2.1) so `bundle install` in frozen mode succeeds.
43
+
44
+ ### Added
45
+
46
+ - **lib/DhanHQ/auth.rb**: New module with `Auth.renew_token` for Dhan RenewToken API.
47
+
48
+ ---
49
+
3
50
  ## [2.2.0] - 2026-01-31
4
51
 
5
52
  ### Authentication & token handling
data/GUIDE.md CHANGED
@@ -71,7 +71,11 @@ Set any of the following environment variables _before_ calling
71
71
 
72
72
  **Dynamic access token**
73
73
 
74
- For token rotation without restarting the app, set `access_token_provider` (Proc/lambda) so the token is resolved at request time. When the API returns 401 or token-expired (error code 807) and the provider is set, the client retries the request once with a fresh token. Optional `on_token_expired` is called before that retry. See [docs/AUTHENTICATION.md](docs/AUTHENTICATION.md) and README “Dynamic access token”.
74
+ For token rotation without restarting the app, set `access_token_provider` (Proc/lambda) so the token is resolved at request time. When the API returns 401 or token-expired (error code 807) and the provider is set, the client retries the request once with a fresh token. Optional `on_token_expired` is called before that retry.
75
+
76
+ **RenewToken (web-generated tokens):** If the token was generated from Dhan Web (24h validity), use `DhanHQ::Auth.renew_token(access_token, client_id)` to refresh it; use the returned token in your provider or store. The gem does **not** implement API key/secret or Partner consent flows—implement those in your app and pass the token to the gem.
77
+
78
+ See [docs/AUTHENTICATION.md](docs/AUTHENTICATION.md) and README “Dynamic access token”.
75
79
 
76
80
  ---
77
81
 
data/README.md CHANGED
@@ -114,6 +114,8 @@ end
114
114
 
115
115
  If the API returns **401** or error code **807** (token expired) and `access_token_provider` is set, the client retries the request **once** with a fresh token from the provider. Otherwise it raises `DhanHQ::InvalidAuthenticationError` or `DhanHQ::TokenExpiredError`. Missing or nil token from config raises `DhanHQ::AuthenticationError`.
116
116
 
117
+ **RenewToken (web-generated tokens):** For tokens generated from Dhan Web (24h validity), use `DhanHQ::Auth.renew_token(access_token, client_id)` to refresh; use the returned token in your provider or store. The gem does **not** implement API key/secret or Partner consent flows—implement those in your app and pass the token to the gem. See [docs/AUTHENTICATION.md](docs/AUTHENTICATION.md).
118
+
117
119
  ### Logging
118
120
 
119
121
  ```ruby
@@ -1,6 +1,20 @@
1
1
  # Authentication & token handling
2
2
 
3
- This document describes how the gem handles access tokens, including dynamic resolution, retry-on-401, and related errors.
3
+ This document describes how the gem handles access tokens, dynamic resolution, retry-on-401, and how that fits with Dhan’s authentication methods.
4
+
5
+ ## How you get the token (Dhan’s responsibility)
6
+
7
+ Dhan supports several ways to obtain an access token. **The gem does not implement these flows**; your app or the user obtains the token, and the gem uses it.
8
+
9
+ | User type | Method | Where it happens |
10
+ | --------- |--------|------------------|
11
+ | **Individual** | **Access token from Dhan Web** | User logs in at web.dhan.co → My Profile → Access DhanHQ APIs → Generate token (24h). You can refresh it with **RenewToken** (see below); the gem can call that for you. |
12
+ | **Individual** | **API key & secret (OAuth)** | User creates API key/secret at web.dhan.co. Your app does: (1) Generate consent, (2) Browser login, (3) Consume consent to get `accessToken` and `expiryTime`. Implement this flow in your app; then pass the token to the gem via `access_token` or `access_token_provider`. |
13
+ | **Partner** | **Partner consent flow** | You have `partner_id` and `partner_secret`. Your app does: (1) Generate consent, (2) User logs in on browser, (3) Consume consent to get `accessToken`. Implement in your app; pass the token to the gem. |
14
+
15
+ **What the gem provides:** It accepts a token (static or from a provider), sends it on every request, and can retry once on 401 when you use `access_token_provider`. It also provides **`DhanHQ::Auth.renew_token`** for refreshing **web-generated** tokens (RenewToken API). It does **not** implement API key/secret consent or Partner consent; use Dhan’s docs and your own HTTP client for those.
16
+
17
+ For full details and curl examples, see [DhanHQ API docs](https://dhanhq.co/docs) (Authentication).
4
18
 
5
19
  ## Static token (default)
6
20
 
@@ -34,6 +48,37 @@ end
34
48
 
35
49
  REST and WebSocket clients both use `config.resolved_access_token`, which calls the provider when set or falls back to `access_token`.
36
50
 
51
+ ## RenewToken (web-generated tokens only)
52
+
53
+ If the token was **generated from Dhan Web** (not API key flow), you can refresh it (24h validity) using Dhan’s RenewToken API. The gem provides a helper:
54
+
55
+ ```ruby
56
+ # Returns a hash with API response (e.g. accessToken, expiryTime). Use the new token for subsequent requests.
57
+ response = DhanHQ::Auth.renew_token(current_access_token, client_id)
58
+ new_token = response["accessToken"] || response[:accessToken]
59
+ # Optional: response may include "expiryTime"
60
+ ```
61
+
62
+ Use this inside `access_token_provider` or in `on_token_expired` to refresh and then return the new token (e.g. store it and return it from the provider on the next request).
63
+
64
+ Example: refresh in provider and cache the result until near expiry:
65
+
66
+ ```ruby
67
+ # Pseudocode: store current token + expiry; in provider, refresh if expired or near expiry
68
+ config.access_token_provider = lambda do
69
+ stored = YourTokenStore.current
70
+ if stored.nil? || stored.expired_soon?
71
+ response = DhanHQ::Auth.renew_token(stored&.access_token || ENV["ACCESS_TOKEN"], config.client_id)
72
+ YourTokenStore.update!(response["accessToken"], response["expiryTime"])
73
+ stored = YourTokenStore.current
74
+ end
75
+ raise "Token missing" unless stored
76
+ stored.access_token
77
+ end
78
+ ```
79
+
80
+ **Note:** RenewToken is only for tokens generated from Dhan Web. For API key or Partner flows, obtain a new token using Dhan’s consent APIs in your app.
81
+
37
82
  ## Retry-on-401
38
83
 
39
84
  When the API returns **401** or a token-expired error (e.g. error code **807**), and `access_token_provider` is set:
@@ -59,5 +104,7 @@ Rescue `AuthenticationError` for local config/token resolution failures; rescue
59
104
  ## See also
60
105
 
61
106
  - [README.md](../README.md) — Configuration and “Dynamic access token”
62
- - [rails_integration.md](rails_integration.md) — Rails initializer with optional `access_token_provider`
63
- - [CHANGELOG.md](../CHANGELOG.md) — 2.2.0 auth and token changes
107
+ - [GUIDE.md](../GUIDE.md) — Dynamic access token and RenewToken
108
+ - [rails_integration.md](rails_integration.md) — Rails initializer with optional `access_token_provider` and RenewToken
109
+ - [TESTING_GUIDE.md](TESTING_GUIDE.md) — Config examples and RenewToken
110
+ - [CHANGELOG.md](../CHANGELOG.md) — 2.2.0 and 2.2.1 auth and token changes
@@ -56,6 +56,8 @@ end
56
56
  # config.access_token_provider = -> { YourTokenStore.active_token }
57
57
  # config.on_token_expired = ->(err) { YourTokenStore.refresh! }
58
58
  # end
59
+ # Optional: refresh web-generated tokens with DhanHQ::Auth.renew_token(current_token, client_id)
60
+ # See docs/AUTHENTICATION.md (API key/Partner flows are implemented in your app, not in the gem)
59
61
 
60
62
  # Set log level for debugging
61
63
  DhanHQ.logger.level = Logger::DEBUG
@@ -18,7 +18,7 @@ The DhanHQ Ruby client provides comprehensive real-time order update functionali
18
18
  ```ruby
19
19
  require 'dhan_hq'
20
20
 
21
- # Configure credentials (or use config.access_token_provider for dynamic token; see docs/AUTHENTICATION.md)
21
+ # Configure credentials. For dynamic token use config.access_token_provider; for web tokens refresh with DhanHQ::Auth.renew_token. API key/Partner: implement in app. See docs/AUTHENTICATION.md.
22
22
  DhanHQ.configure do |config|
23
23
  config.client_id = "your_client_id"
24
24
  config.access_token = "your_access_token"
@@ -122,6 +122,8 @@ end
122
122
 
123
123
  When the API returns 401 or token-expired (error code 807) and `access_token_provider` is set, the client retries the request **once** with a fresh token from the provider. `on_token_expired` is called before that retry so you can refresh your store if needed.
124
124
 
125
+ **RenewToken (web-generated tokens only):** If the token was generated from Dhan Web (24h validity), you can refresh it with `DhanHQ::Auth.renew_token(access_token, client_id)` and store the result; use it in your provider or call it from `on_token_expired`. The gem does **not** implement API key/secret or Partner consent flows—implement those in your app and pass the token to the gem. See [docs/AUTHENTICATION.md](AUTHENTICATION.md).
126
+
125
127
  ## 3. Build service objects for REST flows
126
128
 
127
129
  Wrap trading actions in plain-old Ruby objects so controllers and jobs stay thin:
@@ -99,6 +99,8 @@ end
99
99
 
100
100
  ## Configuration
101
101
 
102
+ For dynamic token use `config.access_token_provider`. For web-generated tokens refresh with `DhanHQ::Auth.renew_token(access_token, client_id)`. API key/Partner flows: implement in your app. See [AUTHENTICATION.md](AUTHENTICATION.md).
103
+
102
104
  ### Environment-Specific Configuration
103
105
 
104
106
  ```ruby
@@ -68,6 +68,8 @@ ruby market_feed_script.rb
68
68
 
69
69
  ## Configuration
70
70
 
71
+ For dynamic token at request time use `config.access_token_provider`. For web-generated tokens (24h) refresh with `DhanHQ::Auth.renew_token(access_token, client_id)`. API key/Partner flows: implement in your app and pass the token to the gem. See [AUTHENTICATION.md](AUTHENTICATION.md).
72
+
71
73
  ### Environment Variables
72
74
 
73
75
  ```bash
@@ -32,7 +32,7 @@ DhanHQ.configure do |config|
32
32
  config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
33
33
  config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
34
34
  end
35
- # For dynamic token at request time (REST + WebSocket), use config.access_token_provider; see docs/AUTHENTICATION.md
35
+ # For dynamic token: use config.access_token_provider. For web-generated tokens, refresh with DhanHQ::Auth.renew_token. API key/Partner flows: implement in your app. See docs/AUTHENTICATION.md.
36
36
  ```
37
37
 
38
38
  ### 2. Market Feed WebSocket (Recommended for Beginners)
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+
6
+ module DhanHQ
7
+ # Helpers for Dhan authentication APIs.
8
+ # The gem does not implement API key/secret consent flows or Partner consent;
9
+ # use Dhan Web or your own OAuth flow to obtain tokens, then pass them via
10
+ # configuration. This module supports refreshing web-generated tokens only.
11
+ module Auth
12
+ # Refreshes a web-generated access token (24h validity).
13
+ # Calls POST /v2/RenewToken; expires the current token and returns a new one.
14
+ # Only valid for tokens generated from Dhan Web (not API key flow).
15
+ #
16
+ # @param access_token [String] Current JWT from Dhan Web
17
+ # @param client_id [String] Dhan client ID (dhanClientId)
18
+ # @param base_url [String, nil] API base URL (default: DhanHQ.configuration.base_url)
19
+ # @return [HashWithIndifferentAccess] Response with :access_token and :expiry_time (if present)
20
+ # @raise [DhanHQ::Error] On HTTP or API error
21
+ def self.renew_token(access_token, client_id, base_url: nil)
22
+ base_url ||= DhanHQ.configuration&.base_url || Configuration::BASE_URL
23
+ url = "#{base_url.chomp("/")}/RenewToken"
24
+
25
+ conn = Faraday.new(url: url) do |c|
26
+ c.request :json
27
+ c.response :json, content_type: /\bjson$/
28
+ c.adapter Faraday.default_adapter
29
+ end
30
+
31
+ response = conn.get("") do |req|
32
+ req.headers["access-token"] = access_token
33
+ req.headers["dhanClientId"] = client_id
34
+ req.headers["Accept"] = "application/json"
35
+ end
36
+
37
+ unless response.success?
38
+ body = begin
39
+ JSON.parse(response.body.to_s)
40
+ rescue JSON::ParserError
41
+ {}
42
+ end
43
+ error_message = body["errorMessage"] || body["message"] || response.body.to_s
44
+ raise DhanHQ::InvalidAuthenticationError, "RenewToken failed: #{response.status} #{error_message}"
45
+ end
46
+
47
+ data = response.body
48
+ data = JSON.parse(data) if data.is_a?(String)
49
+ result = data.is_a?(Hash) ? data : {}
50
+ result.with_indifferent_access
51
+ end
52
+ end
53
+ end
@@ -84,6 +84,7 @@ module DhanHQ
84
84
  if access_token_provider
85
85
  token = access_token_provider.call
86
86
  raise DhanHQ::AuthenticationError, "access_token_provider returned nil or empty" if token.nil? || token.to_s.empty?
87
+
87
88
  token.to_s
88
89
  else
89
90
  access_token
@@ -69,7 +69,8 @@ module DhanHQ
69
69
  from_date = Date.parse(values[:from_date])
70
70
  to_date = Date.parse(values[:to_date])
71
71
 
72
- key.failure("from_date must be on or before to_date") if from_date > to_date
72
+ key.failure("from_date must be before to_date") if from_date >= to_date
73
+ key.failure("from_date must be a valid trading date (no weekend dates)") unless trading_day?(from_date)
73
74
 
74
75
  # Check if date range is not too large (max 31 days; to_date is non-inclusive)
75
76
  key.failure("date range cannot exceed 31 days") if (to_date - from_date).to_i > 31
@@ -98,6 +99,13 @@ module DhanHQ
98
99
  false
99
100
  end
100
101
  end
102
+
103
+ def trading_day?(date)
104
+ return false unless date.is_a?(Date)
105
+
106
+ # Sunday = 0, Saturday = 6; trading days are Monday (1) through Friday (5)
107
+ (1..5).cover?(date.wday)
108
+ end
101
109
  end
102
110
  end
103
111
  end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "date"
4
+
3
5
  module DhanHQ
4
6
  module Contracts
5
7
  # Validates payloads for the historical data endpoints.
6
- class HistoricalDataContract < Dry::Validation::Contract
7
- include DhanHQ::Constants
8
-
8
+ class HistoricalDataContract < BaseContract
9
9
  params do
10
10
  # Common required fields
11
11
  required(:security_id).filled(:string)
@@ -23,6 +23,33 @@ module DhanHQ
23
23
  # (valid: 1, 5, 15, 25, 60)
24
24
  optional(:interval).maybe(:string, included_in?: %w[1 5 15 25 60])
25
25
  end
26
+
27
+ rule(:from_date) do
28
+ next unless value.is_a?(String) && value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
29
+
30
+ d = Date.parse(value)
31
+ key.failure("must be a valid trading date (no weekend dates)") unless trading_day?(d)
32
+ rescue Date::Error
33
+ key.failure("invalid date format")
34
+ end
35
+
36
+ rule(:from_date, :to_date) do
37
+ next unless values[:from_date].match?(/\A\d{4}-\d{2}-\d{2}\z/) && values[:to_date].match?(/\A\d{4}-\d{2}-\d{2}\z/)
38
+
39
+ from_date = Date.parse(values[:from_date])
40
+ to_date = Date.parse(values[:to_date])
41
+ key.failure("from_date must be before to_date") if from_date >= to_date
42
+ rescue Date::Error
43
+ key.failure("invalid date format")
44
+ end
45
+
46
+ private
47
+
48
+ def trading_day?(date)
49
+ return false unless date.is_a?(Date)
50
+
51
+ (1..5).cover?(date.wday)
52
+ end
26
53
  end
27
54
  end
28
55
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "date"
4
+
3
5
  module DhanHQ
4
6
  module Contracts
5
7
  ##
@@ -20,6 +22,7 @@ module DhanHQ
20
22
 
21
23
  rule(:from_date) do
22
24
  key.failure("must be in YYYY-MM-DD format (e.g., 2024-01-15)") unless valid_date_format?(value)
25
+ key.failure("must be a valid trading date (no weekend dates)") if valid_date_format?(value) && !trading_day?(Date.parse(value))
23
26
  end
24
27
 
25
28
  rule(:to_date) do
@@ -35,9 +38,8 @@ module DhanHQ
35
38
  from_date = Date.parse(values[:from_date])
36
39
  to_date = Date.parse(values[:to_date])
37
40
 
38
- key.failure("from_date must be before or equal to to_date") if from_date > to_date
41
+ key.failure("from_date must be before to_date") if from_date >= to_date
39
42
  rescue Date::Error
40
- # This shouldn't happen since we already validated format, but just in case
41
43
  key.failure("invalid date format")
42
44
  end
43
45
  end
@@ -49,7 +51,6 @@ module DhanHQ
49
51
  return false unless date_string.is_a?(String)
50
52
  return false unless date_string.match?(/\A\d{4}-\d{2}-\d{2}\z/)
51
53
 
52
- # Additional check to ensure it's a valid date
53
54
  begin
54
55
  Date.parse(date_string)
55
56
  true
@@ -57,6 +58,12 @@ module DhanHQ
57
58
  false
58
59
  end
59
60
  end
61
+
62
+ def trading_day?(date)
63
+ return false unless date.is_a?(Date)
64
+
65
+ (1..5).cover?(date.wday)
66
+ end
60
67
  end
61
68
 
62
69
  ##
data/lib/DhanHQ/errors.rb CHANGED
@@ -21,6 +21,8 @@ module DhanHQ
21
21
  class InvalidTokenError < Error; end
22
22
  # DH-810
23
23
  class InvalidClientIDError < Error; end
24
+ # Raised when fetching credentials from a token endpoint fails (HTTP error or invalid response).
25
+ class TokenEndpointError < Error; end
24
26
 
25
27
  # Rate limits and input validation errors
26
28
  # DH-904, 805
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DhanHQ
4
4
  # Semantic version of the DhanHQ client gem.
5
- VERSION = "2.2.0"
5
+ VERSION = "2.2.2"
6
6
  end
@@ -29,6 +29,7 @@ module DhanHQ
29
29
 
30
30
  token = DhanHQ.configuration.resolved_access_token
31
31
  raise DhanHQ::AuthenticationError, "Missing access token" if token.nil? || token.empty?
32
+
32
33
  cid = DhanHQ.configuration.client_id or raise "DhanHQ.client_id not set"
33
34
  ver = (DhanHQ.configuration.respond_to?(:ws_version) && DhanHQ.configuration.ws_version) || 2
34
35
  @url = url || "wss://api-feed.dhan.co?version=#{ver}&token=#{token}&clientId=#{cid}&authType=2"
@@ -82,6 +82,7 @@ module DhanHQ
82
82
  def build_market_depth_url(config)
83
83
  token = config.resolved_access_token
84
84
  raise DhanHQ::AuthenticationError, "Missing access token" if token.nil? || token.empty?
85
+
85
86
  cid = config.client_id or raise "DhanHQ.client_id not set"
86
87
  depth_level = config.market_depth_level || 20 # Default to 20 level depth
87
88
 
@@ -88,6 +88,7 @@ module DhanHQ
88
88
  else
89
89
  token = cfg.resolved_access_token
90
90
  raise DhanHQ::AuthenticationError, "Missing access token" if token.nil? || token.empty?
91
+
91
92
  cid = cfg.client_id or raise "DhanHQ.client_id not set"
92
93
  payload = {
93
94
  LoginReq: { MsgCode: 42, ClientId: cid, Token: token },
data/lib/dhan_hq.rb CHANGED
@@ -23,6 +23,7 @@ require_relative "DhanHQ/error_object"
23
23
  require_relative "DhanHQ/client"
24
24
  require_relative "DhanHQ/configuration"
25
25
  require_relative "DhanHQ/rate_limiter"
26
+ require_relative "DhanHQ/auth"
26
27
 
27
28
  # Contracts
28
29
  require_relative "DhanHQ/contracts/base_contract"
@@ -144,5 +145,75 @@ module DhanHQ
144
145
  configuration.partner_id = ENV.fetch("DHAN_PARTNER_ID", configuration.partner_id)
145
146
  configuration.partner_secret = ENV.fetch("DHAN_PARTNER_SECRET", configuration.partner_secret)
146
147
  end
148
+
149
+ # Configures the DhanHQ client by fetching credentials from a token endpoint.
150
+ #
151
+ # Performs GET <base_url>/auth/dhan/token with Authorization: Bearer <bearer_token>.
152
+ # Expects JSON with at least +access_token+ and +client_id+. Optional +base_url+ in
153
+ # the response overrides the Dhan API base URL.
154
+ #
155
+ # @param base_url [String, nil] Base URL of your app (e.g. https://myapp.com). If nil, uses ENV["DHAN_TOKEN_ENDPOINT_BASE_URL"].
156
+ # @param bearer_token [String, nil] Secret token for the endpoint. If nil, uses ENV["DHAN_TOKEN_ENDPOINT_BEARER"].
157
+ # @return [DhanHQ::Configuration] The configured configuration.
158
+ # @raise [DhanHQ::TokenEndpointError] On HTTP error or when response lacks access_token/client_id.
159
+ #
160
+ # @example Explicit
161
+ # DhanHQ.configure_from_token_endpoint(base_url: "https://myapp.com", bearer_token: "secret-token")
162
+ #
163
+ # @example From ENV (DHAN_TOKEN_ENDPOINT_BASE_URL and DHAN_TOKEN_ENDPOINT_BEARER set)
164
+ # DhanHQ.configure_from_token_endpoint
165
+ def configure_from_token_endpoint(base_url: nil, bearer_token: nil)
166
+ base_url ||= ENV.fetch("DHAN_TOKEN_ENDPOINT_BASE_URL", nil)
167
+ bearer_token ||= ENV.fetch("DHAN_TOKEN_ENDPOINT_BEARER", nil)
168
+
169
+ raise TokenEndpointError, "base_url and bearer_token (or ENV DHAN_TOKEN_ENDPOINT_*) are required" if base_url.to_s.empty? || bearer_token.to_s.empty?
170
+
171
+ url = "#{base_url.to_s.chomp("/")}/auth/dhan/token"
172
+ conn = Faraday.new(url: url) do |c|
173
+ c.response :json, content_type: /\bjson$/
174
+ c.adapter Faraday.default_adapter
175
+ end
176
+
177
+ response = conn.get("") do |req|
178
+ req.headers["Authorization"] = "Bearer #{bearer_token}"
179
+ req.headers["Accept"] = "application/json"
180
+ end
181
+
182
+ unless response.success?
183
+ body = if response.body.is_a?(Hash)
184
+ response.body
185
+ else
186
+ begin
187
+ JSON.parse(response.body.to_s)
188
+ rescue StandardError
189
+ {}
190
+ end
191
+ end
192
+ msg = body["error"] || body["message"] || body["errorMessage"] || response.body.to_s
193
+ raise TokenEndpointError, "Token endpoint returned #{response.status}: #{msg}"
194
+ end
195
+
196
+ data = if response.body.is_a?(Hash)
197
+ response.body
198
+ else
199
+ begin
200
+ JSON.parse(response.body.to_s)
201
+ rescue StandardError
202
+ {}
203
+ end
204
+ end
205
+ data = data.transform_keys(&:to_s) if data.is_a?(Hash)
206
+
207
+ access_token = data["access_token"] || data[:access_token]
208
+ client_id = data["client_id"] || data[:client_id]
209
+ raise TokenEndpointError, "Token endpoint response missing access_token or client_id" if access_token.to_s.empty? || client_id.to_s.empty?
210
+
211
+ self.configuration ||= Configuration.new
212
+ configuration.access_token = access_token.to_s
213
+ configuration.client_id = client_id.to_s
214
+ dhan_base = data["base_url"] || data[:base_url]
215
+ configuration.base_url = dhan_base.to_s if dhan_base.to_s != ""
216
+ configuration
217
+ end
147
218
  end
148
219
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: DhanHQ
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shubham Taywade
@@ -202,6 +202,7 @@ files:
202
202
  - examples/order_update_example.rb
203
203
  - examples/trading_fields_example.rb
204
204
  - exe/DhanHQ
205
+ - lib/DhanHQ/auth.rb
205
206
  - lib/DhanHQ/client.rb
206
207
  - lib/DhanHQ/config.rb
207
208
  - lib/DhanHQ/configuration.rb