@onebullex-group/cli 1.1.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.
- package/CHANGELOG.md +61 -0
- package/README.md +243 -0
- package/dist/chunk-AKGCHJPQ.js +88 -0
- package/dist/chunk-CVOY2SOS.js +139 -0
- package/dist/chunk-FTINNKNR.js +492 -0
- package/dist/chunk-J3TAAD3Q.js +58 -0
- package/dist/chunk-JRUFIHU7.js +45 -0
- package/dist/chunk-LVNSFQXS.js +31 -0
- package/dist/chunk-OOLZIYXS.js +41 -0
- package/dist/chunk-ZYO5P4DH.js +19 -0
- package/dist/core/config.d.ts +44 -0
- package/dist/core/config.js +19 -0
- package/dist/core/errors.d.ts +23 -0
- package/dist/core/errors.js +13 -0
- package/dist/core/http-client.d.ts +29 -0
- package/dist/core/http-client.js +13 -0
- package/dist/core/redact.d.ts +15 -0
- package/dist/core/redact.js +10 -0
- package/dist/core/signature.d.ts +39 -0
- package/dist/core/signature.js +11 -0
- package/dist/core/types.d.ts +46 -0
- package/dist/core/types.js +7 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +1627 -0
- package/dist/lint-api.d.ts +38 -0
- package/dist/lint-api.js +257 -0
- package/dist/output-UIZGG7EU.js +13 -0
- package/dist/registry.d.ts +5 -0
- package/dist/registry.js +7 -0
- package/package.json +54 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@onebullex-group/cli` (formerly briefly published as `@onemore-group/cli`).
|
|
4
|
+
|
|
5
|
+
## 1.1.2 — 2026-05-16
|
|
6
|
+
|
|
7
|
+
Rename only — no functional changes from 1.1.1.
|
|
8
|
+
|
|
9
|
+
- Package renamed from `@onebullex/cli` → `@onebullex-group/cli`. The
|
|
10
|
+
CLI binary remains `onebullex` (unchanged), the README still
|
|
11
|
+
describes "OneBullEx Open API V2" — only the npm package name moved.
|
|
12
|
+
- A short-lived `@onemore-group/cli@1.1.2` was published earlier the
|
|
13
|
+
same day and has been unpublished. If you installed it, switch to
|
|
14
|
+
`@onebullex-group/cli@1.1.2` (identical contents).
|
|
15
|
+
- Existing 1.1.0 / 1.1.1 were never published.
|
|
16
|
+
|
|
17
|
+
## Unreleased
|
|
18
|
+
|
|
19
|
+
- ci(cli): nightly `cli-e2e-nightly` job in `.gitlab-ci.yml`, runs the full
|
|
20
|
+
`tests/e2e/` suite when triggered by a Pipeline Schedule. Reads
|
|
21
|
+
`ONEBULLEX_E2E_API_KEY` / `_SECRET` from GitLab CI variables. Marked
|
|
22
|
+
`allow_failure: true` with one retry so testnet flakes don't block the
|
|
23
|
+
schedule.
|
|
24
|
+
- test(cli): testnet write e2e suite covering all 20 V2 write endpoints,
|
|
25
|
+
organized into 4 business-flow blocks (configuration / limit orders /
|
|
26
|
+
plan + order-entrust / position + TP-SL). Gated on `ONEBULLEX_E2E=1`
|
|
27
|
+
with separate `ONEBULLEX_E2E_API_KEY` / `_SECRET` env vars. Default-
|
|
28
|
+
skipped. Each block has best-effort cleanup so consecutive runs leave
|
|
29
|
+
the testnet account state-clean.
|
|
30
|
+
|
|
31
|
+
## 1.1.0 — 2026-05-15
|
|
32
|
+
|
|
33
|
+
Adds Phase 2: 20 write operations covering order placement / cancellation,
|
|
34
|
+
plan + TP/SL entrust management, and position margin / leverage / type changes.
|
|
35
|
+
|
|
36
|
+
- 5 order write commands (create, create-batch, cancel, cancel-batch, cancel-all)
|
|
37
|
+
- 7 entrust write commands (create-plan, create-profit, cancel-plan,
|
|
38
|
+
cancel-profit, update-profit, cancel-all-plan, cancel-all-profit)
|
|
39
|
+
- 2 order-entrust write commands (cancel, cancel-all)
|
|
40
|
+
- 6 position write commands (adjust-leverage, margin, auto-margin, close-all,
|
|
41
|
+
merge, change-type)
|
|
42
|
+
- New `executeWrite` helper guards every write with a default dry-run; `--confirm`
|
|
43
|
+
is required for the actual POST. Dry-run preview goes to stdout, the
|
|
44
|
+
`--confirm` hint goes to stderr.
|
|
45
|
+
- Drift-detection lint now covers all 60 V2 endpoints; the `PHASE1_DEFERRED`
|
|
46
|
+
whitelist is empty.
|
|
47
|
+
- Adds 21 new write command integration tests verifying both the dry-run path
|
|
48
|
+
(no fetch, structured preview) and the confirm path (camelCase body shape).
|
|
49
|
+
|
|
50
|
+
## 1.0.0 — 2026-05-15
|
|
51
|
+
|
|
52
|
+
Initial release. Covers OneBullEx Open API V2:
|
|
53
|
+
|
|
54
|
+
- 22 public endpoints (server time, symbol/ticker/depth/kline/funding/...)
|
|
55
|
+
- 18 private read-only endpoints (balance/order/entrust/position/...)
|
|
56
|
+
- HMAC-SHA256 auth aligned byte-for-byte with `SignatureUtilsV2.java`
|
|
57
|
+
- 6 config commands with multi-profile support, `--stdin` enforcement on
|
|
58
|
+
api-key/secret, and 0600 file mode
|
|
59
|
+
- Drift-detection lint comparing CLI registry against V2 markdown docs
|
|
60
|
+
(endpoint coverage + parameter coverage + write-safety)
|
|
61
|
+
- 155 unit / integration tests + 5 testnet e2e smoke tests
|
package/README.md
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# @onebullex-group/cli
|
|
2
|
+
|
|
3
|
+
OneBullEx Open API V2 的命令行工具。
|
|
4
|
+
|
|
5
|
+
> 当前版本(1.1.0)覆盖 V2 文档的全部 60 个端点:22 个公开 + 18 个私有只读 + 20 个写。所有写命令默认 dry-run,需 `--confirm` 才真发请求。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 安装
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i -g @onebullex-group/cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
或本地构建:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
cd cli
|
|
19
|
+
pnpm install
|
|
20
|
+
pnpm build
|
|
21
|
+
pnpm link --global
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
校验:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
onebullex --version
|
|
28
|
+
onebullex public time --base-url https://futures-openapi.1bullex.com
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# 1. 初始化一个 profile(向导式)
|
|
35
|
+
onebullex config init
|
|
36
|
+
|
|
37
|
+
# 2. 看下当前 profile 配置(敏感字段已脱敏)
|
|
38
|
+
onebullex config show
|
|
39
|
+
|
|
40
|
+
# 3. 跑公开接口验证联通性
|
|
41
|
+
onebullex public time
|
|
42
|
+
onebullex symbol list
|
|
43
|
+
|
|
44
|
+
# 4. 跑私有只读接口(需要在 config init 时填了 api-key/secret)
|
|
45
|
+
onebullex balance list
|
|
46
|
+
onebullex position list
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 命令分组
|
|
50
|
+
|
|
51
|
+
完整命令列表跟 V2 文档逐项对齐,可用 `onebullex <group> --help` 查询。
|
|
52
|
+
|
|
53
|
+
### 公开(22)
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
public time GET /v2/public/time
|
|
57
|
+
symbol list / get / coins / all
|
|
58
|
+
ticker get / list / agg / agg-list
|
|
59
|
+
depth get
|
|
60
|
+
deal list
|
|
61
|
+
kline list
|
|
62
|
+
mark-price list / get
|
|
63
|
+
index-price list / get* (* index-price get 需要认证)
|
|
64
|
+
funding rate / history
|
|
65
|
+
risk-balance list
|
|
66
|
+
open-interest get
|
|
67
|
+
leverage brackets-list / brackets-get
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 私有只读(18)
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
listen-key get
|
|
74
|
+
balance list / get / bills
|
|
75
|
+
order list-unfinished / list-unfinished-multi / list / list-by-ids /
|
|
76
|
+
list-history / get / trades
|
|
77
|
+
entrust plan-list / plan-history / plan-get / profit-list / profit-get
|
|
78
|
+
order-entrust list
|
|
79
|
+
position list
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 写操作(20)
|
|
83
|
+
|
|
84
|
+
所有写命令默认 dry-run:仅打印预览不发请求。**加 `--confirm` 才会真正下单/撤单/改仓。**
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
order create / create-batch / cancel / cancel-batch / cancel-all
|
|
88
|
+
entrust create-plan / create-profit /
|
|
89
|
+
cancel-plan / cancel-profit / update-profit /
|
|
90
|
+
cancel-all-plan / cancel-all-profit
|
|
91
|
+
order-entrust cancel / cancel-all
|
|
92
|
+
position adjust-leverage / margin / auto-margin /
|
|
93
|
+
close-all / merge / change-type
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
例:
|
|
97
|
+
```bash
|
|
98
|
+
# dry-run(默认,退出码 4,preview 在 stdout)
|
|
99
|
+
onebullex order create --symbol btc_usdt --order-type LIMIT \
|
|
100
|
+
--order-side BUY --position-side LONG --orig-qty 0.001 --price 50000
|
|
101
|
+
|
|
102
|
+
# 真正下单
|
|
103
|
+
onebullex order create --symbol btc_usdt --order-type LIMIT \
|
|
104
|
+
--order-side BUY --position-side LONG --orig-qty 0.001 --price 50000 \
|
|
105
|
+
--confirm
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
CLI flag 用 kebab-case,会映射到 API 的 camelCase 字段(如 `--order-side` → `orderSide`)。批量类(`create-batch` / `cancel-batch`)的 `--list` / `--order-ids` 接受 JSON 字符串数组。
|
|
109
|
+
|
|
110
|
+
### 配置管理(6)
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
config init 交互式向导
|
|
114
|
+
config set <key> 仅 base-url / identify / env 可用 --value
|
|
115
|
+
api-key / secret 必须用 --stdin
|
|
116
|
+
config show [--reveal] 默认脱敏,加 --reveal 后明文(带 stderr 警告)
|
|
117
|
+
config use <profile> 切换默认 profile
|
|
118
|
+
config list-profiles 表格列出全部 profile
|
|
119
|
+
config remove <p> 删除 profile(防止删掉最后一个)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## 配置文件
|
|
123
|
+
|
|
124
|
+
默认路径 `~/.onebullex/config.json`,权限 0600(非 Windows 上自动校正)。
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"version": 1,
|
|
129
|
+
"default-profile": "test",
|
|
130
|
+
"profiles": {
|
|
131
|
+
"test": {
|
|
132
|
+
"base-url": "https://futures-openapi.1bullex.com",
|
|
133
|
+
"api-key": "AK...",
|
|
134
|
+
"secret": "...",
|
|
135
|
+
"identify": null,
|
|
136
|
+
"env": "test"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
可通过 `ONEBULLEX_CONFIG_PATH` 环境变量覆盖路径(脚本/CI 友好)。
|
|
143
|
+
|
|
144
|
+
## 优先级与全局 flag
|
|
145
|
+
|
|
146
|
+
每次调用按以下顺序解析:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
CLI flag > 环境变量 > 配置文件 profile > 报错(要求 config init)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
| 全局 flag | 环境变量 | 说明 |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| `--profile <name>` | `ONEBULLEX_PROFILE` | 临时切换 profile |
|
|
155
|
+
| `--api-key <key>` | `ONEBULLEX_API_KEY` | 覆盖 api key |
|
|
156
|
+
| `--secret <s>` | `ONEBULLEX_SECRET` | 覆盖 secret(不建议 CLI 上明文) |
|
|
157
|
+
| `--base-url <url>` | `ONEBULLEX_BASE_URL` | 覆盖 base URL |
|
|
158
|
+
| `--output <fmt>` | — | `table` / `json`,TTY 默认 table,否则 json |
|
|
159
|
+
| `--debug` | `ONEBULLEX_DEBUG=1` | 把请求/响应(脱敏后)写到 stderr |
|
|
160
|
+
| `--timeout <ms>` | — | HTTP 超时,默认 10s |
|
|
161
|
+
| `--confirm` | — | Phase 2 写命令使用,未传时走 dry-run |
|
|
162
|
+
|
|
163
|
+
## 安全
|
|
164
|
+
|
|
165
|
+
- secret 与 api-key **明文落盘**在 config.json,权限收紧到 0600
|
|
166
|
+
- `config set api-key` / `config set secret` 拒绝 `--value`,强制 `--stdin`,防止 shell history 泄漏
|
|
167
|
+
- `config show` 默认脱敏(`AK****1234` / `****`),仅 `--reveal` 时明文并带 stderr 警告
|
|
168
|
+
- `--debug` 输出会通过 redact 过滤掉 `X-Signature` / `X-API-KEY` / `Authorization` / `secret` 字段
|
|
169
|
+
- 每次调用都会校验 `~/.onebullex/config.json` 的 mode(非 win32),不是 0600 时打 warn 并自动修复
|
|
170
|
+
|
|
171
|
+
## 输出格式
|
|
172
|
+
|
|
173
|
+
- TTY 下默认表格(`cli-table3`),列名取并集,时间戳列自动渲染成 `YYYY-MM-DD HH:mm:ss`
|
|
174
|
+
- 非 TTY(管道、重定向、CI)默认 JSON,`jq` 友好
|
|
175
|
+
- 任意时候用 `--output json` 强制 JSON,`--output table` 强制表格
|
|
176
|
+
|
|
177
|
+
## 防漂移 lint
|
|
178
|
+
|
|
179
|
+
`pnpm check-coverage` 跑三层校验:
|
|
180
|
+
|
|
181
|
+
1. **端点级**:把 `docs/onebullex-openapi-cn-v2.md` 里的所有 `#### GET/POST /v2/*` 标题与 `cli/src/registry.ts` 对账,缺/多就 fail
|
|
182
|
+
2. **参数级**:用 ts-morph 解析每个命令文件,提取 commander 的 `command()` / `requiredOption()` / `option()`,与文档参数表的 `必填` 列对账
|
|
183
|
+
3. **写守门**:方法是 GET 但 `isWrite=true`,或者 method 是 POST 但 `isWrite=false`(`/v2/order/list-by-ids` 是已知例外)
|
|
184
|
+
|
|
185
|
+
CI 上当文档新增一个端点时,coverage 检查会立刻 fail,强制开发者补一个新命令。
|
|
186
|
+
|
|
187
|
+
## 开发
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
cd cli
|
|
191
|
+
pnpm install
|
|
192
|
+
pnpm dev <command> # 直接跑 src/index.ts,无需先 build
|
|
193
|
+
pnpm test # 跑全部单测/集成测
|
|
194
|
+
pnpm typecheck # tsc --noEmit
|
|
195
|
+
pnpm lint # biome
|
|
196
|
+
pnpm check-coverage # 防漂移 lint
|
|
197
|
+
pnpm build # 产出 dist/index.js
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### 跑 testnet e2e
|
|
201
|
+
|
|
202
|
+
只读 e2e(默认跳过)只验证公开接口连通性:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
ONEBULLEX_E2E=1 pnpm vitest run tests/e2e/public.e2e.test.ts
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
写 e2e(默认跳过)会真发请求到 testnet:下/撤远价限价单、改杠杆、改持仓模式、用 MARKET 单开仓 → 加保证金 → 创建止盈止损 → close-all。需要测试网账户和 ≥ 50 USDT 余额:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
ONEBULLEX_E2E=1 \
|
|
212
|
+
ONEBULLEX_E2E_API_KEY=<testnet-key> \
|
|
213
|
+
ONEBULLEX_E2E_SECRET=<testnet-secret> \
|
|
214
|
+
pnpm vitest run tests/e2e/write.e2e.test.ts
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
按业务流程组织成 4 个 block,串行执行:configuration chain → limit order chain → plan + order-entrust chain → position + profit chain。每个 block 末尾有 best-effort `cancel-all` / `close-all`,跑挂时也尽量不留挂单/持仓残留。
|
|
218
|
+
|
|
219
|
+
#### Nightly CI(可选)
|
|
220
|
+
|
|
221
|
+
`cli/.gitlab-ci.yml` 里有一个 `cli-e2e-nightly` job,仅在 schedule 触发时跑(`$CI_PIPELINE_SOURCE == "schedule"`),跑完整 `tests/e2e/` 套件。开启方式:
|
|
222
|
+
|
|
223
|
+
1. 在 GitLab project **Settings → CI/CD → Variables** 添加 masked + protected 变量:
|
|
224
|
+
- `ONEBULLEX_E2E_API_KEY` = testnet API key
|
|
225
|
+
- `ONEBULLEX_E2E_SECRET` = testnet API secret
|
|
226
|
+
2. **Build → Pipeline schedules → New schedule**,cron 设成例如 `0 19 * * *`(每日 03:00 UTC+8),target branch 选 `main`。
|
|
227
|
+
|
|
228
|
+
job 标 `allow_failure: true` 且 retry 一次——testnet 偶发抖动不会阻塞调度。变量没配时只有 public e2e 跑,写 e2e 自动 skip(HAS_AUTH 门控)。
|
|
229
|
+
|
|
230
|
+
## 与 Java SDK 的关系
|
|
231
|
+
|
|
232
|
+
签名实现严格对齐 `src/main/java/com/onebullex/api/common/SignatureUtilsV2.java`:
|
|
233
|
+
|
|
234
|
+
- timestamp 秒级(10 位)
|
|
235
|
+
- nonce 是 UUID v4 去横线(32 位 hex 小写)
|
|
236
|
+
- query string 按 key 字典序排序,**不做 URL 编码**
|
|
237
|
+
- HMAC-SHA256,输出 hex 小写
|
|
238
|
+
|
|
239
|
+
Java 端通过 `SignatureVectorGenTest` 生成 `cli/tests/fixtures/signature-vectors.json`,CLI 端 `signature-parity.test.ts` 把 7 条向量逐条对照 Java 输出,保证字节级一致。
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
MIT
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
redactSecret
|
|
4
|
+
} from "./chunk-OOLZIYXS.js";
|
|
5
|
+
|
|
6
|
+
// src/core/config.ts
|
|
7
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
8
|
+
import { chmodSync, statSync } from "fs";
|
|
9
|
+
import { homedir, platform } from "os";
|
|
10
|
+
import { dirname, join } from "path";
|
|
11
|
+
var DEFAULT_CONFIG_PATH = join(homedir(), ".onebullex", "config.json");
|
|
12
|
+
function readConfigFile(path) {
|
|
13
|
+
const raw = readFileSync(path, "utf8");
|
|
14
|
+
const parsed = JSON.parse(raw);
|
|
15
|
+
if (typeof parsed !== "object" || parsed === null || parsed["version"] !== 1) {
|
|
16
|
+
throw new Error(`\u914D\u7F6E\u6587\u4EF6\u683C\u5F0F\u65E0\u6548\uFF1Aversion \u5FC5\u987B\u4E3A 1\uFF08\u8DEF\u5F84\uFF1A${path}\uFF09`);
|
|
17
|
+
}
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
function writeConfigFile(path, cfg) {
|
|
21
|
+
const dir = dirname(path);
|
|
22
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
23
|
+
const tmp = `${path}.tmp.${process.pid}`;
|
|
24
|
+
const content = JSON.stringify(cfg, null, 2);
|
|
25
|
+
writeFileSync(tmp, content, { encoding: "utf8", mode: 384 });
|
|
26
|
+
renameSync(tmp, path);
|
|
27
|
+
}
|
|
28
|
+
function checkConfigPermissions(path, warn) {
|
|
29
|
+
if (platform() === "win32") return;
|
|
30
|
+
try {
|
|
31
|
+
const st = statSync(path);
|
|
32
|
+
const mode = st.mode & 511;
|
|
33
|
+
if (mode !== 384) {
|
|
34
|
+
warn(
|
|
35
|
+
`\u914D\u7F6E\u6587\u4EF6\u6743\u9650\u4E0D\u5B89\u5168\uFF08\u5F53\u524D\uFF1A${mode.toString(8)}\uFF0C\u671F\u671B\uFF1A600\uFF09\uFF1A${path}\uFF0C\u6B63\u5728\u4FEE\u590D\u2026`
|
|
36
|
+
);
|
|
37
|
+
chmodSync(path, 384);
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function resolveConfig(input) {
|
|
43
|
+
const { configPath, flags, env } = input;
|
|
44
|
+
let file = null;
|
|
45
|
+
try {
|
|
46
|
+
file = readConfigFile(configPath);
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
const profileName = flags.profile ?? env["ONEBULLEX_PROFILE"] ?? file?.["default-profile"] ?? "default";
|
|
50
|
+
const profile = file?.profiles?.[profileName];
|
|
51
|
+
const baseUrl = flags["base-url"] ?? env["ONEBULLEX_BASE_URL"] ?? profile?.["base-url"];
|
|
52
|
+
if (!baseUrl) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`\u672A\u627E\u5230 baseUrl \u914D\u7F6E\u3002\u8BF7\u5148\u8FD0\u884C "onebullex config init" \u521D\u59CB\u5316\u914D\u7F6E\uFF0C\u6216\u901A\u8FC7 --base-url \u53C2\u6570 / ONEBULLEX_BASE_URL \u73AF\u5883\u53D8\u91CF\u6307\u5B9A\u3002`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const apiKey = flags["api-key"] ?? env["ONEBULLEX_API_KEY"] ?? profile?.["api-key"];
|
|
58
|
+
const secret = flags.secret ?? env["ONEBULLEX_SECRET"] ?? profile?.secret;
|
|
59
|
+
const identify = profile?.identify ?? null;
|
|
60
|
+
const debug = flags.debug ?? env["ONEBULLEX_DEBUG"] === "1";
|
|
61
|
+
const timeoutMs = flags.timeout ?? 1e4;
|
|
62
|
+
return {
|
|
63
|
+
baseUrl,
|
|
64
|
+
apiKey,
|
|
65
|
+
secret,
|
|
66
|
+
identify,
|
|
67
|
+
profileName,
|
|
68
|
+
env: profile?.env,
|
|
69
|
+
debug,
|
|
70
|
+
timeoutMs
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function redactProfile(p) {
|
|
74
|
+
return {
|
|
75
|
+
...p,
|
|
76
|
+
...p["api-key"] !== void 0 ? { "api-key": redactSecret(p["api-key"]) } : {},
|
|
77
|
+
...p.secret !== void 0 ? { secret: "****" } : {}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export {
|
|
82
|
+
DEFAULT_CONFIG_PATH,
|
|
83
|
+
readConfigFile,
|
|
84
|
+
writeConfigFile,
|
|
85
|
+
checkConfigPermissions,
|
|
86
|
+
resolveConfig,
|
|
87
|
+
redactProfile
|
|
88
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
OneBullExError
|
|
4
|
+
} from "./chunk-JRUFIHU7.js";
|
|
5
|
+
import {
|
|
6
|
+
redactObject
|
|
7
|
+
} from "./chunk-OOLZIYXS.js";
|
|
8
|
+
import {
|
|
9
|
+
generateNonce,
|
|
10
|
+
sign
|
|
11
|
+
} from "./chunk-LVNSFQXS.js";
|
|
12
|
+
|
|
13
|
+
// src/core/http-client.ts
|
|
14
|
+
function flattenForSign(body) {
|
|
15
|
+
if (!body) return {};
|
|
16
|
+
const out = {};
|
|
17
|
+
for (const [k, v] of Object.entries(body)) {
|
|
18
|
+
if (v == null) continue;
|
|
19
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
20
|
+
out[k] = v;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
throw new Error(
|
|
24
|
+
`flattenForSign: unsupported value type for key "${k}" (${typeof v}). Nested objects/arrays must be pre-serialized by the caller (e.g. JSON.stringify).`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
var HttpClient = class {
|
|
30
|
+
constructor(cfg, fetchFn) {
|
|
31
|
+
this.cfg = cfg;
|
|
32
|
+
this.fetch = fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
33
|
+
}
|
|
34
|
+
cfg;
|
|
35
|
+
fetch;
|
|
36
|
+
async getPublic(path, query) {
|
|
37
|
+
return this.withRetry(() => this.send("GET", path, query, void 0, false));
|
|
38
|
+
}
|
|
39
|
+
async getPrivate(path, query) {
|
|
40
|
+
return this.withRetry(() => this.send("GET", path, query, void 0, true));
|
|
41
|
+
}
|
|
42
|
+
async postPrivate(path, body) {
|
|
43
|
+
return this.send("POST", path, void 0, body, true);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* GET 请求重试包装:网络错误最多重试 2 次(共 3 次尝试),延迟 300ms / 900ms。
|
|
47
|
+
* OneBullExError(业务错误)不重试。
|
|
48
|
+
*/
|
|
49
|
+
async withRetry(fn) {
|
|
50
|
+
const delays = [300, 900];
|
|
51
|
+
let lastErr;
|
|
52
|
+
for (let i = 0; i <= delays.length; i++) {
|
|
53
|
+
try {
|
|
54
|
+
return await fn();
|
|
55
|
+
} catch (e) {
|
|
56
|
+
if (e instanceof OneBullExError) throw e;
|
|
57
|
+
lastErr = e;
|
|
58
|
+
if (i === delays.length) break;
|
|
59
|
+
await new Promise((r) => setTimeout(r, delays[i]));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
throw lastErr;
|
|
63
|
+
}
|
|
64
|
+
async send(method, path, query, body, requireAuth) {
|
|
65
|
+
const headers = {
|
|
66
|
+
"Content-Type": "application/json",
|
|
67
|
+
Accept: "application/json",
|
|
68
|
+
"User-Agent": "@onebullex-group/cli"
|
|
69
|
+
};
|
|
70
|
+
let url = `${this.cfg.baseUrl}${path}`;
|
|
71
|
+
const params = method === "GET" ? { ...query ?? {} } : flattenForSign(body);
|
|
72
|
+
if (requireAuth) {
|
|
73
|
+
if (!this.cfg.apiKey || !this.cfg.secret) {
|
|
74
|
+
throw new OneBullExError(
|
|
75
|
+
"API key and secret are required for authenticated requests",
|
|
76
|
+
"NO_AUTH"
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const nonce = generateNonce();
|
|
80
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
81
|
+
const signature = sign({ params, nonce, timestamp, secret: this.cfg.secret });
|
|
82
|
+
headers["X-API-Key"] = this.cfg.apiKey;
|
|
83
|
+
headers["X-Nonce"] = nonce;
|
|
84
|
+
headers["X-Timestamp"] = String(timestamp);
|
|
85
|
+
headers["X-Signature"] = signature;
|
|
86
|
+
if (this.cfg.identify) {
|
|
87
|
+
headers["X-Identify"] = this.cfg.identify;
|
|
88
|
+
headers["Identify"] = this.cfg.identify;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (method === "GET" && query && Object.keys(query).length > 0) {
|
|
92
|
+
const qs = new URLSearchParams();
|
|
93
|
+
for (const [k, v] of Object.entries(query)) qs.set(k, String(v));
|
|
94
|
+
url += `?${qs.toString()}`;
|
|
95
|
+
}
|
|
96
|
+
if (this.cfg.debug) {
|
|
97
|
+
console.error(
|
|
98
|
+
"[debug request]",
|
|
99
|
+
JSON.stringify(redactObject({ method, url, headers, body }))
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const ctrl = new AbortController();
|
|
103
|
+
const t = setTimeout(() => ctrl.abort(), this.cfg.timeoutMs);
|
|
104
|
+
let res;
|
|
105
|
+
try {
|
|
106
|
+
res = await this.fetch(url, {
|
|
107
|
+
method,
|
|
108
|
+
headers,
|
|
109
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
110
|
+
signal: ctrl.signal
|
|
111
|
+
});
|
|
112
|
+
} finally {
|
|
113
|
+
clearTimeout(t);
|
|
114
|
+
}
|
|
115
|
+
if (!res.ok) {
|
|
116
|
+
throw new OneBullExError(
|
|
117
|
+
`HTTP ${res.status} ${res.statusText}`,
|
|
118
|
+
res.status,
|
|
119
|
+
res.status,
|
|
120
|
+
path
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const json = await res.json();
|
|
124
|
+
if (this.cfg.debug) {
|
|
125
|
+
console.error("[debug response]", JSON.stringify(redactObject(json)));
|
|
126
|
+
}
|
|
127
|
+
const returnCode = json.code ?? json.returnCode;
|
|
128
|
+
const msgInfo = json.msg ?? json.msgInfo ?? "";
|
|
129
|
+
if (returnCode !== 0 && returnCode !== "0") {
|
|
130
|
+
throw new OneBullExError(msgInfo, returnCode ?? "UNKNOWN", res.status, path);
|
|
131
|
+
}
|
|
132
|
+
return json.data;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export {
|
|
137
|
+
flattenForSign,
|
|
138
|
+
HttpClient
|
|
139
|
+
};
|