@investoday/investoday-api 1.8.7 → 1.8.9
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.
- package/README.md +67 -2
- package/data/openapi.json +121 -0
- package/data/tree.json +14 -0
- package/lib/call-api.js +112 -17
- package/lib/config.js +55 -86
- package/lib/scheduler.js +309 -0
- package/lib/update.js +655 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,9 +16,16 @@ Configure the API key once:
|
|
|
16
16
|
investoday-api init
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
Enable or disable background auto update during initialization:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
investoday-api init --auto-update
|
|
23
|
+
investoday-api init --no-auto-update
|
|
24
|
+
```
|
|
25
|
+
|
|
19
26
|
Get an API key from:
|
|
20
27
|
|
|
21
|
-
- https://data-api.investoday.net/
|
|
28
|
+
- https://data-api.investoday.net/user/api-key
|
|
22
29
|
|
|
23
30
|
## Usage
|
|
24
31
|
|
|
@@ -26,6 +33,14 @@ Get an API key from:
|
|
|
26
33
|
investoday-api --help
|
|
27
34
|
investoday-api init
|
|
28
35
|
investoday-api config status
|
|
36
|
+
investoday-api config path
|
|
37
|
+
investoday-api config remove
|
|
38
|
+
investoday-api update status
|
|
39
|
+
investoday-api update run
|
|
40
|
+
investoday-api update enable
|
|
41
|
+
investoday-api update disable
|
|
42
|
+
investoday-api update register
|
|
43
|
+
investoday-api update unregister
|
|
29
44
|
investoday-api list
|
|
30
45
|
investoday-api list 沪深京数据
|
|
31
46
|
investoday-api search-api query=违规处罚 tool_ids=list_stock_violation_penalt
|
|
@@ -48,11 +63,61 @@ investoday-api search key=贵州茅台 type=11
|
|
|
48
63
|
investoday-api fund/daily-quotes --method POST fundCode=000001 beginDate=2024-01-01 endDate=2024-12-31
|
|
49
64
|
```
|
|
50
65
|
|
|
66
|
+
## Background updates
|
|
67
|
+
|
|
68
|
+
`investoday-api` can periodically update the CLI package and installed skills after user authorization.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Show task registration status, last run, errors, next run, and local/remote versions
|
|
72
|
+
investoday-api update status
|
|
73
|
+
|
|
74
|
+
# Run one update immediately; skips when auto update is disabled
|
|
75
|
+
investoday-api update run
|
|
76
|
+
|
|
77
|
+
# Enable auto update and register the user-level scheduled task
|
|
78
|
+
investoday-api update enable
|
|
79
|
+
|
|
80
|
+
# Disable auto update and unregister the scheduled task
|
|
81
|
+
investoday-api update disable
|
|
82
|
+
|
|
83
|
+
# Register or repair the scheduled task without changing the enabled flag
|
|
84
|
+
investoday-api update register
|
|
85
|
+
|
|
86
|
+
# Unregister only the scheduled task without changing the enabled flag
|
|
87
|
+
investoday-api update unregister
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The default manifest URL is:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
https://storage.txyun.investoday.net/application/skill-store/configs/investoday-api.manifest.json
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
For debugging, staging, or private deployments, override it with:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
INVESTODAY_API_UPDATE_MANIFEST_URL=https://example.com/investoday-api.manifest.json investoday-api update status
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Config commands
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Show whether the API key is configured and where it is loaded from
|
|
106
|
+
investoday-api config status
|
|
107
|
+
|
|
108
|
+
# Print the local config file path
|
|
109
|
+
investoday-api config path
|
|
110
|
+
|
|
111
|
+
# Remove the local config and legacy credential files
|
|
112
|
+
investoday-api config remove
|
|
113
|
+
```
|
|
114
|
+
|
|
51
115
|
## Notes
|
|
52
116
|
|
|
53
|
-
- Uses the local
|
|
117
|
+
- Uses the local JSON config created by `investoday-api init`
|
|
54
118
|
- Only calls `https://data-api.investoday.net/data`
|
|
55
119
|
- Bundles endpoint metadata for `list` and `search-api`
|
|
56
120
|
- `search-api` defaults to JSON output and includes params, response fields, and `exampleCommand`; use `--text` for a human-readable summary
|
|
57
121
|
- `search-api` only accepts structured inputs such as `query=` and `tool_ids=`; `query=` accepts one value and supports comma-separated keywords
|
|
122
|
+
- Background update settings are stored in the local JSON config and can be inspected with `investoday-api update status`
|
|
58
123
|
- Prints the API response `data` field as formatted JSON
|
package/data/openapi.json
CHANGED
|
@@ -6326,6 +6326,127 @@
|
|
|
6326
6326
|
]
|
|
6327
6327
|
}
|
|
6328
6328
|
},
|
|
6329
|
+
"/stock/finance/financial-strength": {
|
|
6330
|
+
"get": {
|
|
6331
|
+
"tags": [
|
|
6332
|
+
"股票财务实力的最新指标"
|
|
6333
|
+
],
|
|
6334
|
+
"summary": "股票财务实力的最新指标",
|
|
6335
|
+
"description": "通过股票代码获取指定股票的财务实力最新指标,包括现金负债比率、股东权益比率、利息保障倍数等核心财务指标的当前值(TTM)及对应行业排名、历史行业排名,还有基本面趋势F分数、两年破产风险Z分数、财务造假嫌疑M分数、财务实力综合得分,以及股票代码和名称,可用于评估公司财务健康状况、开展个股财务分析等场景。",
|
|
6336
|
+
"operationId": "get_stock_finance_strength",
|
|
6337
|
+
"parameters": [
|
|
6338
|
+
{
|
|
6339
|
+
"name": "stockCode",
|
|
6340
|
+
"in": "query",
|
|
6341
|
+
"description": "股票代码",
|
|
6342
|
+
"required": true,
|
|
6343
|
+
"schema": {
|
|
6344
|
+
"type": "string"
|
|
6345
|
+
}
|
|
6346
|
+
}
|
|
6347
|
+
],
|
|
6348
|
+
"responses": {
|
|
6349
|
+
"200": {
|
|
6350
|
+
"description": "成功响应",
|
|
6351
|
+
"content": {
|
|
6352
|
+
"application/json": {
|
|
6353
|
+
"schema": {
|
|
6354
|
+
"type": "object",
|
|
6355
|
+
"properties": {
|
|
6356
|
+
"f2190": {
|
|
6357
|
+
"type": "string",
|
|
6358
|
+
"description": "现金负债率当前值(TTM)",
|
|
6359
|
+
"example": ""
|
|
6360
|
+
},
|
|
6361
|
+
"f2190Rk": {
|
|
6362
|
+
"type": "string",
|
|
6363
|
+
"description": "现金负债率行业排名",
|
|
6364
|
+
"example": ""
|
|
6365
|
+
},
|
|
6366
|
+
"f2190RkHist": {
|
|
6367
|
+
"type": "string",
|
|
6368
|
+
"description": "现金负债率历史行业排名",
|
|
6369
|
+
"example": ""
|
|
6370
|
+
},
|
|
6371
|
+
"f2200": {
|
|
6372
|
+
"type": "string",
|
|
6373
|
+
"description": "股东权益比率当前值(TTM)",
|
|
6374
|
+
"example": ""
|
|
6375
|
+
},
|
|
6376
|
+
"f2200Rk": {
|
|
6377
|
+
"type": "string",
|
|
6378
|
+
"description": "股东权益比率行业排名",
|
|
6379
|
+
"example": ""
|
|
6380
|
+
},
|
|
6381
|
+
"f2200RkHist": {
|
|
6382
|
+
"type": "string",
|
|
6383
|
+
"description": "股东权益比率历史行业排名",
|
|
6384
|
+
"example": ""
|
|
6385
|
+
},
|
|
6386
|
+
"f2210": {
|
|
6387
|
+
"type": "string",
|
|
6388
|
+
"description": "利息保障倍数当前值(TTM)",
|
|
6389
|
+
"example": ""
|
|
6390
|
+
},
|
|
6391
|
+
"f2210Rk": {
|
|
6392
|
+
"type": "string",
|
|
6393
|
+
"description": "利息保障倍数行业排名【0-100】",
|
|
6394
|
+
"example": ""
|
|
6395
|
+
},
|
|
6396
|
+
"f2210RkHist": {
|
|
6397
|
+
"type": "string",
|
|
6398
|
+
"description": "利息保障倍数历史行业排名【0-100】",
|
|
6399
|
+
"example": ""
|
|
6400
|
+
},
|
|
6401
|
+
"f2220": {
|
|
6402
|
+
"type": "string",
|
|
6403
|
+
"description": "基本面趋势(F分数)当前值(TTM)",
|
|
6404
|
+
"example": ""
|
|
6405
|
+
},
|
|
6406
|
+
"f2230": {
|
|
6407
|
+
"type": "string",
|
|
6408
|
+
"description": "两年破产风险(Z分数)当前值(TTM)",
|
|
6409
|
+
"example": ""
|
|
6410
|
+
},
|
|
6411
|
+
"f2240": {
|
|
6412
|
+
"type": "string",
|
|
6413
|
+
"description": "财务造假嫌疑(M分数)当前值(TTM)",
|
|
6414
|
+
"example": ""
|
|
6415
|
+
},
|
|
6416
|
+
"f2520": {
|
|
6417
|
+
"type": "string",
|
|
6418
|
+
"description": "财务实力综合得分",
|
|
6419
|
+
"example": ""
|
|
6420
|
+
},
|
|
6421
|
+
"stockCode": {
|
|
6422
|
+
"type": "string",
|
|
6423
|
+
"description": "股票代码",
|
|
6424
|
+
"example": ""
|
|
6425
|
+
},
|
|
6426
|
+
"stockName": {
|
|
6427
|
+
"type": "string",
|
|
6428
|
+
"description": "股票名称",
|
|
6429
|
+
"example": ""
|
|
6430
|
+
}
|
|
6431
|
+
}
|
|
6432
|
+
}
|
|
6433
|
+
}
|
|
6434
|
+
}
|
|
6435
|
+
},
|
|
6436
|
+
"400": {
|
|
6437
|
+
"description": "无效请求"
|
|
6438
|
+
},
|
|
6439
|
+
"500": {
|
|
6440
|
+
"description": "服务器内部错误"
|
|
6441
|
+
}
|
|
6442
|
+
},
|
|
6443
|
+
"security": [
|
|
6444
|
+
{
|
|
6445
|
+
"ApiKeyAuth": []
|
|
6446
|
+
}
|
|
6447
|
+
]
|
|
6448
|
+
}
|
|
6449
|
+
},
|
|
6329
6450
|
"/stock/finance/financial-strength-ht": {
|
|
6330
6451
|
"get": {
|
|
6331
6452
|
"tags": [
|
package/data/tree.json
CHANGED
|
@@ -1842,6 +1842,20 @@
|
|
|
1842
1842
|
"apiAccessLevel": "L2",
|
|
1843
1843
|
"apiMethod": "GET"
|
|
1844
1844
|
},
|
|
1845
|
+
{
|
|
1846
|
+
"apiId": 10250,
|
|
1847
|
+
"apiName": "股票财务实力的最新指标",
|
|
1848
|
+
"apiType": "PROXY",
|
|
1849
|
+
"apiStatus": "release",
|
|
1850
|
+
"groupId": 1072,
|
|
1851
|
+
"remarks": null,
|
|
1852
|
+
"toolName": "get_stk_fin_strength",
|
|
1853
|
+
"toolId": "get_stock_finance_strength",
|
|
1854
|
+
"apiPath": "stock/finance/financial-strength",
|
|
1855
|
+
"enabled": 1,
|
|
1856
|
+
"apiAccessLevel": "L2",
|
|
1857
|
+
"apiMethod": "GET"
|
|
1858
|
+
},
|
|
1845
1859
|
{
|
|
1846
1860
|
"apiId": 10251,
|
|
1847
1861
|
"apiName": "股票财务实力的历史指标",
|
package/lib/call-api.js
CHANGED
|
@@ -9,6 +9,7 @@ const {
|
|
|
9
9
|
saveCredentials,
|
|
10
10
|
} = require("./config");
|
|
11
11
|
const { version: PACKAGE_VERSION } = require("../package.json");
|
|
12
|
+
const { runUpdateCommand } = require("./update");
|
|
12
13
|
|
|
13
14
|
const BASE_URL = "https://data-api.investoday.net/data";
|
|
14
15
|
const REQUEST_TIMEOUT = 30_000;
|
|
@@ -36,6 +37,7 @@ function printHelp() {
|
|
|
36
37
|
"Usage:\n" +
|
|
37
38
|
" investoday-api init\n" +
|
|
38
39
|
" investoday-api config status|path|remove\n" +
|
|
40
|
+
" investoday-api update run|status|enable|disable|register|unregister\n" +
|
|
39
41
|
" investoday-api <endpoint> [key=value ...] [--method GET|POST]\n" +
|
|
40
42
|
" investoday-api list [group-or-subgroup]\n" +
|
|
41
43
|
" investoday-api search-api query=<query> [tool_ids=<tool_id,...>] [--text]\n" +
|
|
@@ -43,7 +45,8 @@ function printHelp() {
|
|
|
43
45
|
" investoday-api --help\n\n" +
|
|
44
46
|
"Commands:\n" +
|
|
45
47
|
" init Initialize local InvestToday API key configuration\n" +
|
|
46
|
-
" config Show, locate, or remove the local
|
|
48
|
+
" config Show, locate, or remove the local configuration\n" +
|
|
49
|
+
" update Manage background updates for investoday-api and installed skills\n" +
|
|
47
50
|
" list List available groups, subgroups, or endpoints from bundled metadata\n" +
|
|
48
51
|
" search-api Search endpoints and return method, params, response fields, and example command\n\n" +
|
|
49
52
|
"Examples:\n" +
|
|
@@ -74,9 +77,12 @@ function printInitHelp() {
|
|
|
74
77
|
"Usage:\n" +
|
|
75
78
|
" investoday-api init\n\n" +
|
|
76
79
|
"Default flow:\n" +
|
|
77
|
-
" Prompt for InvestToday API Key, verify it, then save it to local
|
|
80
|
+
" Prompt for InvestToday API Key, verify it, then save it to local JSON config.\n" +
|
|
81
|
+
` Get your API Key from: ${API_KEY_MANAGE_URL}\n\n` +
|
|
78
82
|
"Options:\n" +
|
|
79
|
-
" --skip-verify
|
|
83
|
+
" --skip-verify Save without validating the API key first\n" +
|
|
84
|
+
" --auto-update Enable background auto update without prompting\n" +
|
|
85
|
+
" --no-auto-update Disable background auto update without prompting\n\n" +
|
|
80
86
|
`Local config: ${getCredentialsPath()}\n`
|
|
81
87
|
);
|
|
82
88
|
}
|
|
@@ -101,6 +107,7 @@ async function resolveInitApiKey(args) {
|
|
|
101
107
|
exitWithError("ERROR: Interactive input is unavailable. Run 'investoday-api init' in an interactive terminal.");
|
|
102
108
|
}
|
|
103
109
|
|
|
110
|
+
process.stderr.write(`请访问 ${API_KEY_MANAGE_URL} 获取 API Key。\n`);
|
|
104
111
|
const apiKey = await askInput("InvestToday API Key: ");
|
|
105
112
|
if (!String(apiKey || "").trim()) {
|
|
106
113
|
exitWithError("ERROR: API key cannot be empty.");
|
|
@@ -122,18 +129,86 @@ async function verifyApiKey(apiKey) {
|
|
|
122
129
|
if (apiKey && message.includes(apiKey)) {
|
|
123
130
|
message = message.replaceAll(apiKey, "***");
|
|
124
131
|
}
|
|
125
|
-
|
|
132
|
+
const errorName = String(error.name || "");
|
|
133
|
+
const errorCode = String(error.code || error.cause && error.cause.code || "");
|
|
134
|
+
const errorType = errorName === "TimeoutError" || errorName === "AbortError" || errorCode.includes("TIMEOUT")
|
|
135
|
+
? "network_timeout"
|
|
136
|
+
: "network_unreachable";
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
errorType,
|
|
140
|
+
serverConnected: false,
|
|
141
|
+
message: errorType === "network_timeout"
|
|
142
|
+
? "连接今日投资数据服务超时,请检查网络或代理后重试。"
|
|
143
|
+
: `无法连接今日投资数据服务,请检查网络、DNS、TLS 或代理后重试。${message ? ` ${message}` : ""}`.slice(0, 500),
|
|
144
|
+
};
|
|
126
145
|
}
|
|
127
146
|
|
|
128
147
|
if (!response.ok) {
|
|
129
148
|
const body = await response.text().catch(() => "");
|
|
130
|
-
|
|
149
|
+
let serverMessage = body.slice(0, 500);
|
|
150
|
+
try {
|
|
151
|
+
const payload = JSON.parse(body);
|
|
152
|
+
serverMessage = payload.message || payload.error || serverMessage;
|
|
153
|
+
} catch {
|
|
154
|
+
// keep plain text body
|
|
155
|
+
}
|
|
156
|
+
if (apiKey && serverMessage.includes(apiKey)) {
|
|
157
|
+
serverMessage = serverMessage.replaceAll(apiKey, "***");
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
errorType: response.status >= 500 ? "server_error" : "invalid_api_key",
|
|
162
|
+
serverConnected: true,
|
|
163
|
+
message: serverMessage || `HTTP ${response.status}`,
|
|
164
|
+
};
|
|
131
165
|
}
|
|
132
166
|
|
|
133
167
|
const result = await response.json().catch(() => null);
|
|
168
|
+
if (!result) {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
errorType: "unexpected_response",
|
|
172
|
+
serverConnected: true,
|
|
173
|
+
message: "服务返回异常:响应不是合法 JSON。",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
134
176
|
if (!result || result.code !== 0) {
|
|
135
|
-
|
|
177
|
+
let message = result && result.message ? result.message : "API Key 无效或无接口权限。";
|
|
178
|
+
if (apiKey && message.includes(apiKey)) {
|
|
179
|
+
message = message.replaceAll(apiKey, "***");
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
errorType: "invalid_api_key",
|
|
184
|
+
serverConnected: true,
|
|
185
|
+
message,
|
|
186
|
+
};
|
|
136
187
|
}
|
|
188
|
+
return { ok: true, errorType: null, serverConnected: true, message: result.message || "success" };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function resolveAutoUpdateChoice(args) {
|
|
192
|
+
const enableArg = hasArg(args, "--auto-update");
|
|
193
|
+
const disableArg = hasArg(args, "--no-auto-update");
|
|
194
|
+
if (enableArg && disableArg) {
|
|
195
|
+
exitWithError("ERROR: --auto-update and --no-auto-update cannot be used together.");
|
|
196
|
+
}
|
|
197
|
+
if (enableArg) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
if (disableArg) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const answer = await askInput(
|
|
205
|
+
"\n是否启用自动更新?\n" +
|
|
206
|
+
"启用后将后台定时更新 investoday-api 及 skill,以保障您能及时获取到我们最新的 API。\n" +
|
|
207
|
+
"默认:是\n" +
|
|
208
|
+
"[Y/n]: "
|
|
209
|
+
);
|
|
210
|
+
const normalized = String(answer || "").trim().toLowerCase();
|
|
211
|
+
return normalized !== "n" && normalized !== "no";
|
|
137
212
|
}
|
|
138
213
|
|
|
139
214
|
async function runInitCommand(args) {
|
|
@@ -142,7 +217,7 @@ async function runInitCommand(args) {
|
|
|
142
217
|
return;
|
|
143
218
|
}
|
|
144
219
|
|
|
145
|
-
const allowed = new Set(["--skip-verify"]);
|
|
220
|
+
const allowed = new Set(["--skip-verify", "--auto-update", "--no-auto-update"]);
|
|
146
221
|
for (const arg of args) {
|
|
147
222
|
if (!allowed.has(arg)) {
|
|
148
223
|
exitWithError(`ERROR: Unknown init option '${arg}'`);
|
|
@@ -152,12 +227,12 @@ async function runInitCommand(args) {
|
|
|
152
227
|
const apiKey = await resolveInitApiKey(args);
|
|
153
228
|
if (!hasArg(args, "--skip-verify")) {
|
|
154
229
|
process.stderr.write("Verifying InvestToday API key...\n");
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
} catch (error) {
|
|
230
|
+
const verification = await verifyApiKey(apiKey);
|
|
231
|
+
if (!verification.ok) {
|
|
158
232
|
exitWithError(
|
|
159
233
|
`InvestToday API Key verification failed.\n` +
|
|
160
|
-
`Error: ${
|
|
234
|
+
`Error type: ${verification.errorType}\n` +
|
|
235
|
+
`Error: ${verification.message}\n` +
|
|
161
236
|
`请登录 ${API_KEY_MANAGE_URL} 确认您的apiKey正确。`
|
|
162
237
|
);
|
|
163
238
|
}
|
|
@@ -169,10 +244,22 @@ async function runInitCommand(args) {
|
|
|
169
244
|
exitWithError(`ERROR: Failed to save credentials: ${error.message}`);
|
|
170
245
|
}
|
|
171
246
|
|
|
172
|
-
process.stdout.write(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
)
|
|
247
|
+
process.stdout.write("InvestToday API key 配置成功\n");
|
|
248
|
+
|
|
249
|
+
const autoUpdateEnabled = await resolveAutoUpdateChoice(args);
|
|
250
|
+
if (autoUpdateEnabled) {
|
|
251
|
+
const enableResult = await runUpdateCommand(["enable"], { stdout: process.stdout, stderr: process.stderr });
|
|
252
|
+
if (enableResult && enableResult.ok) {
|
|
253
|
+
const runResult = await runUpdateCommand(["run"], { stdout: process.stdout, stderr: process.stderr });
|
|
254
|
+
if (runResult && runResult.ok === false) {
|
|
255
|
+
process.stderr.write("自动更新首次执行失败,可稍后运行 `investoday-api update status` 查看详情。\n");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
await runUpdateCommand(["disable"], { stdout: process.stdout, stderr: process.stderr });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
process.stdout.write("\n初始化完成 ✅\n");
|
|
176
263
|
}
|
|
177
264
|
|
|
178
265
|
function runConfigCommand(args) {
|
|
@@ -195,7 +282,7 @@ function runConfigCommand(args) {
|
|
|
195
282
|
|
|
196
283
|
if (action === "remove") {
|
|
197
284
|
removeCredentials();
|
|
198
|
-
process.stdout.write("InvestToday local
|
|
285
|
+
process.stdout.write("InvestToday local configuration removed.\n");
|
|
199
286
|
return;
|
|
200
287
|
}
|
|
201
288
|
|
|
@@ -208,7 +295,7 @@ function runConfigCommand(args) {
|
|
|
208
295
|
localConfig: localConfigured ? "configured" : "missing",
|
|
209
296
|
activeSource,
|
|
210
297
|
configDir: getConfigDir(),
|
|
211
|
-
|
|
298
|
+
configFile: getCredentialsPath(),
|
|
212
299
|
}, null, 2) + "\n"
|
|
213
300
|
);
|
|
214
301
|
return;
|
|
@@ -755,6 +842,14 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
755
842
|
return;
|
|
756
843
|
}
|
|
757
844
|
|
|
845
|
+
if (argv[0] === "update") {
|
|
846
|
+
const result = await runUpdateCommand(argv.slice(1), { stdout: process.stdout, stderr: process.stderr });
|
|
847
|
+
if (result && result.ok === false) {
|
|
848
|
+
process.exitCode = 1;
|
|
849
|
+
}
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
758
853
|
if (argv[0] === "list") {
|
|
759
854
|
runListCommand(argv.slice(1));
|
|
760
855
|
return;
|
package/lib/config.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
const crypto = require("node:crypto");
|
|
2
1
|
const fs = require("node:fs");
|
|
3
2
|
const os = require("node:os");
|
|
4
3
|
const path = require("node:path");
|
|
5
4
|
|
|
6
5
|
const CONFIG_DIR_ENV = "INVESTODAY_API_CONFIG_DIR";
|
|
7
6
|
const API_KEY_ENV = "INVESTODAY_API_KEY";
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
7
|
+
const CONFIG_FILE = "investoday-api.config.json";
|
|
8
|
+
const LEGACY_CREDENTIALS_FILE = "credentials.enc";
|
|
9
|
+
const LEGACY_KEY_FILE = ".encryption_key";
|
|
11
10
|
|
|
12
11
|
function getConfigDir(env = process.env) {
|
|
13
12
|
if (env[CONFIG_DIR_ENV]) {
|
|
@@ -18,11 +17,15 @@ function getConfigDir(env = process.env) {
|
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
function getCredentialsPath(env = process.env) {
|
|
21
|
-
return path.join(getConfigDir(env),
|
|
20
|
+
return path.join(getConfigDir(env), CONFIG_FILE);
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
function
|
|
25
|
-
return path.join(getConfigDir(env),
|
|
23
|
+
function getLegacyCredentialsPath(env = process.env) {
|
|
24
|
+
return path.join(getConfigDir(env), LEGACY_CREDENTIALS_FILE);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getLegacyKeyPath(env = process.env) {
|
|
28
|
+
return path.join(getConfigDir(env), LEGACY_KEY_FILE);
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
function ensureConfigDir(env = process.env) {
|
|
@@ -44,69 +47,16 @@ function atomicWriteFile(filePath, contents, mode = 0o600) {
|
|
|
44
47
|
fs.renameSync(tempPath, filePath);
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
const keyPath = getEncryptionKeyPath(env);
|
|
50
|
-
if (fs.existsSync(keyPath)) {
|
|
51
|
-
const encoded = fs.readFileSync(keyPath, "utf8").trim();
|
|
52
|
-
const key = Buffer.from(encoded, "base64");
|
|
53
|
-
if (key.length !== 32) {
|
|
54
|
-
throw new Error(`Invalid encryption key at ${keyPath}`);
|
|
55
|
-
}
|
|
56
|
-
return key;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const key = crypto.randomBytes(32);
|
|
60
|
-
atomicWriteFile(keyPath, key.toString("base64"));
|
|
61
|
-
return key;
|
|
50
|
+
function saveConfig(config, env = process.env) {
|
|
51
|
+
atomicWriteFile(getCredentialsPath(env), `${JSON.stringify(config || {}, null, 2)}\n`);
|
|
62
52
|
}
|
|
63
53
|
|
|
64
|
-
function
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const encoded = fs.readFileSync(keyPath, "utf8").trim();
|
|
71
|
-
const key = Buffer.from(encoded, "base64");
|
|
72
|
-
if (key.length !== 32) {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
return key;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function encryptJson(value, key) {
|
|
79
|
-
const iv = crypto.randomBytes(12);
|
|
80
|
-
const cipher = crypto.createCipheriv(CIPHER_ALGORITHM, key, iv);
|
|
81
|
-
const plaintext = Buffer.from(JSON.stringify(value), "utf8");
|
|
82
|
-
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
83
|
-
const tag = cipher.getAuthTag();
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
version: 1,
|
|
87
|
-
algorithm: CIPHER_ALGORITHM,
|
|
88
|
-
iv: iv.toString("base64"),
|
|
89
|
-
tag: tag.toString("base64"),
|
|
90
|
-
data: encrypted.toString("base64"),
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function decryptJson(payload, key) {
|
|
95
|
-
if (!payload || payload.algorithm !== CIPHER_ALGORITHM) {
|
|
96
|
-
throw new Error("Unsupported credentials format");
|
|
54
|
+
function removeLegacyCredentials(env = process.env) {
|
|
55
|
+
for (const filePath of [getLegacyCredentialsPath(env), getLegacyKeyPath(env)]) {
|
|
56
|
+
if (fs.existsSync(filePath)) {
|
|
57
|
+
fs.rmSync(filePath, { force: true });
|
|
58
|
+
}
|
|
97
59
|
}
|
|
98
|
-
|
|
99
|
-
const decipher = crypto.createDecipheriv(
|
|
100
|
-
CIPHER_ALGORITHM,
|
|
101
|
-
key,
|
|
102
|
-
Buffer.from(payload.iv, "base64")
|
|
103
|
-
);
|
|
104
|
-
decipher.setAuthTag(Buffer.from(payload.tag, "base64"));
|
|
105
|
-
const decrypted = Buffer.concat([
|
|
106
|
-
decipher.update(Buffer.from(payload.data, "base64")),
|
|
107
|
-
decipher.final(),
|
|
108
|
-
]);
|
|
109
|
-
return JSON.parse(decrypted.toString("utf8"));
|
|
110
60
|
}
|
|
111
61
|
|
|
112
62
|
function saveCredentials(apiKey, env = process.env) {
|
|
@@ -115,35 +65,48 @@ function saveCredentials(apiKey, env = process.env) {
|
|
|
115
65
|
throw new Error("API key cannot be empty");
|
|
116
66
|
}
|
|
117
67
|
|
|
118
|
-
const
|
|
119
|
-
const payload =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
68
|
+
const existing = readConfig(env);
|
|
69
|
+
const payload = {
|
|
70
|
+
...(existing || {}),
|
|
71
|
+
[API_KEY_ENV]: trimmedKey,
|
|
72
|
+
};
|
|
73
|
+
saveConfig(payload, env);
|
|
74
|
+
removeLegacyCredentials(env);
|
|
124
75
|
}
|
|
125
76
|
|
|
126
|
-
function
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
if (!key || !fs.existsSync(credentialsPath)) {
|
|
77
|
+
function readConfig(env = process.env) {
|
|
78
|
+
const configPath = getCredentialsPath(env);
|
|
79
|
+
if (!fs.existsSync(configPath)) {
|
|
130
80
|
return null;
|
|
131
81
|
}
|
|
132
82
|
|
|
133
83
|
try {
|
|
134
|
-
|
|
135
|
-
return decryptJson(payload, key);
|
|
84
|
+
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
136
85
|
} catch {
|
|
137
86
|
return null;
|
|
138
87
|
}
|
|
139
88
|
}
|
|
140
89
|
|
|
90
|
+
function readCredentials(env = process.env) {
|
|
91
|
+
const config = readConfig(env);
|
|
92
|
+
if (!config) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const apiKey = String(config[API_KEY_ENV] || "").trim();
|
|
97
|
+
if (!apiKey) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { apiKey };
|
|
102
|
+
}
|
|
103
|
+
|
|
141
104
|
function removeCredentials(env = process.env) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
105
|
+
const configPath = getCredentialsPath(env);
|
|
106
|
+
if (fs.existsSync(configPath)) {
|
|
107
|
+
fs.rmSync(configPath, { force: true });
|
|
146
108
|
}
|
|
109
|
+
removeLegacyCredentials(env);
|
|
147
110
|
}
|
|
148
111
|
|
|
149
112
|
function resolveApiKey(env = process.env) {
|
|
@@ -162,14 +125,20 @@ function resolveApiKey(env = process.env) {
|
|
|
162
125
|
|
|
163
126
|
module.exports = {
|
|
164
127
|
API_KEY_ENV,
|
|
128
|
+
CONFIG_FILE,
|
|
165
129
|
CONFIG_DIR_ENV,
|
|
166
|
-
CREDENTIALS_FILE,
|
|
167
|
-
|
|
130
|
+
CREDENTIALS_FILE: CONFIG_FILE,
|
|
131
|
+
LEGACY_CREDENTIALS_FILE,
|
|
132
|
+
LEGACY_KEY_FILE,
|
|
168
133
|
getConfigDir,
|
|
169
134
|
getCredentialsPath,
|
|
170
|
-
|
|
135
|
+
getLegacyCredentialsPath,
|
|
136
|
+
getLegacyKeyPath,
|
|
137
|
+
removeLegacyCredentials,
|
|
138
|
+
readConfig,
|
|
171
139
|
readCredentials,
|
|
172
140
|
removeCredentials,
|
|
173
141
|
resolveApiKey,
|
|
142
|
+
saveConfig,
|
|
174
143
|
saveCredentials,
|
|
175
144
|
};
|