@tigeropenapi/tigeropen 0.5.0 → 0.5.4

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.
Files changed (33) hide show
  1. package/dist/cjs/client/api-response.js +17 -15
  2. package/dist/cjs/client/token-methods.js +42 -0
  3. package/dist/cjs/config/client-config.js +59 -11
  4. package/dist/cjs/config/config-parser.js +19 -9
  5. package/dist/cjs/config/index.js +2 -1
  6. package/dist/cjs/index.js +2 -1
  7. package/dist/cjs/model/enums.js +4 -0
  8. package/dist/cjs/model/order-helpers.js +126 -21
  9. package/dist/cjs/quote/quote-client.js +112 -21
  10. package/dist/cjs/trade/trade-client.js +74 -16
  11. package/dist/esm/client/api-response.js +17 -15
  12. package/dist/esm/client/token-methods.js +37 -0
  13. package/dist/esm/config/client-config.js +59 -12
  14. package/dist/esm/config/config-parser.js +19 -9
  15. package/dist/esm/config/index.js +1 -1
  16. package/dist/esm/index.js +1 -1
  17. package/dist/esm/model/enums.js +4 -0
  18. package/dist/esm/model/order-helpers.js +119 -20
  19. package/dist/esm/quote/quote-client.js +79 -21
  20. package/dist/esm/trade/trade-client.js +41 -16
  21. package/dist/types/client/api-response.d.ts +6 -5
  22. package/dist/types/client/token-methods.d.ts +34 -0
  23. package/dist/types/config/client-config.d.ts +2 -0
  24. package/dist/types/config/index.d.ts +1 -1
  25. package/dist/types/index.d.ts +3 -3
  26. package/dist/types/model/enums.d.ts +5 -1
  27. package/dist/types/model/order-helpers.d.ts +48 -8
  28. package/dist/types/model/order.d.ts +52 -1
  29. package/dist/types/model/quote.d.ts +56 -0
  30. package/dist/types/model/trade-requests.d.ts +41 -0
  31. package/dist/types/quote/quote-client.d.ts +31 -8
  32. package/dist/types/trade/trade-client.d.ts +19 -3
  33. package/package.json +1 -1
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.QuoteClient = void 0;
4
37
  exports.parseOptionIdentifier = parseOptionIdentifier;
@@ -12,6 +45,7 @@ exports.parseOptionIdentifier = parseOptionIdentifier;
12
45
  const http_client_1 = require("../client/http-client");
13
46
  const api_request_1 = require("../client/api-request");
14
47
  const api_response_1 = require("../client/api-response");
