@okx_ai/okx-trade-cli 1.3.1-beta.2 → 1.3.1-beta.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okx_ai/okx-trade-cli",
3
- "version": "1.3.1-beta.2",
3
+ "version": "1.3.1-beta.3",
4
4
  "description": "OKX CLI - Command line tool for OKX exchange",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,10 +1,15 @@
1
1
  #!/usr/bin/env node
2
- // Shared postinstall notice script — do not edit the copies in packages/*/scripts/
2
+ // Shared postinstall script — do not edit the copies in packages/*/scripts/
3
3
  // This file is the single source of truth; copies are generated during build.
4
4
 
5
- import { readFileSync } from 'node:fs';
5
+ import { readFileSync, createWriteStream, mkdirSync, chmodSync, existsSync, unlinkSync, renameSync } from 'node:fs';
6
+ import { createHash } from 'node:crypto';
6
7
  import { fileURLToPath } from 'node:url';
7
8
  import { dirname, join } from 'node:path';
9
+ import { homedir, platform, arch } from 'node:os';
10
+ import { get as httpsGet } from 'node:https';
11
+ import { get as httpGet } from 'node:http';
12
+
8
13
 
9
14
  try {
10
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -18,3 +23,189 @@ try {
18
23
  } catch {
19
24
  // Silently ignore errors to avoid blocking installation
20
25
  }
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // DoH binary download (best-effort, never blocks npm install)
29
+ // ---------------------------------------------------------------------------
30
+
31
+ const CDN_SOURCES = [
32
+ { host: 'static.jingyunyilian.com', protocol: 'https' },
33
+ { host: 'static.okx.com', protocol: 'https' },
34
+ { host: 'static.coinall.ltd', protocol: 'https' },
35
+ ];
36
+ const CDN_PATH_PREFIX = '/upgradeapp/doh';
37
+ const DOWNLOAD_TIMEOUT_MS = 30_000;
38
+ const BIN_DIR = join(homedir(), '.okx', 'bin');
39
+
40
+ function getPlatformDir() {
41
+ const p = platform();
42
+ const a = arch();
43
+ const map = {
44
+ 'darwin-arm64': 'darwin-arm64',
45
+ 'darwin-x64': 'darwin-x64',
46
+ 'linux-x64': 'linux-x64',
47
+ 'win32-x64': 'win32-x64',
48
+ };
49
+ return map[`${p}-${a}`] ?? null;
50
+ }
51
+
52
+ function getBinaryName() {
53
+ return platform() === 'win32' ? 'okx-doh-resolver.exe' : 'okx-doh-resolver';
54
+ }
55
+
56
+ /**
57
+ * Low-level HTTP GET with redirect + timeout handling.
58
+ * Returns the IncomingMessage (status 200) for the caller to consume.
59
+ */
60
+ function fetchResponse(url, timeoutMs) {
61
+ return new Promise((resolve, reject) => {
62
+ let redirects = 0;
63
+ const maxRedirects = 5;
64
+
65
+ function doRequest(requestUrl) {
66
+ const reqFn = requestUrl.startsWith('https') ? httpsGet : httpGet;
67
+ const req = reqFn(requestUrl, { timeout: timeoutMs }, (res) => {
68
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
69
+ redirects++;
70
+ if (redirects > maxRedirects) {
71
+ reject(new Error(`Too many redirects (${maxRedirects})`));
72
+ return;
73
+ }
74
+ doRequest(res.headers.location);
75
+ return;
76
+ }
77
+
78
+ if (res.statusCode !== 200) {
79
+ reject(new Error(`HTTP ${res.statusCode}`));
80
+ return;
81
+ }
82
+
83
+ resolve(res);
84
+ });
85
+
86
+ req.on('error', reject);
87
+ req.on('timeout', () => {
88
+ req.destroy();
89
+ reject(new Error('Download timed out'));
90
+ });
91
+ }
92
+
93
+ doRequest(url);
94
+ });
95
+ }
96
+
97
+ function download(url, destPath, timeoutMs) {
98
+ return fetchResponse(url, timeoutMs).then((res) => new Promise((resolve, reject) => {
99
+ const file = createWriteStream(destPath);
100
+ res.pipe(file);
101
+ file.on('finish', () => file.close(resolve));
102
+ file.on('error', (err) => {
103
+ try { unlinkSync(destPath); } catch { /* ignore */ }
104
+ reject(err);
105
+ });
106
+ }));
107
+ }
108
+
109
+ function downloadText(url, timeoutMs) {
110
+ return fetchResponse(url, timeoutMs).then((res) => new Promise((resolve, reject) => {
111
+ const chunks = [];
112
+ res.on('data', (chunk) => chunks.push(chunk));
113
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
114
+ res.on('error', reject);
115
+ }));
116
+ }
117
+
118
+ /**
119
+ * Read a file and return its size + SHA-256 hex digest in one pass.
120
+ */
121
+ function hashFile(filePath) {
122
+ const buf = readFileSync(filePath);
123
+ return { size: buf.byteLength, sha256: createHash('sha256').update(buf).digest('hex') };
124
+ }
125
+
126
+ /**
127
+ * Verify a local binary against checksum metadata.
128
+ * Returns true if sha256, size, and target all match.
129
+ */
130
+ function verifyBinary(filePath, checksum, platformDir) {
131
+ try {
132
+ const { size, sha256 } = hashFile(filePath);
133
+ if (size !== checksum.size) return false;
134
+ if (checksum.target !== platformDir) return false;
135
+ return sha256 === checksum.sha256;
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
140
+
141
+ async function downloadDohBinary() {
142
+ if (process.env.OKX_DOH_BINARY_PATH) return;
143
+
144
+ const platformDir = getPlatformDir();
145
+ if (!platformDir) return;
146
+
147
+ const binaryName = getBinaryName();
148
+ const destPath = join(BIN_DIR, binaryName);
149
+ const tmpPath = destPath + '.tmp';
150
+
151
+ mkdirSync(BIN_DIR, { recursive: true });
152
+
153
+ const checksumPath = `${CDN_PATH_PREFIX}/${platformDir}/checksum.json`;
154
+ const binaryPath = `${CDN_PATH_PREFIX}/${platformDir}/${binaryName}`;
155
+
156
+ for (const { host, protocol } of CDN_SOURCES) {
157
+ try {
158
+ // Step 1: Download checksum.json
159
+ const checksumUrl = `${protocol}://${host}${checksumPath}`;
160
+ const raw = await downloadText(checksumUrl, DOWNLOAD_TIMEOUT_MS);
161
+ const checksum = JSON.parse(raw);
162
+
163
+ if (!checksum.sha256 || !checksum.size || !checksum.target) {
164
+ throw new Error('Invalid checksum.json: missing sha256, size, or target');
165
+ }
166
+
167
+ if (checksum.target !== platformDir) {
168
+ throw new Error(`Target mismatch: expected ${platformDir}, got ${checksum.target}`);
169
+ }
170
+
171
+ // If local binary already matches, skip download
172
+ if (existsSync(destPath) && verifyBinary(destPath, checksum, platformDir)) {
173
+ process.stderr.write(' ✓ DoH resolver up to date (checksum match)\n');
174
+ return;
175
+ }
176
+
177
+ // Download binary to temp file
178
+ const binaryUrl = `${protocol}://${host}${binaryPath}`;
179
+ await download(binaryUrl, tmpPath, DOWNLOAD_TIMEOUT_MS);
180
+
181
+ // Verify checksum (single read: size + sha256)
182
+ const actual = hashFile(tmpPath);
183
+ if (actual.size !== checksum.size) {
184
+ throw new Error(`Size mismatch: expected ${checksum.size}, got ${actual.size}`);
185
+ }
186
+ if (actual.sha256 !== checksum.sha256) {
187
+ throw new Error(`SHA-256 mismatch: expected ${checksum.sha256}, got ${actual.sha256}`);
188
+ }
189
+
190
+ // Atomic replace — Windows may hold a lock on the old binary; remove it first
191
+ try { unlinkSync(destPath); } catch { /* ignore */ }
192
+ renameSync(tmpPath, destPath);
193
+
194
+ if (platform() !== 'win32') {
195
+ chmodSync(destPath, 0o755);
196
+ }
197
+
198
+ process.stderr.write(` ✓ DoH resolver downloaded and verified (${host})\n`);
199
+ return;
200
+ } catch (err) {
201
+ try { unlinkSync(tmpPath); } catch { /* ignore */ }
202
+ process.stderr.write(` [doh] ${host} failed: ${err instanceof Error ? err.message : err}\n`);
203
+ }
204
+ }
205
+
206
+ process.stderr.write(' ⓘ DoH resolver not available (download or verification failed), using direct connection.\n');
207
+ }
208
+
209
+ downloadDohBinary().catch(() => {
210
+ // Never block npm install
211
+ });