@jango-blockchained/hoox-shared 1.0.8 → 1.1.0
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 +17 -0
- package/dist/analytics.d.ts +23 -2
- package/dist/analytics.d.ts.map +1 -1
- package/dist/analytics.js +14 -3
- package/dist/api-client.d.ts +12 -2
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +74 -7
- package/dist/colors.d.ts +63 -25
- package/dist/colors.d.ts.map +1 -1
- package/dist/colors.js +60 -20
- package/dist/config.d.ts +34 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +61 -6
- package/dist/cron-handler.d.ts +34 -0
- package/dist/cron-handler.d.ts.map +1 -0
- package/dist/cron-handler.js +48 -0
- package/dist/d1/index.js +50 -6
- package/dist/d1/repository.d.ts +5 -0
- package/dist/d1/repository.d.ts.map +1 -1
- package/dist/d1/repository.js +297 -0
- package/dist/d1/schemas.js +81 -0
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +48 -5
- package/dist/exchanges/base-exchange-client.d.ts +68 -0
- package/dist/exchanges/base-exchange-client.d.ts.map +1 -0
- package/dist/exchanges/base-exchange-client.js +120 -0
- package/dist/exchanges/index.d.ts +3 -0
- package/dist/exchanges/index.d.ts.map +1 -0
- package/dist/exchanges/index.js +120 -0
- package/dist/exchanges/types.d.ts +19 -0
- package/dist/exchanges/types.d.ts.map +1 -0
- package/dist/exchanges/types.js +1 -0
- package/dist/health.js +48 -5
- package/dist/index.d.ts +30 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1058 -73
- package/dist/kvUtils.js +48 -5
- package/dist/middleware/auth.d.ts +46 -7
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +151 -0
- package/dist/middleware/cors.d.ts +28 -2
- package/dist/middleware/cors.d.ts.map +1 -1
- package/dist/middleware/cors.js +77 -0
- package/dist/middleware/index.d.ts +2 -2
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +361 -101
- package/dist/middleware/logger.d.ts +2 -1
- package/dist/middleware/logger.d.ts.map +1 -1
- package/dist/middleware/logger.js +202 -0
- package/dist/middleware/rate-limit.d.ts +3 -1
- package/dist/middleware/rate-limit.d.ts.map +1 -1
- package/dist/middleware/rate-limit.js +181 -0
- package/dist/middleware/security-headers.js +58 -0
- package/dist/middleware/validate.js +56 -0
- package/dist/operator-transport.d.ts +61 -0
- package/dist/operator-transport.d.ts.map +1 -0
- package/dist/operator-transport.js +87 -0
- package/dist/path-utils.d.ts +205 -0
- package/dist/path-utils.d.ts.map +1 -0
- package/dist/path-utils.js +188 -0
- package/dist/queue-handler.d.ts +43 -0
- package/dist/queue-handler.d.ts.map +1 -0
- package/dist/queue-handler.js +46 -0
- package/dist/router.d.ts +2 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +15 -8
- package/dist/schemas/index.js +7 -4
- package/dist/schemas/registry.js +387 -0
- package/dist/schemas/types.js +1 -0
- package/dist/schemas/validators.d.ts.map +1 -1
- package/dist/schemas/validators.js +290 -0
- package/dist/service-bindings.d.ts +68 -26
- package/dist/service-bindings.d.ts.map +1 -1
- package/dist/service-bindings.js +219 -2
- package/dist/session.d.ts +9 -5
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +179 -4
- package/dist/sse.d.ts +12 -3
- package/dist/sse.d.ts.map +1 -1
- package/dist/sse.js +70 -7
- package/dist/stores/config-store.d.ts.map +1 -1
- package/dist/stores/config-store.js +129 -6
- package/dist/stores/service-store.d.ts +50 -1
- package/dist/stores/service-store.d.ts.map +1 -1
- package/dist/stores/service-store.js +162 -27
- package/dist/test-utils.d.ts +132 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +128 -0
- package/dist/types/router.d.ts +18 -8
- package/dist/types/router.d.ts.map +1 -1
- package/dist/types.d.ts +124 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +11 -0
- package/dist/wizard/engine.d.ts.map +1 -1
- package/dist/wizard/engine.js +501 -0
- package/dist/wizard/index.js +2 -1
- package/dist/wizard/persistence.js +31 -0
- package/dist/wizard/presets.js +199 -0
- package/dist/wizard/provisioner.js +1 -0
- package/dist/wizard/types.js +1 -0
- package/package.json +46 -12
package/dist/index.js
CHANGED
|
@@ -15,6 +15,69 @@ var __export = (target, all) => {
|
|
|
15
15
|
};
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
17
|
|
|
18
|
+
// src/operator-transport.ts
|
|
19
|
+
function stripTrailingSlashes(url) {
|
|
20
|
+
return url.replace(/\/+$/, "");
|
|
21
|
+
}
|
|
22
|
+
function parseTransport(raw) {
|
|
23
|
+
if (!raw)
|
|
24
|
+
return null;
|
|
25
|
+
const v = raw.trim().toLowerCase();
|
|
26
|
+
if (VALID_TRANSPORTS.has(v)) {
|
|
27
|
+
return v;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function resolveOperatorTransportProfile(env = process.env, options = {}) {
|
|
32
|
+
const accessClientId = env.CF_ACCESS_CLIENT_ID?.trim() ?? "";
|
|
33
|
+
const accessClientSecret = env.CF_ACCESS_CLIENT_SECRET?.trim() ?? "";
|
|
34
|
+
const hasAccess = Boolean(accessClientId && accessClientSecret);
|
|
35
|
+
const explicit = parseTransport(env.HOOX_TRANSPORT) ?? parseTransport(options.configTransport);
|
|
36
|
+
const transport = explicit ?? (hasAccess ? "access" : "public");
|
|
37
|
+
const apiFromEnv = env.HOOX_API_URL?.trim();
|
|
38
|
+
const apiFromConfig = options.configApiUrl?.trim();
|
|
39
|
+
const tokenFromEnv = env.HOOX_API_TOKEN?.trim();
|
|
40
|
+
const tokenFromConfig = options.configApiToken?.trim();
|
|
41
|
+
return {
|
|
42
|
+
transport,
|
|
43
|
+
apiBase: stripTrailingSlashes(apiFromEnv || apiFromConfig || DEFAULT_API_BASE),
|
|
44
|
+
bearerToken: tokenFromEnv || tokenFromConfig || "",
|
|
45
|
+
accessClientId,
|
|
46
|
+
accessClientSecret
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function buildOperatorAuthHeaders(profile) {
|
|
50
|
+
const headers = {};
|
|
51
|
+
if (profile.bearerToken) {
|
|
52
|
+
headers.Authorization = `Bearer ${profile.bearerToken}`;
|
|
53
|
+
}
|
|
54
|
+
const useAccessHeaders = profile.transport === "access" || Boolean(profile.accessClientId) && Boolean(profile.accessClientSecret);
|
|
55
|
+
if (useAccessHeaders && profile.accessClientId && profile.accessClientSecret) {
|
|
56
|
+
headers["CF-Access-Client-Id"] = profile.accessClientId;
|
|
57
|
+
headers["CF-Access-Client-Secret"] = profile.accessClientSecret;
|
|
58
|
+
}
|
|
59
|
+
return headers;
|
|
60
|
+
}
|
|
61
|
+
function hasOperatorClientCredentials(profile) {
|
|
62
|
+
if (profile.bearerToken)
|
|
63
|
+
return true;
|
|
64
|
+
return Boolean(profile.accessClientId && profile.accessClientSecret);
|
|
65
|
+
}
|
|
66
|
+
function operatorUrl(profile, path) {
|
|
67
|
+
const base = profile.apiBase.replace(/\/+$/, "");
|
|
68
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
69
|
+
return `${base}${p}`;
|
|
70
|
+
}
|
|
71
|
+
var DEFAULT_API_BASE = "http://localhost:8787", VALID_TRANSPORTS;
|
|
72
|
+
var init_operator_transport = __esm(() => {
|
|
73
|
+
VALID_TRANSPORTS = new Set([
|
|
74
|
+
"public",
|
|
75
|
+
"access",
|
|
76
|
+
"mtls",
|
|
77
|
+
"tunnel"
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
|
|
18
81
|
// src/api-client.ts
|
|
19
82
|
var exports_api_client = {};
|
|
20
83
|
__export(exports_api_client, {
|
|
@@ -40,20 +103,25 @@ function isNetworkError(error) {
|
|
|
40
103
|
function sleep(ms) {
|
|
41
104
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
42
105
|
}
|
|
106
|
+
function activeProfile(override) {
|
|
107
|
+
return override ?? resolveOperatorTransportProfile();
|
|
108
|
+
}
|
|
43
109
|
async function hooxFetch(path, options) {
|
|
110
|
+
const profile = options?.transport ?? (options?.transportEnv ? resolveOperatorTransportProfile(options.transportEnv) : activeProfile());
|
|
111
|
+
const { transport: _t, transportEnv: _e, ...fetchInit } = options ?? {};
|
|
44
112
|
let lastError;
|
|
45
113
|
for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
|
|
46
114
|
const controller = new AbortController;
|
|
47
115
|
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
48
116
|
try {
|
|
49
|
-
const url =
|
|
117
|
+
const url = operatorUrl(profile, path);
|
|
50
118
|
const headers = {
|
|
51
119
|
"Content-Type": "application/json",
|
|
52
|
-
...
|
|
53
|
-
...
|
|
120
|
+
...buildOperatorAuthHeaders(profile),
|
|
121
|
+
...fetchInit.headers
|
|
54
122
|
};
|
|
55
123
|
const response = await fetch(url, {
|
|
56
|
-
...
|
|
124
|
+
...fetchInit,
|
|
57
125
|
headers,
|
|
58
126
|
signal: controller.signal
|
|
59
127
|
});
|
|
@@ -101,10 +169,9 @@ async function hooxFetch(path, options) {
|
|
|
101
169
|
retryable: false
|
|
102
170
|
});
|
|
103
171
|
}
|
|
104
|
-
var
|
|
172
|
+
var FETCH_TIMEOUT_MS = 5000, MAX_RETRIES = 3, BASE_RETRY_DELAY_MS = 1000, MAX_RETRY_DELAY_MS = 16000, WorkerAPIError;
|
|
105
173
|
var init_api_client = __esm(() => {
|
|
106
|
-
|
|
107
|
-
API_TOKEN = process.env.HOOX_API_TOKEN || "";
|
|
174
|
+
init_operator_transport();
|
|
108
175
|
WorkerAPIError = class WorkerAPIError extends Error {
|
|
109
176
|
status;
|
|
110
177
|
retryable;
|
|
@@ -122,17 +189,18 @@ var exports_sse = {};
|
|
|
122
189
|
__export(exports_sse, {
|
|
123
190
|
subscribeSSE: () => subscribeSSE
|
|
124
191
|
});
|
|
125
|
-
function subscribeSSE(path, callback, onStatus) {
|
|
192
|
+
function subscribeSSE(path, callback, onStatus, options) {
|
|
126
193
|
let aborted = false;
|
|
194
|
+
const profile = options?.transport ?? (options?.transportEnv ? resolveOperatorTransportProfile(options.transportEnv) : resolveOperatorTransportProfile());
|
|
127
195
|
const run = async () => {
|
|
128
196
|
let attempt = 0;
|
|
129
197
|
while (!aborted && attempt < MAX_RECONNECT_ATTEMPTS) {
|
|
130
198
|
try {
|
|
131
|
-
const url =
|
|
199
|
+
const url = operatorUrl(profile, path);
|
|
132
200
|
const response = await fetch(url, {
|
|
133
201
|
headers: {
|
|
134
202
|
Accept: "text/event-stream",
|
|
135
|
-
...
|
|
203
|
+
...buildOperatorAuthHeaders(profile)
|
|
136
204
|
}
|
|
137
205
|
});
|
|
138
206
|
if (!response.ok || !response.body) {
|
|
@@ -161,7 +229,7 @@ function subscribeSSE(path, callback, onStatus) {
|
|
|
161
229
|
}
|
|
162
230
|
}
|
|
163
231
|
}
|
|
164
|
-
} catch
|
|
232
|
+
} catch {
|
|
165
233
|
if (aborted)
|
|
166
234
|
break;
|
|
167
235
|
onStatus?.("reconnecting");
|
|
@@ -181,21 +249,31 @@ function subscribeSSE(path, callback, onStatus) {
|
|
|
181
249
|
}
|
|
182
250
|
};
|
|
183
251
|
}
|
|
184
|
-
var
|
|
252
|
+
var RECONNECT_BASE_MS = 1000, RECONNECT_MAX_MS = 16000, MAX_RECONNECT_ATTEMPTS = 10;
|
|
185
253
|
var init_sse = __esm(() => {
|
|
186
|
-
|
|
187
|
-
API_TOKEN2 = process.env.HOOX_API_TOKEN || "";
|
|
254
|
+
init_operator_transport();
|
|
188
255
|
});
|
|
189
256
|
|
|
190
257
|
// src/analytics.ts
|
|
191
|
-
async function trackAnalytics(env, endpoint, body) {
|
|
258
|
+
async function trackAnalytics(env, endpoint, body, options) {
|
|
192
259
|
if (!env.ANALYTICS_SERVICE)
|
|
193
260
|
return;
|
|
194
261
|
try {
|
|
262
|
+
const payload = {
|
|
263
|
+
...body,
|
|
264
|
+
...options?.indexes ? { indexes: [...options.indexes] } : {}
|
|
265
|
+
};
|
|
266
|
+
const headers = {
|
|
267
|
+
"Content-Type": "application/json"
|
|
268
|
+
};
|
|
269
|
+
const internalKey = env.INTERNAL_KEY_BINDING;
|
|
270
|
+
if (internalKey) {
|
|
271
|
+
headers["X-Internal-Auth-Key"] = internalKey;
|
|
272
|
+
}
|
|
195
273
|
await env.ANALYTICS_SERVICE.fetch(new Request(`http://localhost${endpoint}`, {
|
|
196
274
|
method: "POST",
|
|
197
|
-
headers
|
|
198
|
-
body: JSON.stringify(
|
|
275
|
+
headers,
|
|
276
|
+
body: JSON.stringify(payload)
|
|
199
277
|
}));
|
|
200
278
|
} catch (e) {
|
|
201
279
|
console.error("Analytics tracking failed:", e);
|
|
@@ -204,36 +282,81 @@ async function trackAnalytics(env, endpoint, body) {
|
|
|
204
282
|
|
|
205
283
|
// src/colors.ts
|
|
206
284
|
var Colors = {
|
|
207
|
-
background: "#
|
|
208
|
-
foreground: "#
|
|
209
|
-
card: "#
|
|
210
|
-
border: "#
|
|
211
|
-
muted: "#
|
|
212
|
-
"muted-foreground": "#
|
|
213
|
-
dim: "#
|
|
214
|
-
accent: "#
|
|
215
|
-
"accent-dim": "#
|
|
216
|
-
success: "#
|
|
217
|
-
warning: "#
|
|
218
|
-
error: "#
|
|
219
|
-
info: "#
|
|
220
|
-
text: "#
|
|
221
|
-
"text-muted": "#
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
285
|
+
background: "#050508",
|
|
286
|
+
foreground: "#E8E8F0",
|
|
287
|
+
card: "#0A0A0F",
|
|
288
|
+
border: "#232330",
|
|
289
|
+
muted: "#8B8B9E",
|
|
290
|
+
"muted-foreground": "#6E6E84",
|
|
291
|
+
dim: "#3C3C50",
|
|
292
|
+
accent: "#818CF8",
|
|
293
|
+
"accent-dim": "#6366F1",
|
|
294
|
+
success: "#34D399",
|
|
295
|
+
warning: "#FBBF24",
|
|
296
|
+
error: "#FB7185",
|
|
297
|
+
info: "#38BDF8",
|
|
298
|
+
text: "#E8E8F0",
|
|
299
|
+
"text-muted": "#8B8B9E",
|
|
300
|
+
panel: "#0A0A0F",
|
|
301
|
+
divider: "#232330",
|
|
302
|
+
highlight: "#22D3EE",
|
|
303
|
+
backdrop: "#000000"
|
|
304
|
+
};
|
|
305
|
+
var CoolBracketPalette = [
|
|
306
|
+
"#22D3EE",
|
|
307
|
+
"#38BDF8",
|
|
308
|
+
"#60A5FA",
|
|
309
|
+
"#818CF8",
|
|
310
|
+
"#A78BFA",
|
|
311
|
+
"#C084FC",
|
|
312
|
+
"#E879F9",
|
|
313
|
+
"#A78BFA",
|
|
314
|
+
"#818CF8",
|
|
315
|
+
"#38BDF8"
|
|
316
|
+
];
|
|
317
|
+
var ConnectionStatusColor = {
|
|
318
|
+
connected: Colors.success,
|
|
319
|
+
polling: Colors.highlight,
|
|
320
|
+
reconnecting: Colors.warning,
|
|
321
|
+
offline: Colors.error
|
|
322
|
+
};
|
|
323
|
+
var WorkerStatusColor = {
|
|
324
|
+
operational: Colors.success,
|
|
325
|
+
degraded: Colors.warning,
|
|
326
|
+
down: Colors.error
|
|
327
|
+
};
|
|
328
|
+
var LogLevelColor = {
|
|
329
|
+
error: Colors.error,
|
|
330
|
+
warn: Colors.warning,
|
|
331
|
+
info: Colors.foreground,
|
|
332
|
+
debug: Colors.muted
|
|
333
|
+
};
|
|
334
|
+
var AlertSeverityColor = {
|
|
335
|
+
info: Colors.info,
|
|
336
|
+
warning: Colors.warning,
|
|
337
|
+
error: Colors.error,
|
|
338
|
+
critical: Colors.error
|
|
226
339
|
};
|
|
227
340
|
|
|
228
341
|
// src/config.ts
|
|
229
|
-
import {
|
|
342
|
+
import {
|
|
343
|
+
readFileSync,
|
|
344
|
+
writeFileSync,
|
|
345
|
+
existsSync,
|
|
346
|
+
mkdirSync,
|
|
347
|
+
chmodSync,
|
|
348
|
+
statSync
|
|
349
|
+
} from "fs";
|
|
230
350
|
import { join } from "path";
|
|
231
351
|
import { homedir } from "os";
|
|
232
352
|
var HOX_DIR = join(homedir(), ".hoox");
|
|
233
353
|
var CONFIG_PATH = join(HOX_DIR, "config.json");
|
|
354
|
+
var HOOX_DIR_MODE = 448;
|
|
355
|
+
var HOOX_CONFIG_FILE_MODE = 384;
|
|
234
356
|
var DEFAULT_CONFIG = {
|
|
235
357
|
apiUrl: "http://localhost:8787",
|
|
236
358
|
apiToken: "",
|
|
359
|
+
transport: "public",
|
|
237
360
|
refreshIntervalMs: 500,
|
|
238
361
|
theme: "dark",
|
|
239
362
|
activeExchanges: ["binance", "bybit", "mexc"],
|
|
@@ -266,12 +389,41 @@ function readConfigSync() {
|
|
|
266
389
|
async function readConfig() {
|
|
267
390
|
return readConfigSync();
|
|
268
391
|
}
|
|
392
|
+
function ensureHooxDirSecure(dir = HOX_DIR) {
|
|
393
|
+
if (!existsSync(dir)) {
|
|
394
|
+
mkdirSync(dir, { recursive: true, mode: HOOX_DIR_MODE });
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
chmodSync(dir, HOOX_DIR_MODE);
|
|
398
|
+
} catch {}
|
|
399
|
+
}
|
|
400
|
+
function writeSecureFileSync(filePath, contents, mode = HOOX_CONFIG_FILE_MODE) {
|
|
401
|
+
writeFileSync(filePath, contents, { encoding: "utf-8", mode });
|
|
402
|
+
try {
|
|
403
|
+
chmodSync(filePath, mode);
|
|
404
|
+
} catch {}
|
|
405
|
+
}
|
|
406
|
+
function isUnixModeGroupOrWorldAccessible(mode) {
|
|
407
|
+
return (mode & 63) !== 0;
|
|
408
|
+
}
|
|
409
|
+
function isConfigWorldOrGroupReadable(filePath = CONFIG_PATH) {
|
|
410
|
+
try {
|
|
411
|
+
if (!existsSync(filePath))
|
|
412
|
+
return false;
|
|
413
|
+
return isUnixModeGroupOrWorldAccessible(statSync(filePath).mode & 511);
|
|
414
|
+
} catch {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function formatConfigPermissionWarning(filePath = CONFIG_PATH) {
|
|
419
|
+
if (!isConfigWorldOrGroupReadable(filePath))
|
|
420
|
+
return null;
|
|
421
|
+
return `Warning: ${filePath} is group/world-accessible. ` + `Run: chmod 600 ${filePath} (and chmod 700 on the parent dir). ` + `This file may contain apiToken / operator secrets.`;
|
|
422
|
+
}
|
|
269
423
|
function writeConfigSync(config) {
|
|
270
424
|
try {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
425
|
+
ensureHooxDirSecure(HOX_DIR);
|
|
426
|
+
writeSecureFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), HOOX_CONFIG_FILE_MODE);
|
|
275
427
|
} catch (err) {
|
|
276
428
|
console.error("Failed to write config:", err);
|
|
277
429
|
}
|
|
@@ -279,6 +431,12 @@ function writeConfigSync(config) {
|
|
|
279
431
|
async function writeConfig(config) {
|
|
280
432
|
writeConfigSync(config);
|
|
281
433
|
}
|
|
434
|
+
var VALID_TRANSPORTS2 = new Set([
|
|
435
|
+
"public",
|
|
436
|
+
"access",
|
|
437
|
+
"mtls",
|
|
438
|
+
"tunnel"
|
|
439
|
+
]);
|
|
282
440
|
function validateConfig(config) {
|
|
283
441
|
const errors = [];
|
|
284
442
|
if (config.apiUrl && !config.apiUrl.startsWith("http")) {
|
|
@@ -290,9 +448,41 @@ function validateConfig(config) {
|
|
|
290
448
|
if (config.theme && !["dark", "light"].includes(config.theme)) {
|
|
291
449
|
errors.push('theme must be "dark" or "light"');
|
|
292
450
|
}
|
|
451
|
+
if (config.transport !== undefined && !VALID_TRANSPORTS2.has(config.transport)) {
|
|
452
|
+
errors.push('transport must be "public" | "access" | "mtls" | "tunnel"');
|
|
453
|
+
}
|
|
293
454
|
return errors;
|
|
294
455
|
}
|
|
295
456
|
|
|
457
|
+
// src/cron-handler.ts
|
|
458
|
+
function createCronHandler(options) {
|
|
459
|
+
const { name, handler, logger } = options;
|
|
460
|
+
return async (event, env, ctx) => {
|
|
461
|
+
const startTime = Date.now();
|
|
462
|
+
logger?.info(`${name} cron triggered`, {
|
|
463
|
+
cron: event.cron,
|
|
464
|
+
scheduledTime: event.scheduledTime
|
|
465
|
+
});
|
|
466
|
+
try {
|
|
467
|
+
await handler(event, env, ctx);
|
|
468
|
+
const durationMs = Date.now() - startTime;
|
|
469
|
+
logger?.info(`${name} cron handler completed successfully`, {
|
|
470
|
+
cron: event.cron,
|
|
471
|
+
durationMs
|
|
472
|
+
});
|
|
473
|
+
} catch (error) {
|
|
474
|
+
const durationMs = Date.now() - startTime;
|
|
475
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
476
|
+
logger?.error(`${name} cron handler failed`, {
|
|
477
|
+
cron: event.cron,
|
|
478
|
+
durationMs,
|
|
479
|
+
error: errorMsg
|
|
480
|
+
});
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
296
486
|
// src/errors.ts
|
|
297
487
|
function toError(err, fallback = "Unknown error") {
|
|
298
488
|
if (err instanceof Error)
|
|
@@ -310,22 +500,65 @@ function toError(err, fallback = "Unknown error") {
|
|
|
310
500
|
return fallback;
|
|
311
501
|
}
|
|
312
502
|
}
|
|
503
|
+
var SENSITIVE_FIELDS = new Set([
|
|
504
|
+
"password",
|
|
505
|
+
"token",
|
|
506
|
+
"secret",
|
|
507
|
+
"api_key",
|
|
508
|
+
"apiKey",
|
|
509
|
+
"authorization"
|
|
510
|
+
]);
|
|
511
|
+
function hasSensitiveFields(obj) {
|
|
512
|
+
for (const key of Object.keys(obj)) {
|
|
513
|
+
if (SENSITIVE_FIELDS.has(key))
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
function hasErrorValues(obj) {
|
|
519
|
+
for (const v of Object.values(obj)) {
|
|
520
|
+
if (v instanceof Error)
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
313
525
|
function sanitizeOutput(data) {
|
|
526
|
+
if (data === null || data === undefined)
|
|
527
|
+
return data;
|
|
528
|
+
const t = typeof data;
|
|
529
|
+
if (t === "string" || t === "number" || t === "boolean")
|
|
530
|
+
return data;
|
|
314
531
|
if (data instanceof Error) {
|
|
315
532
|
return { name: data.name, message: data.message };
|
|
316
533
|
}
|
|
317
|
-
if (
|
|
534
|
+
if (Array.isArray(data)) {
|
|
535
|
+
return data.map(sanitizeOutput);
|
|
536
|
+
}
|
|
537
|
+
if (t === "object") {
|
|
538
|
+
const obj = data;
|
|
539
|
+
const keys = Object.keys(obj);
|
|
540
|
+
if (keys.length < 10) {
|
|
541
|
+
const sanitized2 = {};
|
|
542
|
+
for (const k of keys) {
|
|
543
|
+
if (k === "stack" || k === "cause")
|
|
544
|
+
continue;
|
|
545
|
+
const v = obj[k];
|
|
546
|
+
sanitized2[k] = v instanceof Error ? { name: v.name, message: v.message } : sanitizeOutput(v);
|
|
547
|
+
}
|
|
548
|
+
return sanitized2;
|
|
549
|
+
}
|
|
550
|
+
if (!hasSensitiveFields(obj) && !hasErrorValues(obj)) {
|
|
551
|
+
return obj;
|
|
552
|
+
}
|
|
318
553
|
const sanitized = {};
|
|
319
|
-
for (const
|
|
554
|
+
for (const k of keys) {
|
|
320
555
|
if (k === "stack" || k === "cause")
|
|
321
556
|
continue;
|
|
557
|
+
const v = obj[k];
|
|
322
558
|
sanitized[k] = v instanceof Error ? { name: v.name, message: v.message } : sanitizeOutput(v);
|
|
323
559
|
}
|
|
324
560
|
return sanitized;
|
|
325
561
|
}
|
|
326
|
-
if (Array.isArray(data)) {
|
|
327
|
-
return data.map(sanitizeOutput);
|
|
328
|
-
}
|
|
329
562
|
return data;
|
|
330
563
|
}
|
|
331
564
|
function createJsonResponse(data, status = 200) {
|
|
@@ -421,7 +654,7 @@ class D1Repository {
|
|
|
421
654
|
async batch(statements) {
|
|
422
655
|
try {
|
|
423
656
|
const prepared = statements.map((s) => s.params && s.params.length > 0 ? this.db.prepare(s.query).bind(...s.params) : this.db.prepare(s.query));
|
|
424
|
-
const results = await this.db.batch(
|
|
657
|
+
const results = await this.db.batch(prepared);
|
|
425
658
|
return results.map((r) => ({
|
|
426
659
|
success: r.success,
|
|
427
660
|
meta: r.meta ? { last_row_id: r.meta.last_row_id, changes: r.meta.changes } : undefined,
|
|
@@ -527,6 +760,7 @@ class D1Repository {
|
|
|
527
760
|
}
|
|
528
761
|
}
|
|
529
762
|
}
|
|
763
|
+
|
|
530
764
|
// src/d1/schemas.ts
|
|
531
765
|
import { z } from "zod";
|
|
532
766
|
var TradeRecordSchema = z.object({
|
|
@@ -668,6 +902,106 @@ class BaseExchangeClient {
|
|
|
668
902
|
}
|
|
669
903
|
}
|
|
670
904
|
|
|
905
|
+
// src/exchanges/base-exchange-client.ts
|
|
906
|
+
class BaseExchangeClient2 {
|
|
907
|
+
apiKey;
|
|
908
|
+
apiSecret;
|
|
909
|
+
baseUrl;
|
|
910
|
+
isTestnet;
|
|
911
|
+
importedKeyPromise;
|
|
912
|
+
constructor(apiKey, apiSecret) {
|
|
913
|
+
if (!apiKey || !apiSecret) {
|
|
914
|
+
throw new Error(this.getErrorMessagePrefix() + "API key and secret are required.");
|
|
915
|
+
}
|
|
916
|
+
this.apiKey = apiKey;
|
|
917
|
+
this.apiSecret = apiSecret;
|
|
918
|
+
this.baseUrl = this.getDefaultBaseUrl();
|
|
919
|
+
this.isTestnet = false;
|
|
920
|
+
this.importedKeyPromise = this.importKey();
|
|
921
|
+
}
|
|
922
|
+
getErrorMessagePrefix() {
|
|
923
|
+
return "";
|
|
924
|
+
}
|
|
925
|
+
generateSignature(_params) {
|
|
926
|
+
throw new Error("generateSignature must be implemented by subclass");
|
|
927
|
+
}
|
|
928
|
+
buildHeaders(_method, _path, _params) {
|
|
929
|
+
return new Headers;
|
|
930
|
+
}
|
|
931
|
+
async importKey() {
|
|
932
|
+
const encoder = new TextEncoder;
|
|
933
|
+
return crypto.subtle.importKey("raw", encoder.encode(this.apiSecret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
934
|
+
}
|
|
935
|
+
async cryptoSign(data) {
|
|
936
|
+
const encoder = new TextEncoder;
|
|
937
|
+
const key = await this.importedKeyPromise;
|
|
938
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
|
|
939
|
+
return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
940
|
+
}
|
|
941
|
+
async fetch(method, path, params) {
|
|
942
|
+
const url = `${this.baseUrl}${path}`;
|
|
943
|
+
const headers = this.buildHeaders(method, path, params);
|
|
944
|
+
const body = params ? new URLSearchParams(params).toString() : undefined;
|
|
945
|
+
const response = await fetch(url, {
|
|
946
|
+
method,
|
|
947
|
+
headers,
|
|
948
|
+
body: method !== "GET" ? body : undefined
|
|
949
|
+
});
|
|
950
|
+
if (!response.ok) {
|
|
951
|
+
const error = await response.text();
|
|
952
|
+
throw new Error(`Exchange API error: ${response.status} - ${error}`);
|
|
953
|
+
}
|
|
954
|
+
return response.json();
|
|
955
|
+
}
|
|
956
|
+
setLeverage(_symbol, _leverage) {
|
|
957
|
+
throw new Error("setLeverage must be implemented by subclass");
|
|
958
|
+
}
|
|
959
|
+
executeTrade(_params) {
|
|
960
|
+
throw new Error("executeTrade must be implemented by subclass");
|
|
961
|
+
}
|
|
962
|
+
getAccountInfo() {
|
|
963
|
+
throw new Error("getAccountInfo must be implemented by subclass");
|
|
964
|
+
}
|
|
965
|
+
getPositions(_symbol) {
|
|
966
|
+
throw new Error("getPositions must be implemented by subclass");
|
|
967
|
+
}
|
|
968
|
+
async openLong(symbol, quantity, price, orderType = "MARKET") {
|
|
969
|
+
return this.executeTrade({
|
|
970
|
+
symbol,
|
|
971
|
+
side: "BUY",
|
|
972
|
+
orderType,
|
|
973
|
+
quantity,
|
|
974
|
+
price
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
async openShort(symbol, quantity, price, orderType = "MARKET") {
|
|
978
|
+
return this.executeTrade({
|
|
979
|
+
symbol,
|
|
980
|
+
side: "SELL",
|
|
981
|
+
orderType,
|
|
982
|
+
quantity,
|
|
983
|
+
price
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
async closeLong(symbol, quantity) {
|
|
987
|
+
return this.executeTrade({
|
|
988
|
+
symbol,
|
|
989
|
+
side: "SELL",
|
|
990
|
+
orderType: "MARKET",
|
|
991
|
+
quantity,
|
|
992
|
+
reduceOnly: true
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
async closeShort(symbol, quantity) {
|
|
996
|
+
return this.executeTrade({
|
|
997
|
+
symbol,
|
|
998
|
+
side: "BUY",
|
|
999
|
+
orderType: "MARKET",
|
|
1000
|
+
quantity,
|
|
1001
|
+
reduceOnly: true
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
671
1005
|
// src/format-time.ts
|
|
672
1006
|
function formatRelativeTime(timestampMs, nowMs) {
|
|
673
1007
|
const now = nowMs ?? Date.now();
|
|
@@ -877,6 +1211,16 @@ var WebhookPayloadSchema = z2.object({
|
|
|
877
1211
|
orderType: z2.string().optional(),
|
|
878
1212
|
leverage: z2.number().int().positive().optional()
|
|
879
1213
|
}).strict();
|
|
1214
|
+
var TradeQueueMessageSchema = z2.object({
|
|
1215
|
+
requestId: z2.string().min(1).max(128),
|
|
1216
|
+
exchange: z2.string().min(1).max(64),
|
|
1217
|
+
action: TradeActionSchema,
|
|
1218
|
+
symbol: z2.string().min(1).max(32),
|
|
1219
|
+
quantity: z2.number().positive().finite(),
|
|
1220
|
+
price: z2.number().positive().finite().optional(),
|
|
1221
|
+
leverage: z2.number().int().positive().max(125).optional(),
|
|
1222
|
+
queuedAt: z2.string().min(1).max(64)
|
|
1223
|
+
}).strict();
|
|
880
1224
|
var TradeSignalSchema = z2.object({
|
|
881
1225
|
id: z2.number().int().positive().optional(),
|
|
882
1226
|
source: z2.string().min(1),
|
|
@@ -991,22 +1335,525 @@ function headersToObject(headers) {
|
|
|
991
1335
|
return result;
|
|
992
1336
|
}
|
|
993
1337
|
|
|
1338
|
+
// src/queue-handler.ts
|
|
1339
|
+
function createQueueHandler(options) {
|
|
1340
|
+
const { maxRetries, backoffDelays, onMessage, onRetry, onDLQ, logger } = options;
|
|
1341
|
+
return async (batch) => {
|
|
1342
|
+
for (const msg of batch.messages) {
|
|
1343
|
+
const attemptNumber = msg.attempts || 0;
|
|
1344
|
+
const logId = `[${msg.id}]`;
|
|
1345
|
+
try {
|
|
1346
|
+
logger?.info(`${logId} Processing message (attempt ${attemptNumber + 1})`);
|
|
1347
|
+
await onMessage(msg.body, attemptNumber);
|
|
1348
|
+
logger?.info(`${logId} Message processed successfully`);
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1351
|
+
if (attemptNumber < maxRetries) {
|
|
1352
|
+
const delaySeconds = backoffDelays[attemptNumber] ?? backoffDelays[backoffDelays.length - 1];
|
|
1353
|
+
logger?.info(`${logId} Retrying in ${delaySeconds}s (attempt ${attemptNumber + 2}/${maxRetries + 1})`);
|
|
1354
|
+
await onRetry?.(msg.body, attemptNumber, errorMsg, delaySeconds);
|
|
1355
|
+
msg.retry({ delaySeconds });
|
|
1356
|
+
} else {
|
|
1357
|
+
logger?.error(`${logId} Max retries exceeded (${maxRetries + 1} attempts), moving to DLQ`);
|
|
1358
|
+
await onDLQ?.(msg.body, attemptNumber, errorMsg);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// src/middleware/auth.ts
|
|
1366
|
+
function timingSafeEqual(a, b) {
|
|
1367
|
+
const encoder = new TextEncoder;
|
|
1368
|
+
const aBuf = encoder.encode(a);
|
|
1369
|
+
const bBuf = encoder.encode(b);
|
|
1370
|
+
const len = Math.max(aBuf.length, bBuf.length);
|
|
1371
|
+
let result = aBuf.length === bBuf.length ? 0 : 1;
|
|
1372
|
+
for (let i = 0;i < len; i++) {
|
|
1373
|
+
const av = i < aBuf.length ? aBuf[i] : 0;
|
|
1374
|
+
const bv = i < bBuf.length ? bBuf[i] : 0;
|
|
1375
|
+
result |= av ^ bv;
|
|
1376
|
+
}
|
|
1377
|
+
return result === 0;
|
|
1378
|
+
}
|
|
1379
|
+
function resolveOperatorApiKey(env) {
|
|
1380
|
+
const preferred = env.OPERATOR_API_KEY;
|
|
1381
|
+
if (typeof preferred === "string" && preferred.length > 0) {
|
|
1382
|
+
return preferred;
|
|
1383
|
+
}
|
|
1384
|
+
const legacy = env.INTERNAL_API_KEY;
|
|
1385
|
+
if (typeof legacy === "string" && legacy.length > 0) {
|
|
1386
|
+
return legacy;
|
|
1387
|
+
}
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
async function requireAuth(request, env) {
|
|
1391
|
+
return requireOperatorAuth(request, env);
|
|
1392
|
+
}
|
|
1393
|
+
async function requireOperatorAuth(request, env) {
|
|
1394
|
+
const apiKey = resolveOperatorApiKey(env);
|
|
1395
|
+
if (!apiKey) {
|
|
1396
|
+
return new Response(JSON.stringify({
|
|
1397
|
+
error: "Operator API key not configured",
|
|
1398
|
+
hint: "Set OPERATOR_API_KEY (preferred) or INTERNAL_API_KEY on the Worker"
|
|
1399
|
+
}), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
1400
|
+
}
|
|
1401
|
+
const authHeader = request.headers.get("Authorization");
|
|
1402
|
+
const expectedHeader = `Bearer ${apiKey}`;
|
|
1403
|
+
if (!authHeader || !timingSafeEqual(authHeader, expectedHeader)) {
|
|
1404
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
1405
|
+
status: 401,
|
|
1406
|
+
headers: { "Content-Type": "application/json" }
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
return null;
|
|
1410
|
+
}
|
|
1411
|
+
function createOperatorAuthMiddleware() {
|
|
1412
|
+
return async (request, env, _ctx) => {
|
|
1413
|
+
const denied = await requireOperatorAuth(request, env);
|
|
1414
|
+
if (denied)
|
|
1415
|
+
return denied;
|
|
1416
|
+
return;
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
function normalizeKeyNames(keyName) {
|
|
1420
|
+
if (typeof keyName === "string") {
|
|
1421
|
+
return [keyName];
|
|
1422
|
+
}
|
|
1423
|
+
return [...keyName];
|
|
1424
|
+
}
|
|
1425
|
+
function collectInternalAuthKeys(env, keyNames) {
|
|
1426
|
+
const keys = [];
|
|
1427
|
+
for (const name of normalizeKeyNames(keyNames)) {
|
|
1428
|
+
const value = env[name];
|
|
1429
|
+
if (typeof value === "string" && value.length > 0) {
|
|
1430
|
+
keys.push(value);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return keys;
|
|
1434
|
+
}
|
|
1435
|
+
function matchesAnyInternalAuthKey(providedKey, expectedKeys) {
|
|
1436
|
+
for (const expected of expectedKeys) {
|
|
1437
|
+
if (timingSafeEqual(providedKey, expected)) {
|
|
1438
|
+
return true;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return false;
|
|
1442
|
+
}
|
|
1443
|
+
function internalAuthNotConfiguredResponse(keyNames) {
|
|
1444
|
+
const label = normalizeKeyNames(keyNames).join(" | ");
|
|
1445
|
+
return new Response(JSON.stringify({
|
|
1446
|
+
success: false,
|
|
1447
|
+
error: `Internal auth key(s) not configured: ${label}`
|
|
1448
|
+
}), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
1449
|
+
}
|
|
1450
|
+
function requireInternalAuth(request, env, keyName = "INTERNAL_KEY_BINDING") {
|
|
1451
|
+
const expectedKeys = collectInternalAuthKeys(env, keyName);
|
|
1452
|
+
if (expectedKeys.length === 0) {
|
|
1453
|
+
return internalAuthNotConfiguredResponse(keyName);
|
|
1454
|
+
}
|
|
1455
|
+
const providedKey = request.headers.get("X-Internal-Auth-Key");
|
|
1456
|
+
if (!providedKey || !matchesAnyInternalAuthKey(providedKey, expectedKeys)) {
|
|
1457
|
+
return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
1458
|
+
}
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
function createInternalAuthMiddleware(keyName = "INTERNAL_KEY_BINDING") {
|
|
1462
|
+
return async (request, env, _ctx) => {
|
|
1463
|
+
const expectedKeys = collectInternalAuthKeys(env, keyName);
|
|
1464
|
+
if (expectedKeys.length === 0) {
|
|
1465
|
+
return internalAuthNotConfiguredResponse(keyName);
|
|
1466
|
+
}
|
|
1467
|
+
const providedKey = request.headers.get("X-Internal-Auth-Key");
|
|
1468
|
+
if (!providedKey || !matchesAnyInternalAuthKey(providedKey, expectedKeys)) {
|
|
1469
|
+
return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
1470
|
+
}
|
|
1471
|
+
return;
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
function checkInternalAuth(request, env, keyName = "INTERNAL_KEY_BINDING") {
|
|
1475
|
+
const expectedKeys = collectInternalAuthKeys(env, keyName);
|
|
1476
|
+
if (expectedKeys.length === 0) {
|
|
1477
|
+
return {
|
|
1478
|
+
authorized: false,
|
|
1479
|
+
error: `${normalizeKeyNames(keyName).join(" | ")} not configured`
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
const providedKey = request.headers.get("X-Internal-Auth-Key");
|
|
1483
|
+
if (!providedKey || !matchesAnyInternalAuthKey(providedKey, expectedKeys)) {
|
|
1484
|
+
return { authorized: false, error: "Unauthorized" };
|
|
1485
|
+
}
|
|
1486
|
+
return { authorized: true };
|
|
1487
|
+
}
|
|
1488
|
+
|
|
994
1489
|
// src/service-bindings.ts
|
|
1490
|
+
var InternalAuthKeyFields = {
|
|
1491
|
+
DEFAULT: "INTERNAL_KEY_BINDING",
|
|
1492
|
+
D1_READ: "D1_READ_KEY_BINDING",
|
|
1493
|
+
D1_WRITE: "D1_WRITE_KEY_BINDING",
|
|
1494
|
+
TRADE_EXECUTE: "TRADE_EXECUTE_KEY_BINDING",
|
|
1495
|
+
TRADE_READ: "TRADE_READ_KEY_BINDING",
|
|
1496
|
+
TELEGRAM_ALERT: "TELEGRAM_INTERNAL_KEY_BINDING",
|
|
1497
|
+
WALLET_EXECUTE: "WALLET_EXECUTE_KEY_BINDING",
|
|
1498
|
+
AGENT: "AGENT_INTERNAL_KEY",
|
|
1499
|
+
D1_INTERNAL_ALIAS: "D1_INTERNAL_KEY",
|
|
1500
|
+
TRADE_INTERNAL_ALIAS: "TRADE_INTERNAL_KEY",
|
|
1501
|
+
TELEGRAM_INTERNAL_ALIAS: "TELEGRAM_INTERNAL_KEY"
|
|
1502
|
+
};
|
|
1503
|
+
var D1_READ_AUTH_KEY_FIELDS = [
|
|
1504
|
+
InternalAuthKeyFields.D1_READ,
|
|
1505
|
+
InternalAuthKeyFields.DEFAULT
|
|
1506
|
+
];
|
|
1507
|
+
var D1_WRITE_AUTH_KEY_FIELDS = [
|
|
1508
|
+
InternalAuthKeyFields.D1_WRITE,
|
|
1509
|
+
InternalAuthKeyFields.DEFAULT
|
|
1510
|
+
];
|
|
1511
|
+
var TRADE_EXECUTE_AUTH_KEY_FIELDS = [
|
|
1512
|
+
InternalAuthKeyFields.TRADE_EXECUTE,
|
|
1513
|
+
InternalAuthKeyFields.DEFAULT,
|
|
1514
|
+
InternalAuthKeyFields.AGENT
|
|
1515
|
+
];
|
|
1516
|
+
var TRADE_READ_AUTH_KEY_FIELDS = [
|
|
1517
|
+
InternalAuthKeyFields.TRADE_READ,
|
|
1518
|
+
InternalAuthKeyFields.DEFAULT
|
|
1519
|
+
];
|
|
1520
|
+
var TELEGRAM_ALERT_AUTH_KEY_FIELDS = [
|
|
1521
|
+
InternalAuthKeyFields.TELEGRAM_ALERT,
|
|
1522
|
+
InternalAuthKeyFields.DEFAULT
|
|
1523
|
+
];
|
|
1524
|
+
var WALLET_EXECUTE_AUTH_KEY_FIELDS = [
|
|
1525
|
+
InternalAuthKeyFields.WALLET_EXECUTE,
|
|
1526
|
+
InternalAuthKeyFields.DEFAULT
|
|
1527
|
+
];
|
|
1528
|
+
var DASHBOARD_D1_READ_AUTH_KEY_FIELDS = [
|
|
1529
|
+
InternalAuthKeyFields.D1_READ,
|
|
1530
|
+
InternalAuthKeyFields.D1_INTERNAL_ALIAS,
|
|
1531
|
+
InternalAuthKeyFields.DEFAULT
|
|
1532
|
+
];
|
|
1533
|
+
var DASHBOARD_TRADE_EXECUTE_AUTH_KEY_FIELDS = [
|
|
1534
|
+
InternalAuthKeyFields.TRADE_EXECUTE,
|
|
1535
|
+
InternalAuthKeyFields.TRADE_INTERNAL_ALIAS,
|
|
1536
|
+
InternalAuthKeyFields.DEFAULT
|
|
1537
|
+
];
|
|
1538
|
+
var DASHBOARD_TELEGRAM_ALERT_AUTH_KEY_FIELDS = [
|
|
1539
|
+
InternalAuthKeyFields.TELEGRAM_ALERT,
|
|
1540
|
+
InternalAuthKeyFields.TELEGRAM_INTERNAL_ALIAS,
|
|
1541
|
+
InternalAuthKeyFields.DEFAULT
|
|
1542
|
+
];
|
|
1543
|
+
function resolveInternalAuthKey(env, keyFields) {
|
|
1544
|
+
return collectInternalAuthKeys(env, keyFields)[0];
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
class ServiceAuthError extends Error {
|
|
1548
|
+
constructor(message) {
|
|
1549
|
+
super(message);
|
|
1550
|
+
this.name = "ServiceAuthError";
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
function authenticatedServiceFetch(binding, env, path, body, options) {
|
|
1554
|
+
const key = options?.internalKey ?? (options?.internalKeyFields ? resolveInternalAuthKey(env, options.internalKeyFields) : env.INTERNAL_KEY_BINDING);
|
|
1555
|
+
if (!key) {
|
|
1556
|
+
const label = options?.internalKeyFields ? Array.isArray(options.internalKeyFields) ? options.internalKeyFields.join(" | ") : options.internalKeyFields : "INTERNAL_KEY_BINDING";
|
|
1557
|
+
return Promise.reject(new ServiceAuthError(`${label} not configured`));
|
|
1558
|
+
}
|
|
1559
|
+
return serviceFetch(binding, path, body, {
|
|
1560
|
+
method: options?.method,
|
|
1561
|
+
timeout: options?.timeout,
|
|
1562
|
+
headers: {
|
|
1563
|
+
...options?.headers,
|
|
1564
|
+
"X-Internal-Auth-Key": key
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
995
1568
|
function serviceFetch(binding, path, body, options) {
|
|
1569
|
+
const timeout = options?.timeout ?? 30000;
|
|
996
1570
|
return binding.fetch(`http://internal${path}`, {
|
|
997
1571
|
method: options?.method ?? "POST",
|
|
998
1572
|
headers: {
|
|
999
1573
|
"Content-Type": "application/json",
|
|
1000
1574
|
...options?.headers
|
|
1001
1575
|
},
|
|
1002
|
-
body: body != null ? JSON.stringify(body) : undefined
|
|
1576
|
+
body: body != null ? JSON.stringify(body) : undefined,
|
|
1577
|
+
signal: AbortSignal.timeout(timeout)
|
|
1003
1578
|
});
|
|
1004
1579
|
}
|
|
1005
1580
|
|
|
1006
|
-
// src/
|
|
1581
|
+
// src/test-utils.ts
|
|
1582
|
+
function createMockKV(overrides) {
|
|
1583
|
+
return {
|
|
1584
|
+
get: async (_key) => null,
|
|
1585
|
+
put: async (_key, _value, _options) => {},
|
|
1586
|
+
delete: async (_key) => {},
|
|
1587
|
+
list: async (_options) => ({
|
|
1588
|
+
keys: []
|
|
1589
|
+
}),
|
|
1590
|
+
...overrides
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
function createMockR2(overrides) {
|
|
1594
|
+
return {
|
|
1595
|
+
put: async (key, _value, _options) => ({
|
|
1596
|
+
key
|
|
1597
|
+
}),
|
|
1598
|
+
get: async (_key) => null,
|
|
1599
|
+
delete: async (_key) => {},
|
|
1600
|
+
list: async (_options) => ({
|
|
1601
|
+
objects: []
|
|
1602
|
+
}),
|
|
1603
|
+
...overrides
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
function createMockPreparedStatement() {
|
|
1607
|
+
return {
|
|
1608
|
+
bind: (..._params) => createMockPreparedStatement(),
|
|
1609
|
+
run: async () => ({ success: true }),
|
|
1610
|
+
all: async () => ({ success: true, results: [] }),
|
|
1611
|
+
first: async () => null
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
function createMockD1(overrides) {
|
|
1615
|
+
return {
|
|
1616
|
+
prepare: (_sql) => createMockPreparedStatement(),
|
|
1617
|
+
dump: async () => new ArrayBuffer(0),
|
|
1618
|
+
batch: async (_statements) => [],
|
|
1619
|
+
exec: async (_sql) => ({ success: true }),
|
|
1620
|
+
...overrides
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
function createMockFetcher(response) {
|
|
1624
|
+
const defaultResponse = response ?? new Response("OK", { status: 200 });
|
|
1625
|
+
return {
|
|
1626
|
+
fetch: async (_request, _init) => defaultResponse
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
function createMockAnalyticsEngine() {
|
|
1630
|
+
return {
|
|
1631
|
+
writeDataPoint: (_data) => {}
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
function createMockAnalyticsEngineDataset() {
|
|
1635
|
+
return createMockAnalyticsEngine();
|
|
1636
|
+
}
|
|
1637
|
+
function createMockQueue() {
|
|
1638
|
+
return {
|
|
1639
|
+
send: async (_body, _options) => {},
|
|
1640
|
+
sendBatch: async (_messages) => {}
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
function createMockVectorizeIndex(overrides) {
|
|
1644
|
+
return {
|
|
1645
|
+
query: async (_vector, _options) => ({
|
|
1646
|
+
matches: []
|
|
1647
|
+
}),
|
|
1648
|
+
upsert: async (_vectors) => {},
|
|
1649
|
+
deleteByIds: async (_ids) => {},
|
|
1650
|
+
describe: async () => ({ dimension: 384, totalVectors: 0 }),
|
|
1651
|
+
...overrides
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
function createMockAi(overrides) {
|
|
1655
|
+
return {
|
|
1656
|
+
run: async (_model, _input) => ({}),
|
|
1657
|
+
...overrides
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
function createMockExecutionContext(overrides) {
|
|
1661
|
+
return {
|
|
1662
|
+
waitUntil: (_promise) => {},
|
|
1663
|
+
passThroughOnException: () => {},
|
|
1664
|
+
...overrides
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
function createMockSecretsStore(overrides) {
|
|
1668
|
+
return {
|
|
1669
|
+
get: async (_key) => null,
|
|
1670
|
+
put: async (_key, _value) => {},
|
|
1671
|
+
delete: async (_key) => {},
|
|
1672
|
+
...overrides
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
function createMockEnv(overrides) {
|
|
1676
|
+
return overrides;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// src/path-utils.ts
|
|
1680
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1007
1681
|
import { homedir as homedir2 } from "os";
|
|
1008
|
-
import { join as join2 } from "path";
|
|
1009
|
-
|
|
1682
|
+
import { dirname, join as join2, resolve } from "path";
|
|
1683
|
+
function createHooxPath(path) {
|
|
1684
|
+
return path;
|
|
1685
|
+
}
|
|
1686
|
+
function getHooxHome() {
|
|
1687
|
+
try {
|
|
1688
|
+
const override = process.env.HOOX_HOME?.trim();
|
|
1689
|
+
if (override) {
|
|
1690
|
+
return createHooxPath(resolve(override));
|
|
1691
|
+
}
|
|
1692
|
+
const home = homedir2();
|
|
1693
|
+
if (!home || home.length === 0) {
|
|
1694
|
+
return createHooxPath(resolve(process.cwd(), ".hoox"));
|
|
1695
|
+
}
|
|
1696
|
+
return createHooxPath(join2(home, ".hoox"));
|
|
1697
|
+
} catch {
|
|
1698
|
+
return createHooxPath(resolve(process.cwd(), ".hoox"));
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
function isHooxSetupRoot(dir) {
|
|
1702
|
+
if (!dir)
|
|
1703
|
+
return false;
|
|
1704
|
+
try {
|
|
1705
|
+
const root = resolve(dir);
|
|
1706
|
+
return existsSync2(join2(root, "wrangler.jsonc")) && existsSync2(join2(root, "packages", "cli", "package.json"));
|
|
1707
|
+
} catch {
|
|
1708
|
+
return false;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
function findHooxSetupRoot(startDir = process.cwd()) {
|
|
1712
|
+
let dir = resolve(startDir);
|
|
1713
|
+
for (;; ) {
|
|
1714
|
+
if (isHooxSetupRoot(dir))
|
|
1715
|
+
return dir;
|
|
1716
|
+
const parent = dirname(dir);
|
|
1717
|
+
if (parent === dir)
|
|
1718
|
+
return null;
|
|
1719
|
+
dir = parent;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
function resolveHooxRuntimeRoot(options) {
|
|
1723
|
+
const env = options?.env ?? process.env;
|
|
1724
|
+
const cwd = resolve(options?.cwd ?? process.cwd());
|
|
1725
|
+
const globalRepo = getHooxRepoPath();
|
|
1726
|
+
const envRepo = env.HOOX_REPO?.trim();
|
|
1727
|
+
if (envRepo) {
|
|
1728
|
+
const resolved = resolve(envRepo);
|
|
1729
|
+
if (isHooxSetupRoot(resolved)) {
|
|
1730
|
+
return {
|
|
1731
|
+
root: resolved,
|
|
1732
|
+
source: "env",
|
|
1733
|
+
checked: {
|
|
1734
|
+
env: resolved,
|
|
1735
|
+
cwd: findHooxSetupRoot(cwd),
|
|
1736
|
+
global: globalRepo
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
return {
|
|
1741
|
+
root: null,
|
|
1742
|
+
source: "env",
|
|
1743
|
+
checked: {
|
|
1744
|
+
env: resolved,
|
|
1745
|
+
cwd: findHooxSetupRoot(cwd),
|
|
1746
|
+
global: globalRepo
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
const local = findHooxSetupRoot(cwd);
|
|
1751
|
+
if (local) {
|
|
1752
|
+
return {
|
|
1753
|
+
root: local,
|
|
1754
|
+
source: "cwd",
|
|
1755
|
+
checked: { cwd: local, global: globalRepo }
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
if (isHooxSetupRoot(globalRepo)) {
|
|
1759
|
+
return {
|
|
1760
|
+
root: globalRepo,
|
|
1761
|
+
source: "global",
|
|
1762
|
+
checked: { cwd: null, global: globalRepo }
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
return {
|
|
1766
|
+
root: null,
|
|
1767
|
+
source: "none",
|
|
1768
|
+
checked: { cwd: null, global: globalRepo }
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
function getTuiEntryCandidates(runtimeRoot) {
|
|
1772
|
+
const root = resolve(runtimeRoot);
|
|
1773
|
+
return [
|
|
1774
|
+
join2(root, "packages", "tui", "src", "main.tsx"),
|
|
1775
|
+
join2(root, "packages", "tui", "dist", "main.js"),
|
|
1776
|
+
join2(root, "packages", "tui", "src", "main.ts")
|
|
1777
|
+
];
|
|
1778
|
+
}
|
|
1779
|
+
function resolveHooxPath(relativePath) {
|
|
1780
|
+
if (!relativePath || typeof relativePath !== "string") {
|
|
1781
|
+
throw new Error("relativePath must be a non-empty string");
|
|
1782
|
+
}
|
|
1783
|
+
if (relativePath.includes("..")) {
|
|
1784
|
+
throw new Error(`Path traversal detected in relativePath: "${relativePath}"`);
|
|
1785
|
+
}
|
|
1786
|
+
const normalizedPath = relativePath.replace(/\\/g, "/");
|
|
1787
|
+
const hooxHome = getHooxHome();
|
|
1788
|
+
const fullPath = join2(hooxHome, normalizedPath);
|
|
1789
|
+
const resolvedPath = resolve(fullPath);
|
|
1790
|
+
const resolvedHome = resolve(hooxHome);
|
|
1791
|
+
if (!resolvedPath.startsWith(resolvedHome)) {
|
|
1792
|
+
throw new Error(`Resolved path "${resolvedPath}" is outside Hoox home directory`);
|
|
1793
|
+
}
|
|
1794
|
+
return createHooxPath(resolvedPath);
|
|
1795
|
+
}
|
|
1796
|
+
function isWithinHooxHome(path) {
|
|
1797
|
+
try {
|
|
1798
|
+
const hooxHome = getHooxHome();
|
|
1799
|
+
const resolvedPath = resolve(path);
|
|
1800
|
+
const resolvedHome = resolve(hooxHome);
|
|
1801
|
+
return resolvedPath.startsWith(resolvedHome);
|
|
1802
|
+
} catch {
|
|
1803
|
+
return false;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
function getRelativeHooxPath(absolutePath) {
|
|
1807
|
+
try {
|
|
1808
|
+
if (!isWithinHooxHome(absolutePath)) {
|
|
1809
|
+
return null;
|
|
1810
|
+
}
|
|
1811
|
+
const hooxHome = getHooxHome();
|
|
1812
|
+
const resolvedPath = resolve(absolutePath);
|
|
1813
|
+
const resolvedHome = resolve(hooxHome);
|
|
1814
|
+
const relativePath = resolvedPath.slice(resolvedHome.length);
|
|
1815
|
+
return relativePath.startsWith("/") || relativePath.startsWith("\\") ? relativePath.slice(1) : relativePath;
|
|
1816
|
+
} catch {
|
|
1817
|
+
return null;
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
function getHooxRepoPath() {
|
|
1821
|
+
return resolveHooxPath("repo");
|
|
1822
|
+
}
|
|
1823
|
+
function getHooxConfigDir() {
|
|
1824
|
+
return resolveHooxPath("config");
|
|
1825
|
+
}
|
|
1826
|
+
function getHooxDataDir() {
|
|
1827
|
+
return resolveHooxPath("data");
|
|
1828
|
+
}
|
|
1829
|
+
function getHooxWranglerPath() {
|
|
1830
|
+
return resolveHooxPath("config/wrangler.jsonc");
|
|
1831
|
+
}
|
|
1832
|
+
function getHooxStatePath() {
|
|
1833
|
+
return resolveHooxPath("data/state.json");
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// src/session.ts
|
|
1837
|
+
import { z as z3 } from "zod";
|
|
1838
|
+
var SessionStateFileSchema = z3.object({
|
|
1839
|
+
activeView: z3.string().optional(),
|
|
1840
|
+
sidebarExpanded: z3.boolean().optional(),
|
|
1841
|
+
windowSize: z3.object({
|
|
1842
|
+
cols: z3.number().int().positive(),
|
|
1843
|
+
rows: z3.number().int().positive()
|
|
1844
|
+
}).optional(),
|
|
1845
|
+
lastData: z3.number().nonnegative().optional(),
|
|
1846
|
+
savedAt: z3.string().optional()
|
|
1847
|
+
}).strict();
|
|
1848
|
+
var TUI_STATE_DIR = ".tui-state";
|
|
1849
|
+
var SESSION_FILE = (() => {
|
|
1850
|
+
try {
|
|
1851
|
+
const hooxHome = getHooxHome();
|
|
1852
|
+
return `${hooxHome}/${TUI_STATE_DIR}/session.json`;
|
|
1853
|
+
} catch {
|
|
1854
|
+
return `${process.cwd()}/${TUI_STATE_DIR}/session.json`;
|
|
1855
|
+
}
|
|
1856
|
+
})();
|
|
1010
1857
|
var DEFAULT_SESSION = {
|
|
1011
1858
|
activeView: "dashboard",
|
|
1012
1859
|
sidebarExpanded: true,
|
|
@@ -1035,7 +1882,8 @@ async function restoreSession() {
|
|
|
1035
1882
|
if (!exists)
|
|
1036
1883
|
return { ...DEFAULT_SESSION };
|
|
1037
1884
|
const raw = await file.text();
|
|
1038
|
-
const
|
|
1885
|
+
const result = SessionStateFileSchema.safeParse(JSON.parse(raw));
|
|
1886
|
+
const parsed = result.success ? result.data : {};
|
|
1039
1887
|
return {
|
|
1040
1888
|
activeView: validateViewId(parsed.activeView) ? parsed.activeView : DEFAULT_SESSION.activeView,
|
|
1041
1889
|
sidebarExpanded: typeof parsed.sidebarExpanded === "boolean" ? parsed.sidebarExpanded : DEFAULT_SESSION.sidebarExpanded,
|
|
@@ -1223,7 +2071,7 @@ var BASE_WORKERS = {
|
|
|
1223
2071
|
path: "workers/d1-worker",
|
|
1224
2072
|
vars: { database_name: "hoox-db" }
|
|
1225
2073
|
},
|
|
1226
|
-
hoox: { enabled: true, path: "workers/hoox", vars: {} },
|
|
2074
|
+
hoox: { enabled: true, path: "workers/hoox-worker", vars: {} },
|
|
1227
2075
|
"agent-worker": { enabled: true, path: "workers/agent-worker", vars: {} },
|
|
1228
2076
|
"analytics-worker": {
|
|
1229
2077
|
enabled: true,
|
|
@@ -1543,6 +2391,7 @@ class WizardEngine {
|
|
|
1543
2391
|
}
|
|
1544
2392
|
}
|
|
1545
2393
|
}
|
|
2394
|
+
|
|
1546
2395
|
// src/wizard/persistence.ts
|
|
1547
2396
|
function serializeState(state) {
|
|
1548
2397
|
return JSON.stringify(state, null, 2);
|
|
@@ -1573,7 +2422,7 @@ function deriveCalledBy(manifests) {
|
|
|
1573
2422
|
var manifests = {
|
|
1574
2423
|
hoox: {
|
|
1575
2424
|
name: "hoox",
|
|
1576
|
-
path: "workers/hoox",
|
|
2425
|
+
path: "workers/hoox-worker",
|
|
1577
2426
|
vars: {
|
|
1578
2427
|
WEBHOOK_API_KEY_BINDING: {
|
|
1579
2428
|
type: "secret",
|
|
@@ -1917,6 +2766,7 @@ var manifests = {
|
|
|
1917
2766
|
var CALLED_BY = deriveCalledBy(manifests);
|
|
1918
2767
|
var WORKER_MANIFESTS = manifests;
|
|
1919
2768
|
var WORKER_NAMES = Object.keys(manifests).sort();
|
|
2769
|
+
|
|
1920
2770
|
// src/schemas/validators.ts
|
|
1921
2771
|
import { parse } from "jsonc-parser";
|
|
1922
2772
|
function validateWranglerJsonc(workerName, manifest, jsoncContent) {
|
|
@@ -1942,7 +2792,8 @@ function validateWranglerJsonc(workerName, manifest, jsoncContent) {
|
|
|
1942
2792
|
});
|
|
1943
2793
|
return errors;
|
|
1944
2794
|
}
|
|
1945
|
-
const
|
|
2795
|
+
const wrangler = parsed;
|
|
2796
|
+
const declaredVars = wrangler.vars ?? {};
|
|
1946
2797
|
for (const [name, def] of Object.entries(manifest.vars)) {
|
|
1947
2798
|
if (!(name in declaredVars)) {
|
|
1948
2799
|
errors.push({
|
|
@@ -1963,7 +2814,7 @@ function validateWranglerJsonc(workerName, manifest, jsoncContent) {
|
|
|
1963
2814
|
});
|
|
1964
2815
|
}
|
|
1965
2816
|
}
|
|
1966
|
-
const declaredServices =
|
|
2817
|
+
const declaredServices = wrangler.services ?? [];
|
|
1967
2818
|
for (const expected of manifest.services) {
|
|
1968
2819
|
const match = declaredServices.find((s) => s.binding === expected.binding && s.service === expected.service);
|
|
1969
2820
|
if (!match) {
|
|
@@ -2019,7 +2870,8 @@ function validateRootSecrets(workerName, manifest, rootJsoncContent) {
|
|
|
2019
2870
|
});
|
|
2020
2871
|
return errors;
|
|
2021
2872
|
}
|
|
2022
|
-
const
|
|
2873
|
+
const rootWrangler = parsed;
|
|
2874
|
+
const rootSecrets = rootWrangler.workers?.[workerName]?.secrets ?? [];
|
|
2023
2875
|
const expectedSecrets = Object.entries(manifest.vars).filter(([_, def]) => def.type === "secret").map(([name]) => name);
|
|
2024
2876
|
for (const expected of expectedSecrets) {
|
|
2025
2877
|
if (!rootSecrets.includes(expected)) {
|
|
@@ -2226,6 +3078,13 @@ var useUIStore = create()(immer((set) => ({
|
|
|
2226
3078
|
// src/stores/service-store.ts
|
|
2227
3079
|
import { create as create2 } from "zustand";
|
|
2228
3080
|
import { immer as immer2 } from "zustand/middleware/immer";
|
|
3081
|
+
function summarizeCliError(details) {
|
|
3082
|
+
const raw = details.stderr.trim() || details.stdout.trim() || details.command || "Unknown CLI error";
|
|
3083
|
+
const firstLine = raw.split(`
|
|
3084
|
+
`, 1)[0] ?? "";
|
|
3085
|
+
const trimmed = firstLine.slice(0, 120).trimEnd();
|
|
3086
|
+
return trimmed.length > 0 ? trimmed : details.command;
|
|
3087
|
+
}
|
|
2229
3088
|
var BACKOFF_SEQUENCE = [1000, 2000, 4000, 8000, 16000];
|
|
2230
3089
|
var MAX_RETRIES2 = 5;
|
|
2231
3090
|
function ringPush(arr, item, max) {
|
|
@@ -2246,6 +3105,7 @@ var initialState2 = {
|
|
|
2246
3105
|
selectedWorkerId: null,
|
|
2247
3106
|
retryCount: 0,
|
|
2248
3107
|
lastError: null,
|
|
3108
|
+
lastErrorDetails: null,
|
|
2249
3109
|
lastSuccessfulFetch: 0,
|
|
2250
3110
|
reconnectDelay: 0,
|
|
2251
3111
|
disconnectedAt: null
|
|
@@ -2261,7 +3121,7 @@ var useServiceStore = create2()(immer2((set, get) => ({
|
|
|
2261
3121
|
fetchWorkers: async () => {
|
|
2262
3122
|
const { hooxFetch: hooxFetch2 } = await Promise.resolve().then(() => (init_api_client(), exports_api_client));
|
|
2263
3123
|
try {
|
|
2264
|
-
const data = await hooxFetch2("/workers");
|
|
3124
|
+
const data = await hooxFetch2("/v1/workers");
|
|
2265
3125
|
set((state) => {
|
|
2266
3126
|
state.workers = data;
|
|
2267
3127
|
state.lastUpdated = Date.now();
|
|
@@ -2270,27 +3130,42 @@ var useServiceStore = create2()(immer2((set, get) => ({
|
|
|
2270
3130
|
state.connectionStatus = "connected";
|
|
2271
3131
|
state.retryCount = 0;
|
|
2272
3132
|
state.lastError = null;
|
|
3133
|
+
state.lastErrorDetails = null;
|
|
2273
3134
|
state.reconnectDelay = 0;
|
|
2274
3135
|
state.disconnectedAt = null;
|
|
2275
3136
|
}
|
|
2276
3137
|
});
|
|
2277
3138
|
} catch (error) {
|
|
2278
3139
|
const msg = error instanceof Error ? error.message : "Unknown fetch error";
|
|
3140
|
+
get().addAlert({
|
|
3141
|
+
id: crypto.randomUUID(),
|
|
3142
|
+
type: "connection",
|
|
3143
|
+
severity: "error",
|
|
3144
|
+
message: msg,
|
|
3145
|
+
timestamp: Date.now(),
|
|
3146
|
+
acknowledged: false
|
|
3147
|
+
});
|
|
2279
3148
|
set((state) => {
|
|
3149
|
+
state.lastError = msg;
|
|
3150
|
+
state.disconnectedAt = state.disconnectedAt ?? Date.now();
|
|
2280
3151
|
if (state.connectionStatus === "connected" || state.connectionStatus === "polling") {
|
|
2281
3152
|
state.connectionStatus = "reconnecting";
|
|
2282
3153
|
state.retryCount = state.retryCount + 1;
|
|
2283
|
-
state.lastError = msg;
|
|
2284
|
-
state.disconnectedAt = state.disconnectedAt ?? Date.now();
|
|
2285
3154
|
if (state.retryCount >= MAX_RETRIES2) {
|
|
2286
3155
|
state.connectionStatus = "offline";
|
|
2287
3156
|
state.lastError = `Connection lost after ${MAX_RETRIES2} retries`;
|
|
2288
3157
|
} else {
|
|
2289
3158
|
state.reconnectDelay = getBackoffDelay(state.retryCount);
|
|
2290
3159
|
}
|
|
2291
|
-
}
|
|
2292
|
-
|
|
2293
|
-
state.
|
|
3160
|
+
} else if (state.connectionStatus === "reconnecting") {
|
|
3161
|
+
state.retryCount = state.retryCount + 1;
|
|
3162
|
+
if (state.retryCount >= MAX_RETRIES2) {
|
|
3163
|
+
state.connectionStatus = "offline";
|
|
3164
|
+
state.lastError = `Connection lost after ${MAX_RETRIES2} retries`;
|
|
3165
|
+
state.reconnectDelay = 0;
|
|
3166
|
+
} else {
|
|
3167
|
+
state.reconnectDelay = getBackoffDelay(state.retryCount);
|
|
3168
|
+
}
|
|
2294
3169
|
}
|
|
2295
3170
|
});
|
|
2296
3171
|
}
|
|
@@ -2298,18 +3173,40 @@ var useServiceStore = create2()(immer2((set, get) => ({
|
|
|
2298
3173
|
streamTrades: async () => {
|
|
2299
3174
|
const { subscribeSSE: subscribeSSE2 } = await Promise.resolve().then(() => (init_sse(), exports_sse));
|
|
2300
3175
|
try {
|
|
2301
|
-
await subscribeSSE2("/trades/stream", (trade) => {
|
|
3176
|
+
await subscribeSSE2("/v1/trades/stream", (trade) => {
|
|
2302
3177
|
get().pushTrade(trade);
|
|
2303
3178
|
});
|
|
2304
|
-
} catch {
|
|
3179
|
+
} catch (error) {
|
|
3180
|
+
const msg = error instanceof Error ? error.message : "SSE trade stream failed";
|
|
3181
|
+
get().addAlert({
|
|
3182
|
+
id: crypto.randomUUID(),
|
|
3183
|
+
type: "connection",
|
|
3184
|
+
severity: "error",
|
|
3185
|
+
message: msg,
|
|
3186
|
+
timestamp: Date.now(),
|
|
3187
|
+
acknowledged: false
|
|
3188
|
+
});
|
|
3189
|
+
get().handleConnectionFailure(msg);
|
|
3190
|
+
}
|
|
2305
3191
|
},
|
|
2306
3192
|
streamLogs: async () => {
|
|
2307
3193
|
const { subscribeSSE: subscribeSSE2 } = await Promise.resolve().then(() => (init_sse(), exports_sse));
|
|
2308
3194
|
try {
|
|
2309
|
-
await subscribeSSE2("/logs/stream", (log) => {
|
|
3195
|
+
await subscribeSSE2("/v1/logs/stream", (log) => {
|
|
2310
3196
|
get().pushLog(log);
|
|
2311
3197
|
});
|
|
2312
|
-
} catch {
|
|
3198
|
+
} catch (error) {
|
|
3199
|
+
const msg = error instanceof Error ? error.message : "SSE log stream failed";
|
|
3200
|
+
get().addAlert({
|
|
3201
|
+
id: crypto.randomUUID(),
|
|
3202
|
+
type: "connection",
|
|
3203
|
+
severity: "error",
|
|
3204
|
+
message: msg,
|
|
3205
|
+
timestamp: Date.now(),
|
|
3206
|
+
acknowledged: false
|
|
3207
|
+
});
|
|
3208
|
+
get().handleConnectionFailure(msg);
|
|
3209
|
+
}
|
|
2313
3210
|
},
|
|
2314
3211
|
addAlert: (alert) => set((state) => {
|
|
2315
3212
|
state.alerts = ringPush(state.alerts, alert, MAX_ALERTS);
|
|
@@ -2322,6 +3219,7 @@ var useServiceStore = create2()(immer2((set, get) => ({
|
|
|
2322
3219
|
if (status === "connected") {
|
|
2323
3220
|
state.retryCount = 0;
|
|
2324
3221
|
state.lastError = null;
|
|
3222
|
+
state.lastErrorDetails = null;
|
|
2325
3223
|
state.reconnectDelay = 0;
|
|
2326
3224
|
state.disconnectedAt = null;
|
|
2327
3225
|
}
|
|
@@ -2351,11 +3249,10 @@ var useServiceStore = create2()(immer2((set, get) => ({
|
|
|
2351
3249
|
state.lastSuccessfulFetch = Date.now();
|
|
2352
3250
|
state.retryCount = 0;
|
|
2353
3251
|
state.lastError = null;
|
|
3252
|
+
state.lastErrorDetails = null;
|
|
2354
3253
|
state.reconnectDelay = 0;
|
|
2355
3254
|
state.disconnectedAt = null;
|
|
2356
|
-
|
|
2357
|
-
state.connectionStatus = "connected";
|
|
2358
|
-
}
|
|
3255
|
+
state.connectionStatus = "connected";
|
|
2359
3256
|
}),
|
|
2360
3257
|
handleConnectionFailure: (errorMessage) => set((state) => {
|
|
2361
3258
|
state.lastError = errorMessage;
|
|
@@ -2378,6 +3275,7 @@ var useServiceStore = create2()(immer2((set, get) => ({
|
|
|
2378
3275
|
resetRetries: () => set((state) => {
|
|
2379
3276
|
state.retryCount = 0;
|
|
2380
3277
|
state.lastError = null;
|
|
3278
|
+
state.lastErrorDetails = null;
|
|
2381
3279
|
state.reconnectDelay = 0;
|
|
2382
3280
|
state.disconnectedAt = null;
|
|
2383
3281
|
}),
|
|
@@ -2386,8 +3284,30 @@ var useServiceStore = create2()(immer2((set, get) => ({
|
|
|
2386
3284
|
state.connectionStatus = "polling";
|
|
2387
3285
|
state.retryCount = 0;
|
|
2388
3286
|
state.lastError = null;
|
|
3287
|
+
state.lastErrorDetails = null;
|
|
2389
3288
|
state.reconnectDelay = 0;
|
|
2390
3289
|
}
|
|
3290
|
+
}),
|
|
3291
|
+
setLastErrorDetails: (details) => set((state) => {
|
|
3292
|
+
state.lastErrorDetails = details;
|
|
3293
|
+
state.lastError = details === null ? null : summarizeCliError(details);
|
|
3294
|
+
}),
|
|
3295
|
+
clearError: () => set((state) => {
|
|
3296
|
+
state.lastError = null;
|
|
3297
|
+
state.lastErrorDetails = null;
|
|
3298
|
+
}),
|
|
3299
|
+
addCliErrorAlert: (details) => set((state) => {
|
|
3300
|
+
state.lastErrorDetails = details;
|
|
3301
|
+
state.lastError = summarizeCliError(details);
|
|
3302
|
+
const summary = summarizeCliError(details);
|
|
3303
|
+
state.alerts = ringPush(state.alerts, {
|
|
3304
|
+
id: crypto.randomUUID(),
|
|
3305
|
+
type: "connection",
|
|
3306
|
+
severity: "error",
|
|
3307
|
+
message: `CLI failure: ${summary}`,
|
|
3308
|
+
timestamp: Date.now(),
|
|
3309
|
+
acknowledged: false
|
|
3310
|
+
}, MAX_ALERTS);
|
|
2391
3311
|
})
|
|
2392
3312
|
})));
|
|
2393
3313
|
|
|
@@ -2395,6 +3315,7 @@ var useServiceStore = create2()(immer2((set, get) => ({
|
|
|
2395
3315
|
import { create as create3 } from "zustand";
|
|
2396
3316
|
import { immer as immer3 } from "zustand/middleware/immer";
|
|
2397
3317
|
import { persist, createJSONStorage } from "zustand/middleware";
|
|
3318
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
|
|
2398
3319
|
import { homedir as homedir3 } from "os";
|
|
2399
3320
|
import { join as join3 } from "path";
|
|
2400
3321
|
var bunConfigStorage = {
|
|
@@ -2411,11 +3332,17 @@ var bunConfigStorage = {
|
|
|
2411
3332
|
},
|
|
2412
3333
|
setItem: async (_name, value) => {
|
|
2413
3334
|
const dir = join3(homedir3(), ".hoox");
|
|
3335
|
+
if (!existsSync3(dir)) {
|
|
3336
|
+
mkdirSync2(dir, { recursive: true, mode: HOOX_DIR_MODE });
|
|
3337
|
+
}
|
|
2414
3338
|
try {
|
|
2415
|
-
|
|
3339
|
+
chmodSync2(dir, HOOX_DIR_MODE);
|
|
2416
3340
|
} catch {}
|
|
2417
3341
|
const filePath = join3(dir, "config.json");
|
|
2418
3342
|
await Bun.write(filePath, value);
|
|
3343
|
+
try {
|
|
3344
|
+
chmodSync2(filePath, HOOX_CONFIG_FILE_MODE);
|
|
3345
|
+
} catch {}
|
|
2419
3346
|
},
|
|
2420
3347
|
removeItem: async (_name) => {}
|
|
2421
3348
|
};
|
|
@@ -2507,7 +3434,9 @@ var useConfigStore = create3()(persist(immer3((set) => ({
|
|
|
2507
3434
|
// src/index.ts
|
|
2508
3435
|
init_api_client();
|
|
2509
3436
|
init_sse();
|
|
3437
|
+
init_operator_transport();
|
|
2510
3438
|
export {
|
|
3439
|
+
writeSecureFileSync,
|
|
2511
3440
|
writeConfigSync,
|
|
2512
3441
|
writeConfig,
|
|
2513
3442
|
validateWranglerJsonc,
|
|
@@ -2525,13 +3454,31 @@ export {
|
|
|
2525
3454
|
serializeState,
|
|
2526
3455
|
saveSession,
|
|
2527
3456
|
restoreSession,
|
|
3457
|
+
resolveOperatorTransportProfile,
|
|
3458
|
+
resolveInternalAuthKey,
|
|
3459
|
+
resolveHooxRuntimeRoot,
|
|
3460
|
+
resolveHooxPath,
|
|
2528
3461
|
resolveDependencies,
|
|
2529
3462
|
readConfigSync,
|
|
2530
3463
|
readConfig,
|
|
3464
|
+
operatorUrl,
|
|
2531
3465
|
logKvTimestamp,
|
|
3466
|
+
isWithinHooxHome,
|
|
3467
|
+
isUnixModeGroupOrWorldAccessible,
|
|
3468
|
+
isHooxSetupRoot,
|
|
3469
|
+
isConfigWorldOrGroupReadable,
|
|
2532
3470
|
hooxFetch,
|
|
2533
3471
|
healthCheck,
|
|
2534
3472
|
headersToObject,
|
|
3473
|
+
hasOperatorClientCredentials,
|
|
3474
|
+
getTuiEntryCandidates,
|
|
3475
|
+
getRelativeHooxPath,
|
|
3476
|
+
getHooxWranglerPath,
|
|
3477
|
+
getHooxStatePath,
|
|
3478
|
+
getHooxRepoPath,
|
|
3479
|
+
getHooxHome,
|
|
3480
|
+
getHooxDataDir,
|
|
3481
|
+
getHooxConfigDir,
|
|
2535
3482
|
generateWranglerJsonc,
|
|
2536
3483
|
generateDevVars,
|
|
2537
3484
|
formatUptime,
|
|
@@ -2547,11 +3494,31 @@ export {
|
|
|
2547
3494
|
formatDuration2 as formatDuration,
|
|
2548
3495
|
formatCurrency,
|
|
2549
3496
|
formatCpu,
|
|
3497
|
+
formatConfigPermissionWarning,
|
|
2550
3498
|
formatCompactCurrency,
|
|
3499
|
+
findHooxSetupRoot,
|
|
3500
|
+
ensureHooxDirSecure,
|
|
2551
3501
|
deserializeState,
|
|
2552
3502
|
createSuccessResponse,
|
|
3503
|
+
createQueueHandler,
|
|
3504
|
+
createMockVectorizeIndex,
|
|
3505
|
+
createMockSecretsStore,
|
|
3506
|
+
createMockR2,
|
|
3507
|
+
createMockQueue,
|
|
3508
|
+
createMockKV,
|
|
3509
|
+
createMockFetcher,
|
|
3510
|
+
createMockExecutionContext,
|
|
3511
|
+
createMockEnv,
|
|
3512
|
+
createMockD1,
|
|
3513
|
+
createMockAnalyticsEngineDataset,
|
|
3514
|
+
createMockAnalyticsEngine,
|
|
3515
|
+
createMockAi,
|
|
2553
3516
|
createJsonResponse,
|
|
2554
3517
|
createErrorResponse,
|
|
3518
|
+
createCronHandler,
|
|
3519
|
+
buildOperatorAuthHeaders,
|
|
3520
|
+
authenticatedServiceFetch,
|
|
3521
|
+
WorkerStatusColor,
|
|
2555
3522
|
WorkerAPIError,
|
|
2556
3523
|
WizardEngine,
|
|
2557
3524
|
WebhookPayloadSchema,
|
|
@@ -2559,23 +3526,41 @@ export {
|
|
|
2559
3526
|
WORKER_MANIFESTS,
|
|
2560
3527
|
WORKER_DEPENDENCIES,
|
|
2561
3528
|
WIZARD_STATE_PATH,
|
|
3529
|
+
WALLET_EXECUTE_AUTH_KEY_FIELDS,
|
|
2562
3530
|
TradeSignalSchema,
|
|
3531
|
+
TradeQueueMessageSchema,
|
|
2563
3532
|
TradeActionSchema,
|
|
3533
|
+
TRADE_READ_AUTH_KEY_FIELDS,
|
|
3534
|
+
TRADE_EXECUTE_AUTH_KEY_FIELDS,
|
|
2564
3535
|
TRADEMARK_NOTICE,
|
|
2565
3536
|
TRADEMARKS,
|
|
3537
|
+
TELEGRAM_ALERT_AUTH_KEY_FIELDS,
|
|
3538
|
+
ServiceAuthError,
|
|
2566
3539
|
PositionSchema,
|
|
2567
3540
|
PRESETS,
|
|
3541
|
+
LogLevelColor,
|
|
2568
3542
|
KVKeys,
|
|
3543
|
+
InternalAuthKeyFields,
|
|
2569
3544
|
INTEGRATIONS,
|
|
3545
|
+
HOOX_DIR_MODE,
|
|
3546
|
+
HOOX_CONFIG_FILE_MODE,
|
|
2570
3547
|
FULL_LEGAL_NOTICE,
|
|
2571
3548
|
ExchangeRouter,
|
|
2572
3549
|
Errors,
|
|
2573
3550
|
DISCLAIMER_HEADER,
|
|
2574
3551
|
DISCLAIMER,
|
|
3552
|
+
DASHBOARD_TRADE_EXECUTE_AUTH_KEY_FIELDS,
|
|
3553
|
+
DASHBOARD_TELEGRAM_ALERT_AUTH_KEY_FIELDS,
|
|
3554
|
+
DASHBOARD_D1_READ_AUTH_KEY_FIELDS,
|
|
3555
|
+
D1_WRITE_AUTH_KEY_FIELDS,
|
|
3556
|
+
D1_READ_AUTH_KEY_FIELDS,
|
|
2575
3557
|
D1Repository,
|
|
3558
|
+
CoolBracketPalette,
|
|
3559
|
+
ConnectionStatusColor,
|
|
2576
3560
|
Colors,
|
|
2577
3561
|
COPYRIGHT,
|
|
2578
3562
|
CALLED_BY,
|
|
2579
|
-
BaseExchangeClient,
|
|
2580
|
-
BalanceSchema
|
|
3563
|
+
BaseExchangeClient2 as BaseExchangeClient,
|
|
3564
|
+
BalanceSchema,
|
|
3565
|
+
AlertSeverityColor
|
|
2581
3566
|
};
|