48
+ const tokenMethods = __importStar(require("../client/token-methods"));
15
49
  /**
16
50
  * resolveOptionTimezone returns the timezone string to use for expiry conversion.
17
51
  * Explicit tz takes precedence; otherwise infer from symbol (.HK suffix → Asia/Hong_Kong, else America/New_York).
@@ -94,6 +128,25 @@ class QuoteClient {
94
128
  static fromConfig(config) {
95
129
  return new QuoteClient(new http_client_1.HttpClient(config, undefined, { useQuoteServerUrl: true }));
96
130
  }
131
+ // ── Token management ──────────────────────────────────────────────────────
132
+ /** Call the `user_token_refresh` API and return the new token. Read-only. */
133
+ async queryToken() {
134
+ return tokenMethods.queryToken(this.httpClient);
135
+ }
136
+ /** Refresh the token and update the in-memory config token. */
137
+ async refreshToken(tokenManager) {
138
+ return tokenMethods.refreshToken(this.httpClient, tokenManager);
139
+ }
140
+ /**
141
+ * Start background token auto-refresh. Returns a TokenManager to stop with `tm.stopAutoRefresh()`.
142
+ *
143
+ * @param refreshDurationSecs - Refresh threshold in seconds (minimum 30).
144
+ * @param checkIntervalMs - How often to check, in milliseconds (default 300 000 / 5 min).
145
+ * @param tokenWriter - Optional callback invoked after each successful refresh.
146
+ */
147
+ startTokenAutoRefresh(refreshDurationSecs, checkIntervalMs, tokenWriter) {
148
+ return tokenMethods.startTokenAutoRefresh(this.httpClient, refreshDurationSecs, checkIntervalMs, tokenWriter);
149
+ }
97
150
  async callInto(method, bizParams, version) {
98
151
  const request = (0, api_request_1.createApiRequest)(method, bizParams, version);
99
152
  const response = await this.httpClient.executeRequest(request);
@@ -155,8 +208,10 @@ class QuoteClient {
155
208
  *
156
209
  * @param timezone - Optional IANA timezone for expiry conversion (e.g. 'America/New_York').
157
210
  * If omitted, defaults to America/New_York for US symbols and Asia/Hong_Kong for .HK symbols.
211
+ * @param returnGreekValue - Whether to return Greeks (delta/gamma/theta/vega/rho).
212
+ * @param optionFilter - Optional filter for in-the-money status, IV range, open interest range, Greeks ranges.
158
213
  */
159
- async getOptionChain(items, timezone) {
214
+ async getOptionChain(items, timezone, returnGreekValue, optionFilter) {
160
215
  const optionBasic = items.map(([symbol, expiry]) => {
161
216
  if (!/^\d{4}-\d{2}-\d{2}$/.test(expiry)) {
162
217
  throw new Error(`invalid expiry date, expected YYYY-MM-DD: ${expiry}`);
@@ -165,37 +220,27 @@ class QuoteClient {
165
220
  const expiryMs = localMidnightMs(expiry, tz);
166
221
  return { symbol, expiry: expiryMs };
167
222
  });
168
- return this.callInto('option_chain', { option_basic: optionBasic }, '3.0');
169
- }
170
- /**
171
- * Real-time option quotes by OCC-style identifier.
172
- *
173
- * @param timezone - Optional IANA timezone for expiry conversion (e.g. 'America/New_York').
174
- * If omitted, defaults to America/New_York for US symbols and Asia/Hong_Kong for .HK symbols.
175
- */
176
- async getOptionQuote(identifiers, timezone) {
177
- const optionBasic = identifiers.map((id) => {
178
- const p = parseOptionIdentifier(id, timezone);
179
- return { symbol: p.symbol, expiry: p.expiryMs, right: p.right, strike: p.strike };
180
- });
181
- return this.callInto('option_brief', { option_basic: optionBasic }, '2.0');
182
- }
183
- /** @deprecated Use getOptionQuote instead. */
184
- async getOptionBrief(identifiers) {
185
- return this.getOptionQuote(identifiers);
223
+ const body = { option_basic: optionBasic };
224
+ if (returnGreekValue !== undefined)
225
+ body['return_greek_value'] = returnGreekValue;
226
+ if (optionFilter !== undefined)
227
+ body['option_filter'] = optionFilter;
228
+ return this.callInto('option_chain', body, '3.0');
186
229
  }
187
230
  /**
188
231
  * Option K-line by OCC-style identifier.
189
232
  *
190
233
  * @param timezone - Optional IANA timezone for expiry conversion (e.g. 'America/New_York').
191
234
  * If omitted, defaults to America/New_York for US symbols and Asia/Hong_Kong for .HK symbols.
235
+ * @param limit - Maximum number of bars to return.
236
+ * @param sortDir - Sort direction ('asc' or 'desc').
192
237
  */
193
- async getOptionKline(identifiers, period, beginTime = -1, endTime = -1, timezone) {
238
+ async getOptionKline(identifiers, period, beginTime = -1, endTime = -1, timezone, limit, sortDir) {
194
239
  const resolvedBegin = beginTime === 0 ? -1 : beginTime;
195
240
  const resolvedEnd = endTime === 0 ? -1 : endTime;
196
241
  const optionQuery = identifiers.map((id) => {
197
242
  const p = parseOptionIdentifier(id, timezone);
198
- return {
243
+ const entry = {
199
244
  symbol: p.symbol,
200
245
  expiry: p.expiryMs,
201
246
  right: p.right,
@@ -204,9 +249,31 @@ class QuoteClient {
204
249
  begin_time: resolvedBegin,
205
250
  end_time: resolvedEnd,
206
251
  };
252
+ if (limit !== undefined && limit > 0)
253
+ entry['limit'] = limit;
254
+ if (sortDir !== undefined && sortDir !== '')
255
+ entry['sort_dir'] = sortDir;
256
+ return entry;
207
257
  });
208
258
  return this.callInto('option_kline', { option_query: optionQuery }, '2.0');
209
259
  }
260
+ /**
261
+ * Real-time option quotes by OCC-style identifier.
262
+ *
263
+ * @param timezone - Optional IANA timezone for expiry conversion (e.g. 'America/New_York').
264
+ * If omitted, defaults to America/New_York for US symbols and Asia/Hong_Kong for .HK symbols.
265
+ */
266
+ async getOptionQuote(identifiers, timezone) {
267
+ const optionBasic = identifiers.map((id) => {
268
+ const p = parseOptionIdentifier(id, timezone);
269
+ return { symbol: p.symbol, expiry: p.expiryMs, right: p.right, strike: p.strike };
270
+ });
271
+ return this.callInto('option_brief', { option_basic: optionBasic }, '2.0');
272
+ }
273
+ /** @deprecated Use getOptionQuote instead. */
274
+ async getOptionBrief(identifiers) {
275
+ return this.getOptionQuote(identifiers);
276
+ }
210
277
  // === Futures ===
211
278
  async getFutureExchange() {
212
279
  return this.callInto('future_exchange', { secType: 'FUT' });
@@ -524,6 +591,30 @@ class QuoteClient {
524
591
  return [];
525
592
  return Object.values(grouped).flat();
526
593
  }
594
+ /** Corporate symbol change (flattened). wire: corporate_action (action_type=symbol_change) */
595
+ async getCorporateSymbolChange(req) {
596
+ const body = { ...req, actionType: 'symbol_change' };
597
+ const grouped = await this.callInto('corporate_action', body);
598
+ if (!grouped || typeof grouped !== 'object')
599
+ return [];
600
+ return Object.values(grouped).flat();
601
+ }
602
+ /** Corporate delisting (flattened). wire: corporate_action (action_type=delisting) */
603
+ async getCorporateDelisting(req) {
604
+ const body = { ...req, actionType: 'delisting' };
605
+ const grouped = await this.callInto('corporate_action', body);
606
+ if (!grouped || typeof grouped !== 'object')
607
+ return [];
608
+ return Object.values(grouped).flat();
609
+ }
610
+ /** Corporate IPO (flattened). wire: corporate_action (action_type=ipo) */
611
+ async getCorporateIPO(req) {
612
+ const body = { ...req, actionType: 'ipo' };
613
+ const grouped = await this.callInto('corporate_action', body);
614
+ if (!grouped || typeof grouped !== 'object')
615
+ return [];
616
+ return Object.values(grouped).flat();
617
+ }
527
618
  /** Financial currency per symbol. wire: financial_currency */
528
619
  async getFinancialCurrency(req) {
529
620
  return this.callInto('financial_currency', req);
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.TradeClient = void 0;
4
37
  /**
@@ -13,6 +46,7 @@ exports.TradeClient = void 0;
13
46
  const http_client_1 = require("../client/http-client");
14
47
  const api_request_1 = require("../client/api-request");
15
48
  const api_response_1 = require("../client/api-response");
49
+ const tokenMethods = __importStar(require("../client/token-methods"));
16
50
  const order_status_1 = require("../model/order-status");
17
51
  /** Normalize `Order.status` field in-place (handles int → string). */
18
52
  function normalizeOrderInPlace(o) {
@@ -44,8 +78,42 @@ class TradeClient {
44
78
  static fromConfig(config, account, secretKey) {
45
79
  return new TradeClient(new http_client_1.HttpClient(config), account, secretKey);
46
80
  }
81
+ // ── Token management ──────────────────────────────────────────────────────
82
+ /** Call the `user_token_refresh` API and return the new token. Read-only. */
83
+ async queryToken() {
84
+ return tokenMethods.queryToken(this.httpClient);
85
+ }
86
+ /**
87
+ * Refresh the token: call the API, update the in-memory config token, and
88
+ * optionally persist via the provided TokenManager (file + writer callback).
89
+ *
90
+ * @param tokenManager - optional TokenManager for file persistence and writer callback
91
+ */
92
+ async refreshToken(tokenManager) {
93
+ return tokenMethods.refreshToken(this.httpClient, tokenManager);
94
+ }
95
+ /**
96
+ * Start background token auto-refresh. Returns a TokenManager that can be
97
+ * used to stop the background task via `tm.stopAutoRefresh()`.
98
+ *
99
+ * @param refreshDurationSecs - Refresh threshold in seconds (minimum 30).
100
+ * @param checkIntervalMs - How often to check, in milliseconds (default 300 000 / 5 min).
101
+ * @param tokenWriter - Optional callback invoked after each successful refresh.
102
+ */
103
+ startTokenAutoRefresh(refreshDurationSecs, checkIntervalMs, tokenWriter) {
104
+ return tokenMethods.startTokenAutoRefresh(this.httpClient, refreshDurationSecs, checkIntervalMs, tokenWriter);
105
+ }
47
106
  async callInto(method, bizParams) {
48
- const request = (0, api_request_1.createApiRequest)(method, bizParams);
107
+ // Inject secretKey into every request when available (institution accounts require it).
108
+ // Individual methods that already set secretKey will not be overwritten (|| not &&).
109
+ let params = bizParams;
110
+ if (this.secretKey && params !== null && typeof params === 'object' && !Array.isArray(params)) {
111
+ const p = params;
112
+ if (p['secretKey'] == null && p['secret_key'] == null) {
113
+ params = { ...p, secretKey: this.secretKey };
114
+ }
115
+ }
116
+ const request = (0, api_request_1.createApiRequest)(method, params);
49
117
  const response = await this.httpClient.executeRequest(request);
50
118
  return (0, api_response_1.unmarshalData)(response.data);
51
119
  }
@@ -78,25 +146,18 @@ class TradeClient {
78
146
  }
79
147
  // === Order operations ===
80
148
  async placeOrder(order) {
81
- const params = { ...order, account: this.account };
82
- if (!params.secretKey && this.secretKey)
83
- params.secretKey = this.secretKey;
84
- return this.callInto('place_order', params);
149
+ return this.callInto('place_order', { ...order, account: this.account });
85
150
  }
86
151
  async previewOrder(order) {
87
152
  return this.callInto('preview_order', { ...order, account: this.account });
88
153
  }
89
154
  async modifyOrder(id, order) {
90
- const params = { ...order, account: this.account, id };
91
- if (!params.secretKey && this.secretKey)
92
- params.secretKey = this.secretKey;
93
- return this.callInto('modify_order', params);
155
+ return this.callInto('modify_order', { ...order, account: this.account, id });
94
156
  }
95
157
  async cancelOrder(id, secretKey) {
96
- const sk = secretKey ?? this.secretKey;
97
158
  const params = { account: this.account, id };
98
- if (sk)
99
- params.secretKey = sk;
159
+ if (secretKey !== undefined)
160
+ params.secretKey = secretKey; // per-call override takes precedence; undefined means use config default
100
161
  return this.callInto('cancel_order', params);
101
162
  }
102
163
  // === Order queries (BREAKING v0.4.0) ===
@@ -134,14 +195,11 @@ class TradeClient {
134
195
  }
135
196
  /**
136
197
  * Get a single order by ID (v0.4.0 new).
137
- * Wire method: `orders` with id/order_id param; server returns a single Order object
138
- * (not `{items:[]}` envelope).
139
- *
140
198
  * Returns `undefined` if no match.
141
199
  */
142
200
  async getOrder(req) {
143
201
  const merged = withAccount(req, this.account);
144
- const out = await this.callInto('orders', merged);
202
+ const out = await this.callInto('order_no', merged);
145
203
  if (!out || (!out.id && !out.orderId))
146
204
  return undefined;
147
205
  normalizeOrderInPlace(out);
@@ -3,29 +3,33 @@
3
3
  */
4
4
  import { TigerError } from './errors.js';
5
5
  /**
6
- * Replace bare integers (not inside JSON string values) that exceed
7
- * Number.MAX_SAFE_INTEGER (> 15 digits) with quoted string equivalents,
8
- * so that int64 order IDs survive JSON.parse without precision loss.
6
+ * Replaces bare int64 values (≥17 digits, not part of a float) with quoted
7
+ * strings so they survive JSON.parse without precision loss.
8
+ * 17+ digits are guaranteed to exceed Number.MAX_SAFE_INTEGER (≈9.007e15),
9
+ * so 16-digit microsecond timestamps remain as numbers.
9
10
  */
10
11
  function patchLargeIntegers(text) {
11
- // Mask all existing JSON string values so their content isn't touched.
12
12
  const strings = [];
13
13
  const masked = text.replace(/"(?:[^"\\]|\\.)*"/g, (match) => {
14
14
  strings.push(match);
15
15
  return '\x01' + (strings.length - 1) + '\x01';
16
16
  });
17
- // Quote any bare integer with >15 digits.
18
- const patched = masked.replace(/-?\d{16,}/g, (m) => '"' + m + '"');
19
- // Restore original string values.
17
+ // Match bare integers preceded by a JSON structural character or line start,
18
+ // not followed by decimal point or exponent (avoids matching float components).
19
+ // The `m` flag makes `^` match line-start for multi-line JSON.
20
+ const patched = masked.replace(/([:,\[{]|^)\s*(-?\d{17,})(?![.\deE])/gm, (full, prefix, num) => {
21
+ return prefix + '"' + num + '"';
22
+ });
20
23
  return patched.replace(/\x01(\d+)\x01/g, (_, i) => strings[parseInt(i, 10)]);
21
24
  }
22
25
  /**
23
- * Parse the raw HTTP response body.
24
- * Throws TigerError when code != 0.
25
- * Large integers in the outer envelope are not an issue (only inner `data` contains int64 IDs).
26
+ * Parse the raw HTTP response body. Throws TigerError when code != 0.
27
+ *
28
+ * patchLargeIntegers is applied before JSON.parse to preserve int64 order IDs
29
+ * (e.g. 28868646234578944) as strings rather than losing precision.
26
30
  */
27
31
  export function parseApiResponse(body) {
28
- const resp = JSON.parse(body);
32
+ const resp = JSON.parse(patchLargeIntegers(body));
29
33
  if (resp.code !== 0) {
30
34
  throw new TigerError(resp.code, resp.message);
31
35
  }
@@ -33,15 +37,13 @@ export function parseApiResponse(body) {
33
37
  }
34
38
  /**
35
39
  * Decode an ApiResponse.data payload into a typed value.
36
- * Handles the server's occasional double-encoded JSON (where `data` is a
37
- * JSON string that itself wraps JSON), seen on some trade endpoints.
40
+ * Handles double-encoded JSON (where `data` is a JSON string wrapping JSON),
41
+ * seen on some trade endpoints.
38
42
  */
39
43
  export function unmarshalData(data) {
40
44
  if (data == null)
41
45
  return undefined;
42
46
  if (typeof data === 'string') {
43
- // Attempt to parse JSON string (double-encoded case).
44
- // Patch large integers before parsing so int64 IDs survive as strings.
45
47
  try {
46
48
  return JSON.parse(patchLargeIntegers(data));
47
49
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Call the `user_token_refresh` API and return the new token string.
3
+ * Read-only: does NOT update the stored token.
4
+ * Use `refreshToken()` to also update the in-memory config and optionally persist.
5
+ */
6
+ export async function queryToken(httpClient) {
7
+ return httpClient.queryToken();
8
+ }
9
+ /**
10
+ * Refresh the token: call the API, update the in-memory config token, and
11
+ * optionally persist via the provided TokenManager (file + writer callback).
12
+ *
13
+ * @param httpClient - The underlying HTTP client
14
+ * @param tokenManager - optional TokenManager for file persistence and writer callback
15
+ */
16
+ export async function refreshToken(httpClient, tokenManager) {
17
+ return httpClient.refreshToken(tokenManager);
18
+ }
19
+ /**
20
+ * Start background token auto-refresh. Returns a TokenManager that can be
21
+ * used to stop the background task via `tm.stopAutoRefresh()`.
22
+ *
23
+ * @param httpClient - The underlying HTTP client
24
+ * @param refreshDurationSecs - Refresh threshold in seconds (minimum 30).
25
+ * When the token age exceeds this value the refresh is triggered.
26
+ * @param checkIntervalMs - How often to check whether a refresh is needed,
27
+ * in milliseconds. Defaults to 300 000 ms (5 minutes).
28
+ * @param tokenWriter - Optional callback invoked with the new token string
29
+ * after each successful refresh.
30
+ */
31
+ export function startTokenAutoRefresh(httpClient, refreshDurationSecs, checkIntervalMs, tokenWriter) {
32
+ return httpClient.startTokenAutoRefresh(null, {
33
+ refreshDuration: refreshDurationSecs,
34
+ refreshInterval: checkIntervalMs,
35
+ tokenWriter,
36
+ });
37
+ }
@@ -9,7 +9,7 @@
9
9
  * Priority: environment variables > code options (incl. config file) > defaults
10
10
  */
11
11
  import { readFileSync, existsSync, statSync } from 'fs';
12
- import { join } from 'path';
12
+ import { join, dirname } from 'path';
13
13
  import { homedir, networkInterfaces } from 'os';
14
14
  import { parsePropertiesString } from './config-parser.js';
15
15
  import { queryDomains, resolveDynamicServerUrl, resolveDynamicQuoteServerUrl } from './domain.js';
@@ -22,6 +22,10 @@ const DEFAULT_TIMEOUT = 15;
22
22
  const DEFAULT_SERVER_URL = 'https://openapi.tigerfintech.com/gateway';
23
23
  /** Tiger public key for response signature verification (Base64-encoded) */
24
24
  const TIGER_PUBLIC_KEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNF3G8SoEcCZh2rshUbayDgLLrj6rKgzNMxDL2HSnKcB0+GPOsndqSv+a4IBu9+I3fyBp5hkyMMG2+AXugd9pMpy6VxJxlNjhX1MYbNTZJUT4nudki4uh+LMOkIBHOceGNXjgB+cXqmlUnjlqha/HgboeHSnSgpM3dKSJQlIOsDwIDAQAB';
25
+ /** Sandbox / QA tiger public key for response signature verification */
26
+ export const SANDBOX_TIGER_PUBLIC_KEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCbm21i11hgAENGd3/f280PSe4g9YGkS3TEXBYMidihTvHHf' +
27
+ '+tJ0PYD0o3PruI0hl3qhEjHTAxb75T5YD3SGK4IBhHn/Rk6mhqlGgI+bBrBVYaXixmHfRo75RpUUuWACyeqQk' +
28
+ 'ZckgR0McxuW9xRMIa2cXZOoL1E4SL4lXKGhKoWbwIDAQAB';
25
29
  /** Environment variable names */
26
30
  const ENV_TIGER_ID = 'TIGEROPEN_TIGER_ID';
27
31
  const ENV_PRIVATE_KEY = 'TIGEROPEN_PRIVATE_KEY';
@@ -29,6 +33,9 @@ const ENV_ACCOUNT = 'TIGEROPEN_ACCOUNT';
29
33
  const ENV_SECRET_KEY = 'TIGEROPEN_SECRET_KEY';
30
34
  const ENV_TOKEN = 'TIGEROPEN_TOKEN';
31
35
  const ENV_TOKEN_FILE = 'TIGEROPEN_TOKEN_FILE';
36
+ const ENV_TIGER_PUBLIC_KEY = 'TIGEROPEN_TIGER_PUBLIC_KEY';
37
+ const ENV_SERVER_URL = 'TIGEROPEN_SERVER_URL';
38
+ const ENV_QUOTE_SERVER_URL = 'TIGEROPEN_QUOTE_SERVER_URL';
32
39
  /**
33
40
  * Load and parse a Java-style properties file.
34
41
  *
@@ -101,23 +108,32 @@ export function createClientConfig(options) {
101
108
  const opts = options ?? {};
102
109
  // Load properties: explicit path > auto-discovery
103
110
  let fileProps = {};
111
+ let configFileDir;
104
112
  if (opts.propertiesFilePath) {
105
113
  // Support both directory path and full file path
106
114
  let filePath = opts.propertiesFilePath;
107
115
  if (existsSync(filePath) && statSync(filePath).isDirectory()) {
116
+ configFileDir = filePath;
108
117
  filePath = join(filePath, CONFIG_FILE_NAME);
109
118
  }
110
119
  else if (!existsSync(filePath) && !filePath.endsWith('.properties')) {
111
120
  // Path doesn't exist as-is; try treating it as a directory
112
121
  const asDir = join(filePath, CONFIG_FILE_NAME);
113
- if (existsSync(asDir))
122
+ if (existsSync(asDir)) {
123
+ configFileDir = filePath;
114
124
  filePath = asDir;
125
+ }
126
+ }
127
+ else {
128
+ // It's a file path: derive directory from it
129
+ configFileDir = dirname(filePath);
115
130
  }
116
131
  fileProps = loadPropertiesFile(filePath);
117
132
  }
118
133
  else {
119
134
  const discovered = discoverConfigFile();
120
135
  if (discovered) {
136
+ configFileDir = dirname(discovered);
121
137
  fileProps = loadPropertiesFile(discovered);
122
138
  }
123
139
  }
@@ -130,12 +146,15 @@ export function createClientConfig(options) {
130
146
  const license = opts.license || fileProps['license'];
131
147
  const language = opts.language || fileProps['language'] || DEFAULT_LANGUAGE;
132
148
  const timezone = opts.timezone || fileProps['timezone'];
149
+ // tiger_public_key from properties file (e.g. for QA/sandbox environments)
150
+ let tigerPublicKey = opts.tigerPublicKey || fileProps['tiger_public_key'] || TIGER_PUBLIC_KEY;
133
151
  // Environment variables override everything
134
152
  const envTigerId = process.env[ENV_TIGER_ID];
135
153
  const envPrivateKey = process.env[ENV_PRIVATE_KEY];
136
154
  const envAccount = process.env[ENV_ACCOUNT];
137
155
  const envSecretKey = process.env[ENV_SECRET_KEY];
138
156
  const envToken = process.env[ENV_TOKEN];
157
+ const envTigerPublicKey = process.env[ENV_TIGER_PUBLIC_KEY];
139
158
  if (envTigerId) {
140
159
  tigerId = envTigerId;
141
160
  }
@@ -151,6 +170,9 @@ export function createClientConfig(options) {
151
170
  if (envToken) {
152
171
  token = envToken;
153
172
  }
173
+ if (envTigerPublicKey) {
174
+ tigerPublicKey = envTigerPublicKey;
175
+ }
154
176
  // Token loading priority: env TIGEROPEN_TOKEN > tokenLoader > token file (TIGEROPEN_TOKEN_FILE or default)
155
177
  if (!token) {
156
178
  if (opts.tokenLoader) {
@@ -171,8 +193,9 @@ export function createClientConfig(options) {
171
193
  }
172
194
  }
173
195
  else {
174
- // Load from token file: TIGEROPEN_TOKEN_FILE env var > default file
175
- const tokenFilePath = process.env[ENV_TOKEN_FILE] || 'tiger_openapi_token.properties';
196
+ // Load from token file: TIGEROPEN_TOKEN_FILE env var > config dir > default
197
+ const tokenFilePath = process.env[ENV_TOKEN_FILE]
198
+ || (configFileDir ? join(configFileDir, 'tiger_openapi_token.properties') : 'tiger_openapi_token.properties');
176
199
  const tm = new TokenManager({ filePath: tokenFilePath });
177
200
  try {
178
201
  token = tm.loadTokenSync();
@@ -189,29 +212,53 @@ export function createClientConfig(options) {
189
212
  if (!privateKey) {
190
213
  throw new Error(`privateKey is required. Set it via options.privateKey, a config file, or the ${ENV_PRIVATE_KEY} environment variable.`);
191
214
  }
192
- // Resolve server URL: explicit > dynamic domain > default
215
+ // Resolve server URL: env var > explicit opts > file > dynamic domain > default
193
216
  const enableDynamicDomain = opts.enableDynamicDomain ?? true;
194
217
  let serverUrl;
195
218
  let quoteServerUrl;
196
219
  // Query dynamic domains once (may spawn a child process); reuse for both URLs.
197
220
  let domainConf = {};
198
- if (enableDynamicDomain && (!opts.serverUrl || !opts.quoteServerUrl)) {
221
+ const envServerUrl = process.env[ENV_SERVER_URL];
222
+ const envQuoteServerUrl = process.env[ENV_QUOTE_SERVER_URL];
223
+ const needServerUrl = !envServerUrl && !opts.serverUrl && !fileProps['server_url'];
224
+ const needQuoteServerUrl = !envQuoteServerUrl && !opts.quoteServerUrl && !fileProps['quote_server_url'];
225
+ if (enableDynamicDomain && (needServerUrl || needQuoteServerUrl)) {
199
226
  domainConf = queryDomains(license);
200
227
  }
201
- if (opts.serverUrl) {
228
+ if (envServerUrl) {
229
+ serverUrl = envServerUrl;
230
+ }
231
+ else if (opts.serverUrl) {
202
232
  serverUrl = opts.serverUrl;
203
233
  }
234
+ else if (fileProps['server_url']) {
235
+ serverUrl = fileProps['server_url'];
236
+ }
204
237
  else {
205
238
  const dynamicUrl = resolveDynamicServerUrl(domainConf, license);
206
239
  serverUrl = dynamicUrl || DEFAULT_SERVER_URL;
207
240
  }
208
- // Resolve quote server URL: explicit > dynamic domain > serverUrl fallback
209
- if (opts.quoteServerUrl) {
241
+ // Resolve quote server URL: env var > explicit opts > file > dynamic domain > serverUrl fallback
242
+ if (envQuoteServerUrl) {
243
+ quoteServerUrl = envQuoteServerUrl;
244
+ }
245
+ else if (opts.quoteServerUrl) {
210
246
  quoteServerUrl = opts.quoteServerUrl;
211
247
  }
248
+ else if (fileProps['quote_server_url']) {
249
+ quoteServerUrl = fileProps['quote_server_url'];
250
+ }
212
251
  else {
213
- const dynamicQuoteUrl = resolveDynamicQuoteServerUrl(domainConf, license);
214
- quoteServerUrl = dynamicQuoteUrl || serverUrl;
252
+ // Only use dynamic domain if serverUrl itself came from dynamic resolution (not pinned by env/opts/file).
253
+ // When serverUrl is pinned, prefer it as the quote fallback to avoid mixing environments.
254
+ const serverUrlPinned = !!(envServerUrl || opts.serverUrl || fileProps['server_url']);
255
+ if (!serverUrlPinned) {
256
+ const dynamicQuoteUrl = resolveDynamicQuoteServerUrl(domainConf, license);
257
+ quoteServerUrl = dynamicQuoteUrl || serverUrl;
258
+ }
259
+ else {
260
+ quoteServerUrl = serverUrl;
261
+ }
215
262
  }
216
263
  return {
217
264
  tigerId,
@@ -230,7 +277,7 @@ export function createClientConfig(options) {
230
277
  serverUrl,
231
278
  quoteServerUrl,
232
279
  deviceId: detectDeviceId(),
233
- tigerPublicKey: opts.tigerPublicKey ?? TIGER_PUBLIC_KEY,
280
+ tigerPublicKey,
234
281
  };
235
282
  }
236
283
  /**
@@ -33,34 +33,34 @@ export function parsePropertiesString(content) {
33
33
  let continuation = false;
34
34
  for (const rawLine of lines) {
35
35
  if (continuation) {
36
- // 续行:去除前导空格后拼接
37
36
  const trimmedLeft = rawLine.replace(/^[ \t]+/, '');
38
- if (trimmedLeft.endsWith('\\')) {
37
+ if (trimmedLeft === '' || trimmedLeft.startsWith('#') || trimmedLeft.startsWith('!')) {
38
+ continuation = false;
39
+ }
40
+ else if (endsWithContinuation(trimmedLeft)) {
39
41
  currentLine += trimmedLeft.slice(0, -1);
40
42
  continue;
41
43
  }
42
- currentLine += trimmedLeft;
43
- continuation = false;
44
+ else {
45
+ currentLine += trimmedLeft;
46
+ continuation = false;
47
+ }
44
48
  }
45
49
  else {
46
50
  const trimmed = rawLine.trim();
47
- // 跳过空行
48
51
  if (trimmed === '') {
49
52
  continue;
50
53
  }
51
- // 跳过注释行
52
54
  if (trimmed.startsWith('#') || trimmed.startsWith('!')) {
53
55
  continue;
54
56
  }
55
- // 检查续行
56
- if (trimmed.endsWith('\\')) {
57
+ if (endsWithContinuation(trimmed)) {
57
58
  currentLine = trimmed.slice(0, -1);
58
59
  continuation = true;
59
60
  continue;
60
61
  }
61
62
  currentLine = trimmed;
62
63
  }
63
- // 解析键值对
64
64
  const result = parseKeyValue(currentLine);
65
65
  if (result) {
66
66
  props[result[0]] = result[1];
@@ -76,6 +76,16 @@ export function parsePropertiesString(content) {
76
76
  }
77
77
  return props;
78
78
  }
79
+ /**
80
+ * 判断一行是否以续行标记结尾(奇数个末尾反斜杠)。
81
+ */
82
+ function endsWithContinuation(line) {
83
+ let count = 0;
84
+ for (let i = line.length - 1; i >= 0 && line[i] === '\\'; i--) {
85
+ count++;
86
+ }
87
+ return count % 2 === 1;
88
+ }
79
89
  /**
80
90
  * 解析单行键值对,支持 = 和 : 分隔符。
81
91
  * 值中可以包含 = 或 :,只按第一个分隔符拆分。