@hoox-sh/hoox-shared 1.4.0 → 1.5.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 +6 -0
- package/dist/agent-config-fields.js +21 -29
- package/dist/analytics.d.ts +3 -4
- package/dist/analytics.d.ts.map +1 -1
- package/dist/analytics.js +56 -16
- package/dist/api-client.js +12 -21
- package/dist/colors.js +5 -21
- package/dist/config.js +11 -27
- package/dist/cron-handler.js +0 -16
- package/dist/d1/index.js +8 -25
- package/dist/d1/repository.js +3 -19
- package/dist/d1/schemas.js +4 -20
- package/dist/dashboard-manifest.d.ts +2 -1
- package/dist/dashboard-manifest.d.ts.map +1 -1
- package/dist/dashboard-manifest.js +37 -33
- package/dist/entitlements.d.ts +46 -0
- package/dist/entitlements.d.ts.map +1 -0
- package/dist/entitlements.js +15 -0
- package/dist/errors.js +7 -23
- package/dist/exchange-client.js +2 -18
- package/dist/exchanges/base-exchange-client.js +0 -16
- package/dist/exchanges/index.js +0 -16
- package/dist/format-time.js +2 -18
- package/dist/formatters.js +12 -28
- package/dist/health.js +3 -19
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2211 -2098
- package/dist/kill-switch.js +12 -20
- package/dist/kvKeys.d.ts +8 -0
- package/dist/kvKeys.d.ts.map +1 -1
- package/dist/kvKeys.js +42 -46
- package/dist/kvUtils.js +8 -24
- package/dist/legal.js +5 -21
- package/dist/middleware/auth.js +9 -25
- package/dist/middleware/cors.js +4 -20
- package/dist/middleware/index.js +141 -163
- package/dist/middleware/logger.js +9 -25
- package/dist/middleware/rate-limit.js +2 -18
- package/dist/middleware/security-headers.js +2 -18
- package/dist/middleware/validate.js +5 -21
- package/dist/middleware/wait-until.js +2 -18
- package/dist/operator-transport.js +13 -17
- package/dist/path-utils.js +15 -31
- package/dist/queue-handler.js +0 -16
- package/dist/router.js +0 -16
- package/dist/schemas/index.js +35 -27
- package/dist/schemas/registry.d.ts.map +1 -1
- package/dist/schemas/registry.js +29 -20
- package/dist/schemas/validators.js +5 -21
- package/dist/service-bindings.js +13 -29
- package/dist/session.js +2 -18
- package/dist/sse.js +10 -18
- package/dist/stores/config-store.js +8 -22
- package/dist/stores/service-store.js +16 -29
- package/dist/stores/ui-store.js +0 -16
- package/dist/tenant.d.ts +61 -0
- package/dist/tenant.d.ts.map +1 -0
- package/dist/tenant.js +56 -0
- package/dist/test-utils.js +11 -27
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -24
- package/dist/wizard/engine.d.ts.map +1 -1
- package/dist/wizard/engine.js +21 -21
- package/dist/wizard/index.d.ts +1 -0
- package/dist/wizard/index.d.ts.map +1 -1
- package/dist/wizard/index.js +36 -33
- package/dist/wizard/persistence.d.ts +4 -0
- package/dist/wizard/persistence.d.ts.map +1 -1
- package/dist/wizard/persistence.js +7 -19
- package/dist/wizard/presets.d.ts.map +1 -1
- package/dist/wizard/presets.js +10 -22
- package/dist/wizard/types.d.ts +6 -0
- package/dist/wizard/types.d.ts.map +1 -1
- package/dist/wizard/types.js +5 -0
- package/package.json +7 -2
package/dist/index.js
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
configurable: true,
|
|
13
|
-
set: __exportSetter.bind(all, name)
|
|
14
|
-
});
|
|
2
|
+
var __esm = (fn, res, err) => () => {
|
|
3
|
+
if (fn)
|
|
4
|
+
try {
|
|
5
|
+
res = fn(fn = 0);
|
|
6
|
+
} catch (e) {
|
|
7
|
+
err = [e];
|
|
8
|
+
}
|
|
9
|
+
if (err)
|
|
10
|
+
throw err[0];
|
|
11
|
+
return res;
|
|
15
12
|
};
|
|
16
|
-
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
13
|
|
|
18
14
|
// src/operator-transport.ts
|
|
19
15
|
function stripTrailingSlashes(url) {
|
|
@@ -27,7 +23,7 @@ function parseTransport(raw) {
|
|
|
27
23
|
if (!raw)
|
|
28
24
|
return null;
|
|
29
25
|
const v = raw.trim().toLowerCase();
|
|
30
|
-
if (
|
|
26
|
+
if (VALID_TRANSPORTS2.has(v)) {
|
|
31
27
|
return v;
|
|
32
28
|
}
|
|
33
29
|
return null;
|
|
@@ -72,9 +68,9 @@ function operatorUrl(profile, path) {
|
|
|
72
68
|
const p = path.startsWith("/") ? path : `/${path}`;
|
|
73
69
|
return `${base}${p}`;
|
|
74
70
|
}
|
|
75
|
-
var DEFAULT_API_BASE = "http://localhost:8787",
|
|
71
|
+
var DEFAULT_API_BASE = "http://localhost:8787", VALID_TRANSPORTS2;
|
|
76
72
|
var init_operator_transport = __esm(() => {
|
|
77
|
-
|
|
73
|
+
VALID_TRANSPORTS2 = new Set([
|
|
78
74
|
"public",
|
|
79
75
|
"access",
|
|
80
76
|
"mtls",
|
|
@@ -83,11 +79,6 @@ var init_operator_transport = __esm(() => {
|
|
|
83
79
|
});
|
|
84
80
|
|
|
85
81
|
// src/api-client.ts
|
|
86
|
-
var exports_api_client = {};
|
|
87
|
-
__export(exports_api_client, {
|
|
88
|
-
hooxFetch: () => hooxFetch,
|
|
89
|
-
WorkerAPIError: () => WorkerAPIError
|
|
90
|
-
});
|
|
91
82
|
function isRetryableStatus(status) {
|
|
92
83
|
if (status === 401 || status === 403 || status === 429)
|
|
93
84
|
return false;
|
|
@@ -189,10 +180,6 @@ var init_api_client = __esm(() => {
|
|
|
189
180
|
});
|
|
190
181
|
|
|
191
182
|
// src/sse.ts
|
|
192
|
-
var exports_sse = {};
|
|
193
|
-
__export(exports_sse, {
|
|
194
|
-
subscribeSSE: () => subscribeSSE
|
|
195
|
-
});
|
|
196
183
|
function subscribeSSE(path, callback, onStatus, options) {
|
|
197
184
|
let aborted = false;
|
|
198
185
|
const profile = options?.transport ?? (options?.transportEnv ? resolveOperatorTransportProfile(options.transportEnv) : resolveOperatorTransportProfile());
|
|
@@ -258,6 +245,84 @@ var init_sse = __esm(() => {
|
|
|
258
245
|
init_operator_transport();
|
|
259
246
|
});
|
|
260
247
|
|
|
248
|
+
// src/legal.ts
|
|
249
|
+
var COPYRIGHT = `Copyright (C) ${new Date().getFullYear()} Hoox Team. All rights reserved.`;
|
|
250
|
+
var TRADEMARKS = [
|
|
251
|
+
"TradingView\xAE is a registered trademark of TradingView, Inc.",
|
|
252
|
+
"Cloudflare\xAE is a registered trademark of Cloudflare, Inc."
|
|
253
|
+
];
|
|
254
|
+
var TRADEMARK_NOTICE = TRADEMARKS.join(`
|
|
255
|
+
`);
|
|
256
|
+
var DISCLAIMER = "DISCLAIMER: This software is provided for educational and informational purposes only. " + "Trading cryptocurrencies and other financial instruments involves substantial risk of loss. " + "No guarantee of profits or protection against losses is made or implied. " + "Past performance does not indicate future results. Use at your own risk.";
|
|
257
|
+
var FULL_LEGAL_NOTICE = `${COPYRIGHT}
|
|
258
|
+
|
|
259
|
+
${TRADEMARK_NOTICE}
|
|
260
|
+
|
|
261
|
+
${DISCLAIMER}`;
|
|
262
|
+
var DISCLAIMER_HEADER = "X-Disclaimer";
|
|
263
|
+
// src/types.ts
|
|
264
|
+
import { z } from "zod";
|
|
265
|
+
var TradeActionSchema = z.enum([
|
|
266
|
+
"LONG",
|
|
267
|
+
"SHORT",
|
|
268
|
+
"CLOSE_LONG",
|
|
269
|
+
"CLOSE_SHORT"
|
|
270
|
+
]);
|
|
271
|
+
var MAX_WEBHOOK_LEVERAGE = 125;
|
|
272
|
+
var MAX_WEBHOOK_QUANTITY = 1e6;
|
|
273
|
+
var WebhookPayloadSchema = z.object({
|
|
274
|
+
exchange: z.string().min(1),
|
|
275
|
+
action: TradeActionSchema,
|
|
276
|
+
symbol: z.string().min(1).max(20),
|
|
277
|
+
quantity: z.number().positive().finite().max(MAX_WEBHOOK_QUANTITY),
|
|
278
|
+
price: z.number().positive().finite().optional(),
|
|
279
|
+
orderType: z.string().optional(),
|
|
280
|
+
leverage: z.number().int().positive().max(MAX_WEBHOOK_LEVERAGE).optional(),
|
|
281
|
+
test: z.boolean().optional(),
|
|
282
|
+
tenantId: z.string().min(1).max(64).optional()
|
|
283
|
+
}).strict();
|
|
284
|
+
var TradeQueueMessageSchema = z.object({
|
|
285
|
+
requestId: z.string().min(1).max(128),
|
|
286
|
+
exchange: z.string().min(1).max(64),
|
|
287
|
+
action: TradeActionSchema,
|
|
288
|
+
symbol: z.string().min(1).max(32),
|
|
289
|
+
quantity: z.number().positive().finite().max(MAX_WEBHOOK_QUANTITY),
|
|
290
|
+
price: z.number().positive().finite().optional(),
|
|
291
|
+
leverage: z.number().int().positive().max(MAX_WEBHOOK_LEVERAGE).optional(),
|
|
292
|
+
test: z.boolean().optional(),
|
|
293
|
+
idempotencyKey: z.string().min(1).max(512).optional(),
|
|
294
|
+
tenantId: z.string().min(1).max(64).optional(),
|
|
295
|
+
queuedAt: z.string().min(1).max(64)
|
|
296
|
+
}).strict();
|
|
297
|
+
var TradeSignalSchema = z.object({
|
|
298
|
+
id: z.number().int().positive().optional(),
|
|
299
|
+
source: z.string().min(1),
|
|
300
|
+
symbol: z.string().min(1),
|
|
301
|
+
action: TradeActionSchema,
|
|
302
|
+
price: z.number().positive().finite().optional(),
|
|
303
|
+
quantity: z.number().positive().finite(),
|
|
304
|
+
leverage: z.number().int().positive().optional(),
|
|
305
|
+
status: z.enum(["pending", "executed", "failed", "skipped"]).optional(),
|
|
306
|
+
createdAt: z.string().optional(),
|
|
307
|
+
executedAt: z.string().optional(),
|
|
308
|
+
error: z.string().optional()
|
|
309
|
+
}).strict();
|
|
310
|
+
var PositionSchema = z.object({
|
|
311
|
+
id: z.string(),
|
|
312
|
+
symbol: z.string().min(1),
|
|
313
|
+
side: TradeActionSchema,
|
|
314
|
+
quantity: z.number().positive(),
|
|
315
|
+
entry_price: z.number().positive(),
|
|
316
|
+
current_price: z.number().positive(),
|
|
317
|
+
unrealized_pnl: z.number(),
|
|
318
|
+
timestamp: z.number().int().positive()
|
|
319
|
+
}).strict();
|
|
320
|
+
var BalanceSchema = z.object({
|
|
321
|
+
asset: z.string().min(1),
|
|
322
|
+
free: z.number().min(0),
|
|
323
|
+
locked: z.number().min(0),
|
|
324
|
+
timestamp: z.number().int().positive()
|
|
325
|
+
}).strict();
|
|
261
326
|
// src/kvKeys.ts
|
|
262
327
|
var KV_TRADE_DEFAULT_LEVERAGE = "trade:default_leverage";
|
|
263
328
|
var KV_TRADE_MAX_POSITION_SIZE = "trade:max_position_size";
|
|
@@ -269,6 +334,10 @@ var KV_BOT_ENABLED = "bot:enabled";
|
|
|
269
334
|
var KV_BOT_DEFAULT_CHAT_ID = "bot:default_chat_id";
|
|
270
335
|
var KV_BOT_NOTIFY_ON_EXECUTION = "bot:notify_on_execution";
|
|
271
336
|
var KV_BOT_NOTIFY_ON_ERROR = "bot:notify_on_error";
|
|
337
|
+
var KV_BOT_NOTIFY_ON_STARTUP = "bot:notify_on_startup";
|
|
338
|
+
var KV_BOT_AI_SUMMARIES_ENABLED = "bot:ai_summaries_enabled";
|
|
339
|
+
var KV_BOT_SUMMARY_STYLE = "bot:summary_style";
|
|
340
|
+
var KV_BOT_INCLUDE_PNL = "bot:include_pnl";
|
|
272
341
|
var KV_TELEGRAM_ALLOWED_CHAT_IDS = "telegram:allowed_chat_ids";
|
|
273
342
|
var KV_WEBHOOK_IP_CHECK_ENABLED = "webhook:tradingview:ip_check_enabled";
|
|
274
343
|
var KV_WEBHOOK_ALLOWED_IPS = "webhook:tradingview:allowed_ips";
|
|
@@ -300,6 +369,10 @@ var KVKeys = {
|
|
|
300
369
|
KV_BOT_DEFAULT_CHAT_ID,
|
|
301
370
|
KV_BOT_NOTIFY_ON_EXECUTION,
|
|
302
371
|
KV_BOT_NOTIFY_ON_ERROR,
|
|
372
|
+
KV_BOT_NOTIFY_ON_STARTUP,
|
|
373
|
+
KV_BOT_AI_SUMMARIES_ENABLED,
|
|
374
|
+
KV_BOT_SUMMARY_STYLE,
|
|
375
|
+
KV_BOT_INCLUDE_PNL,
|
|
303
376
|
KV_TELEGRAM_ALLOWED_CHAT_IDS,
|
|
304
377
|
KV_WEBHOOK_IP_CHECK_ENABLED,
|
|
305
378
|
KV_WEBHOOK_ALLOWED_IPS,
|
|
@@ -321,501 +394,621 @@ var KVKeys = {
|
|
|
321
394
|
QUEUE_MODE_EVERYWHERE,
|
|
322
395
|
QUEUE_MODE_DISABLED
|
|
323
396
|
};
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
"models"
|
|
346
|
-
]);
|
|
347
|
-
function isAgentConfigEmbeddedField(fieldKey) {
|
|
348
|
-
return Object.prototype.hasOwnProperty.call(AGENT_CONFIG_EMBEDDED_FIELDS, fieldKey);
|
|
397
|
+
// src/tenant.ts
|
|
398
|
+
var TENANT_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
399
|
+
function isTenantFlagOn(env) {
|
|
400
|
+
const v = env?.TENANT_MODE;
|
|
401
|
+
return v === "true" || v === "1";
|
|
402
|
+
}
|
|
403
|
+
function tenantScope(env) {
|
|
404
|
+
if (!isTenantFlagOn(env))
|
|
405
|
+
return { enabled: false, tenantId: null };
|
|
406
|
+
const id = (env?.TENANT_ID ?? "").trim();
|
|
407
|
+
if (!id || !TENANT_ID_RE.test(id)) {
|
|
408
|
+
throw new Error("TENANT_MODE is enabled but TENANT_ID is missing or invalid (expected [A-Za-z0-9_-], 1-64 chars)");
|
|
409
|
+
}
|
|
410
|
+
return { enabled: true, tenantId: id };
|
|
411
|
+
}
|
|
412
|
+
function isTenantMode(env) {
|
|
413
|
+
try {
|
|
414
|
+
return tenantScope(env).enabled;
|
|
415
|
+
} catch {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
349
418
|
}
|
|
350
|
-
function
|
|
351
|
-
|
|
419
|
+
function effectiveTenantId(env, msg) {
|
|
420
|
+
if (isTenantFlagOn(env)) {
|
|
421
|
+
return tenantScope(env).tenantId;
|
|
422
|
+
}
|
|
423
|
+
const m = typeof msg?.tenantId === "string" ? msg.tenantId.trim() : "";
|
|
424
|
+
return m && TENANT_ID_RE.test(m) ? m : null;
|
|
352
425
|
}
|
|
353
|
-
function
|
|
354
|
-
|
|
355
|
-
|
|
426
|
+
function scopeKey(env, key, msg) {
|
|
427
|
+
const id = effectiveTenantId(env, msg);
|
|
428
|
+
return id ? `t/${id}/${key}` : key;
|
|
429
|
+
}
|
|
430
|
+
function scopeR2Key(env, key, msg) {
|
|
431
|
+
const id = effectiveTenantId(env, msg);
|
|
432
|
+
return id ? `t-${id}/${key}` : key;
|
|
433
|
+
}
|
|
434
|
+
function withTenantId(env, obj) {
|
|
435
|
+
const ts = tenantScope(env);
|
|
436
|
+
return ts.enabled && ts.tenantId ? { ...obj, tenantId: ts.tenantId } : obj;
|
|
437
|
+
}
|
|
438
|
+
function tenantContextOf(env) {
|
|
439
|
+
const ts = tenantScope(env);
|
|
440
|
+
return ts.enabled && ts.tenantId ? { tenantId: ts.tenantId } : null;
|
|
441
|
+
}
|
|
442
|
+
// src/kill-switch.ts
|
|
443
|
+
var KILL_SWITCH_ACTIVE_PREFIX = "KILL_SWITCH_ACTIVE";
|
|
444
|
+
var KILL_SWITCH_KEYS = [
|
|
445
|
+
KV_TRADE_KILL_SWITCH,
|
|
446
|
+
KV_GLOBAL_KILL_SWITCH
|
|
447
|
+
];
|
|
448
|
+
function isTruthyKillSwitchFlag(value) {
|
|
449
|
+
if (value == null)
|
|
450
|
+
return false;
|
|
451
|
+
const v = value.trim().toLowerCase();
|
|
452
|
+
return v === "true" || v === "1" || v === "yes" || v === "on";
|
|
453
|
+
}
|
|
454
|
+
async function checkKillSwitch(kv, options) {
|
|
455
|
+
const onMissingKv = options?.onMissingKv ?? "open";
|
|
456
|
+
const onReadError = options?.onReadError ?? "closed";
|
|
457
|
+
if (!kv) {
|
|
458
|
+
if (onMissingKv === "closed") {
|
|
459
|
+
return { enabled: true, source: "missing_kv" };
|
|
460
|
+
}
|
|
461
|
+
return { enabled: false };
|
|
462
|
+
}
|
|
356
463
|
try {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
464
|
+
const values = await Promise.all(KILL_SWITCH_KEYS.map(async (key) => ({
|
|
465
|
+
key,
|
|
466
|
+
value: await kv.get(key)
|
|
467
|
+
})));
|
|
468
|
+
for (const { key, value } of values) {
|
|
469
|
+
if (isTruthyKillSwitchFlag(value)) {
|
|
470
|
+
return { enabled: true, source: key };
|
|
363
471
|
}
|
|
364
472
|
}
|
|
365
|
-
|
|
366
|
-
|
|
473
|
+
return { enabled: false };
|
|
474
|
+
} catch (error) {
|
|
475
|
+
const errorStr = String(error);
|
|
476
|
+
if (onReadError === "closed") {
|
|
477
|
+
return { enabled: true, source: "read_error", error: errorStr };
|
|
367
478
|
}
|
|
368
|
-
|
|
369
|
-
|
|
479
|
+
return { enabled: false, error: errorStr };
|
|
480
|
+
}
|
|
370
481
|
}
|
|
371
|
-
function
|
|
372
|
-
|
|
482
|
+
async function isTradingPaused(kv, options) {
|
|
483
|
+
const result = await checkKillSwitch(kv, options);
|
|
484
|
+
return result.enabled;
|
|
373
485
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
"
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
return;
|
|
385
|
-
if (spec.kind === "model") {
|
|
386
|
-
const map = config.modelMap;
|
|
387
|
-
if (!map || typeof map !== "object" || Array.isArray(map))
|
|
388
|
-
return;
|
|
389
|
-
const v2 = map[spec.provider];
|
|
390
|
-
if (typeof v2 === "string" || typeof v2 === "number" || typeof v2 === "boolean")
|
|
391
|
-
return v2;
|
|
392
|
-
return;
|
|
486
|
+
// src/errors.ts
|
|
487
|
+
function toError(err, fallback = "Unknown error") {
|
|
488
|
+
if (err instanceof Error)
|
|
489
|
+
return err.message;
|
|
490
|
+
if (typeof err === "string")
|
|
491
|
+
return err;
|
|
492
|
+
if (err && typeof err === "object" && "message" in err) {
|
|
493
|
+
const message = err.message;
|
|
494
|
+
if (typeof message === "string")
|
|
495
|
+
return message;
|
|
393
496
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
return;
|
|
497
|
+
if (err === null || err === undefined)
|
|
498
|
+
return fallback;
|
|
499
|
+
try {
|
|
500
|
+
return JSON.stringify(err) || fallback;
|
|
501
|
+
} catch {
|
|
502
|
+
return fallback;
|
|
401
503
|
}
|
|
402
|
-
|
|
403
|
-
|
|
504
|
+
}
|
|
505
|
+
var SENSITIVE_FIELDS = new Set([
|
|
506
|
+
"password",
|
|
507
|
+
"token",
|
|
508
|
+
"secret",
|
|
509
|
+
"api_key",
|
|
510
|
+
"apiKey",
|
|
511
|
+
"authorization"
|
|
512
|
+
]);
|
|
513
|
+
function hasSensitiveFields(obj) {
|
|
514
|
+
for (const key of Object.keys(obj)) {
|
|
515
|
+
if (SENSITIVE_FIELDS.has(key))
|
|
516
|
+
return true;
|
|
404
517
|
}
|
|
405
|
-
return;
|
|
518
|
+
return false;
|
|
406
519
|
}
|
|
407
|
-
function
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const next = { ...config };
|
|
412
|
-
if (spec.kind === "model") {
|
|
413
|
-
const prev = next.modelMap && typeof next.modelMap === "object" && !Array.isArray(next.modelMap) ? next.modelMap : {};
|
|
414
|
-
next.modelMap = { ...prev, [spec.provider]: String(value) };
|
|
415
|
-
return next;
|
|
520
|
+
function hasErrorValues(obj) {
|
|
521
|
+
for (const v of Object.values(obj)) {
|
|
522
|
+
if (v instanceof Error)
|
|
523
|
+
return true;
|
|
416
524
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
function sanitizeOutput(data) {
|
|
528
|
+
if (data === null || data === undefined)
|
|
529
|
+
return data;
|
|
530
|
+
const t = typeof data;
|
|
531
|
+
if (t === "string" || t === "number" || t === "boolean")
|
|
532
|
+
return data;
|
|
533
|
+
if (data instanceof Error) {
|
|
534
|
+
return { name: data.name, message: data.message };
|
|
535
|
+
}
|
|
536
|
+
if (Array.isArray(data)) {
|
|
537
|
+
return data.map(sanitizeOutput);
|
|
538
|
+
}
|
|
539
|
+
if (t === "object") {
|
|
540
|
+
const obj = data;
|
|
541
|
+
const keys = Object.keys(obj);
|
|
542
|
+
if (keys.length < 10) {
|
|
543
|
+
const sanitized = {};
|
|
544
|
+
for (const k of keys) {
|
|
545
|
+
if (k === "stack" || k === "cause")
|
|
546
|
+
continue;
|
|
547
|
+
const v = obj[k];
|
|
548
|
+
sanitized[k] = v instanceof Error ? { name: v.name, message: v.message } : sanitizeOutput(v);
|
|
423
549
|
}
|
|
424
|
-
|
|
425
|
-
next.fallbackChain = value;
|
|
550
|
+
return sanitized;
|
|
426
551
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
552
|
+
if (!hasSensitiveFields(obj) && !hasErrorValues(obj)) {
|
|
553
|
+
return obj;
|
|
554
|
+
}
|
|
555
|
+
const sanitized = {};
|
|
556
|
+
for (const k of keys) {
|
|
557
|
+
if (k === "stack" || k === "cause")
|
|
558
|
+
continue;
|
|
559
|
+
const v = obj[k];
|
|
560
|
+
sanitized[k] = v instanceof Error ? { name: v.name, message: v.message } : sanitizeOutput(v);
|
|
561
|
+
}
|
|
562
|
+
return sanitized;
|
|
433
563
|
}
|
|
434
|
-
|
|
435
|
-
return next;
|
|
564
|
+
return data;
|
|
436
565
|
}
|
|
437
|
-
function
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
out[fieldKey] = v;
|
|
443
|
-
}
|
|
444
|
-
return out;
|
|
566
|
+
function createJsonResponse(data, status = 200) {
|
|
567
|
+
return new Response(JSON.stringify(sanitizeOutput(data)), {
|
|
568
|
+
status,
|
|
569
|
+
headers: { "Content-Type": "application/json" }
|
|
570
|
+
});
|
|
445
571
|
}
|
|
446
|
-
function
|
|
447
|
-
|
|
448
|
-
for (const [fieldKey, value] of Object.entries(updates)) {
|
|
449
|
-
if (isAgentConfigEmbeddedField(fieldKey)) {
|
|
450
|
-
next = setAgentConfigEmbeddedValue(next, fieldKey, value);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
return next;
|
|
572
|
+
function createSuccessResponse(result) {
|
|
573
|
+
return createJsonResponse({ success: true, result }, 200);
|
|
454
574
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
};
|
|
468
|
-
const internalKey = env.INTERNAL_KEY_BINDING;
|
|
469
|
-
if (internalKey) {
|
|
470
|
-
headers["X-Internal-Auth-Key"] = internalKey;
|
|
471
|
-
}
|
|
472
|
-
await env.ANALYTICS_SERVICE.fetch(new Request(`http://localhost${endpoint}`, {
|
|
473
|
-
method: "POST",
|
|
474
|
-
headers,
|
|
475
|
-
body: JSON.stringify(payload)
|
|
476
|
-
}));
|
|
477
|
-
} catch (e) {
|
|
478
|
-
console.error("Analytics tracking failed:", e);
|
|
479
|
-
}
|
|
575
|
+
function createErrorResponse(error, status) {
|
|
576
|
+
const message = typeof error === "string" ? error : error.message;
|
|
577
|
+
const statusCode = typeof error === "string" ? status ?? 500 : error.status ?? 500;
|
|
578
|
+
const body = { success: false, error: message };
|
|
579
|
+
if (typeof error !== "string" && error.code)
|
|
580
|
+
body.code = error.code;
|
|
581
|
+
if (typeof error !== "string" && error.details !== undefined)
|
|
582
|
+
body.details = error.details;
|
|
583
|
+
return new Response(JSON.stringify(body), {
|
|
584
|
+
status: statusCode,
|
|
585
|
+
headers: { "Content-Type": "application/json" }
|
|
586
|
+
});
|
|
480
587
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
info: "#38BDF8",
|
|
497
|
-
text: "#E8E8F0",
|
|
498
|
-
"text-muted": "#8B8B9E",
|
|
499
|
-
panel: "#0A0A0F",
|
|
500
|
-
divider: "#232330",
|
|
501
|
-
highlight: "#22D3EE",
|
|
502
|
-
backdrop: "#000000"
|
|
503
|
-
};
|
|
504
|
-
var CoolBracketPalette = [
|
|
505
|
-
"#22D3EE",
|
|
506
|
-
"#38BDF8",
|
|
507
|
-
"#60A5FA",
|
|
508
|
-
"#818CF8",
|
|
509
|
-
"#A78BFA",
|
|
510
|
-
"#C084FC",
|
|
511
|
-
"#E879F9",
|
|
512
|
-
"#A78BFA",
|
|
513
|
-
"#818CF8",
|
|
514
|
-
"#38BDF8"
|
|
515
|
-
];
|
|
516
|
-
var ConnectionStatusColor = {
|
|
517
|
-
connected: Colors.success,
|
|
518
|
-
polling: Colors.highlight,
|
|
519
|
-
reconnecting: Colors.warning,
|
|
520
|
-
offline: Colors.error
|
|
521
|
-
};
|
|
522
|
-
var WorkerStatusColor = {
|
|
523
|
-
operational: Colors.success,
|
|
524
|
-
degraded: Colors.warning,
|
|
525
|
-
down: Colors.error
|
|
526
|
-
};
|
|
527
|
-
var LogLevelColor = {
|
|
528
|
-
error: Colors.error,
|
|
529
|
-
warn: Colors.warning,
|
|
530
|
-
info: Colors.foreground,
|
|
531
|
-
debug: Colors.muted
|
|
532
|
-
};
|
|
533
|
-
var AlertSeverityColor = {
|
|
534
|
-
info: Colors.info,
|
|
535
|
-
warning: Colors.warning,
|
|
536
|
-
error: Colors.error,
|
|
537
|
-
critical: Colors.error
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
// src/config.ts
|
|
541
|
-
import {
|
|
542
|
-
readFileSync,
|
|
543
|
-
writeFileSync,
|
|
544
|
-
existsSync,
|
|
545
|
-
mkdirSync,
|
|
546
|
-
chmodSync,
|
|
547
|
-
statSync
|
|
548
|
-
} from "fs";
|
|
549
|
-
import { join } from "path";
|
|
550
|
-
import { homedir } from "os";
|
|
551
|
-
var HOX_DIR = join(homedir(), ".hoox");
|
|
552
|
-
var CONFIG_PATH = join(HOX_DIR, "config.json");
|
|
553
|
-
var HOOX_DIR_MODE = 448;
|
|
554
|
-
var HOOX_CONFIG_FILE_MODE = 384;
|
|
555
|
-
var DEFAULT_CONFIG = {
|
|
556
|
-
apiUrl: "http://localhost:8787",
|
|
557
|
-
apiToken: "",
|
|
558
|
-
transport: "public",
|
|
559
|
-
refreshIntervalMs: 500,
|
|
560
|
-
theme: "dark",
|
|
561
|
-
activeExchanges: ["binance", "bybit", "mexc"],
|
|
562
|
-
notifications: {
|
|
563
|
-
alerts: true,
|
|
564
|
-
trades: true,
|
|
565
|
-
debug: false,
|
|
566
|
-
system: true
|
|
588
|
+
var Errors = {
|
|
589
|
+
badRequest: (message) => createErrorResponse({ message, status: 400, code: "BAD_REQUEST" }),
|
|
590
|
+
unauthorized: (message = "Unauthorized") => createErrorResponse({ message, status: 401, code: "UNAUTHORIZED" }),
|
|
591
|
+
forbidden: (message = "Forbidden") => createErrorResponse({ message, status: 403, code: "FORBIDDEN" }),
|
|
592
|
+
notFound: (message = "Not found") => createErrorResponse({ message, status: 404, code: "NOT_FOUND" }),
|
|
593
|
+
methodNotAllowed: (message = "Method not allowed") => createErrorResponse({ message, status: 405, code: "METHOD_NOT_ALLOWED" }),
|
|
594
|
+
rateLimited: (retryAfter) => {
|
|
595
|
+
const res = createErrorResponse({
|
|
596
|
+
message: "Rate limit exceeded",
|
|
597
|
+
status: 429,
|
|
598
|
+
code: "RATE_LIMITED"
|
|
599
|
+
});
|
|
600
|
+
if (retryAfter)
|
|
601
|
+
res.headers.set("Retry-After", String(retryAfter));
|
|
602
|
+
return res;
|
|
567
603
|
},
|
|
568
|
-
|
|
569
|
-
|
|
604
|
+
internal: (err) => {
|
|
605
|
+
const message = err ? toError(err, "Internal server error") : "Internal server error";
|
|
606
|
+
return createErrorResponse({
|
|
607
|
+
message,
|
|
608
|
+
status: 500,
|
|
609
|
+
code: "INTERNAL_ERROR"
|
|
610
|
+
});
|
|
611
|
+
}
|
|
570
612
|
};
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
let fileConfig = {};
|
|
577
|
-
try {
|
|
578
|
-
if (existsSync(CONFIG_PATH)) {
|
|
579
|
-
fileConfig = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
580
|
-
}
|
|
581
|
-
} catch {}
|
|
582
|
-
return {
|
|
583
|
-
...DEFAULT_CONFIG,
|
|
584
|
-
...fileConfig,
|
|
585
|
-
...Object.fromEntries(Object.entries(envConfig).filter(([_, v]) => v !== undefined))
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
async function readConfig() {
|
|
589
|
-
return readConfigSync();
|
|
590
|
-
}
|
|
591
|
-
function ensureHooxDirSecure(dir = HOX_DIR) {
|
|
592
|
-
if (!existsSync(dir)) {
|
|
593
|
-
mkdirSync(dir, { recursive: true, mode: HOOX_DIR_MODE });
|
|
613
|
+
// src/exchange-client.ts
|
|
614
|
+
class ExchangeRouter {
|
|
615
|
+
providers = new Map;
|
|
616
|
+
registerProvider(provider) {
|
|
617
|
+
this.providers.set(provider.name.toLowerCase(), provider);
|
|
594
618
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
}
|
|
608
|
-
function isConfigWorldOrGroupReadable(filePath = CONFIG_PATH) {
|
|
609
|
-
try {
|
|
610
|
-
if (!existsSync(filePath))
|
|
611
|
-
return false;
|
|
612
|
-
return isUnixModeGroupOrWorldAccessible(statSync(filePath).mode & 511);
|
|
613
|
-
} catch {
|
|
614
|
-
return false;
|
|
619
|
+
route(exchange, env, options) {
|
|
620
|
+
const key = exchange.toLowerCase();
|
|
621
|
+
const provider = this.providers.get(key);
|
|
622
|
+
if (!provider) {
|
|
623
|
+
throw new Error(`Unsupported exchange: ${exchange}`);
|
|
624
|
+
}
|
|
625
|
+
if (options?.testnet && provider.supportsTestTrading === false) {
|
|
626
|
+
throw new Error(`TEST_TRADING_UNSUPPORTED: ${exchange} does not support test/sandbox trading via API`);
|
|
627
|
+
}
|
|
628
|
+
if (!provider.hasCredentials(env)) {
|
|
629
|
+
throw new Error(`API secret bindings not configured or accessible for ${exchange}`);
|
|
630
|
+
}
|
|
631
|
+
return { exchange: key, client: provider.createClient(env, options) };
|
|
615
632
|
}
|
|
616
633
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
634
|
+
|
|
635
|
+
class BaseExchangeClient {
|
|
636
|
+
apiKey;
|
|
637
|
+
apiSecret;
|
|
638
|
+
baseUrl;
|
|
639
|
+
isTestnet;
|
|
640
|
+
constructor(config) {
|
|
641
|
+
if (!config.apiKey || !config.apiSecret) {
|
|
642
|
+
throw new Error("API key and secret are required.");
|
|
643
|
+
}
|
|
644
|
+
this.apiKey = config.apiKey;
|
|
645
|
+
this.apiSecret = config.apiSecret;
|
|
646
|
+
this.baseUrl = config.baseUrl || this.getDefaultBaseUrl();
|
|
647
|
+
this.isTestnet = config.testnet || false;
|
|
628
648
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
649
|
+
async fetch(method, path, params) {
|
|
650
|
+
const url = `${this.baseUrl}${path}`;
|
|
651
|
+
const headers = this.buildHeaders(method, path, params);
|
|
652
|
+
const body = params ? new URLSearchParams(params).toString() : undefined;
|
|
653
|
+
const response = await fetch(url, {
|
|
654
|
+
method,
|
|
655
|
+
headers,
|
|
656
|
+
body: method !== "GET" ? body : undefined
|
|
657
|
+
});
|
|
658
|
+
if (!response.ok) {
|
|
659
|
+
const error = await response.text();
|
|
660
|
+
throw new Error(`Exchange API error: ${response.status} - ${error}`);
|
|
661
|
+
}
|
|
662
|
+
return response.json();
|
|
643
663
|
}
|
|
644
|
-
|
|
645
|
-
|
|
664
|
+
async openLong(symbol, quantity, price, orderType = "MARKET") {
|
|
665
|
+
return this.executeTrade({
|
|
666
|
+
symbol,
|
|
667
|
+
side: "long",
|
|
668
|
+
orderType,
|
|
669
|
+
quantity,
|
|
670
|
+
price
|
|
671
|
+
});
|
|
646
672
|
}
|
|
647
|
-
|
|
648
|
-
|
|
673
|
+
async openShort(symbol, quantity, price, orderType = "MARKET") {
|
|
674
|
+
return this.executeTrade({
|
|
675
|
+
symbol,
|
|
676
|
+
side: "short",
|
|
677
|
+
orderType,
|
|
678
|
+
quantity,
|
|
679
|
+
price
|
|
680
|
+
});
|
|
649
681
|
}
|
|
650
|
-
|
|
651
|
-
|
|
682
|
+
async closeLong(symbol, quantity) {
|
|
683
|
+
return this.executeTrade({
|
|
684
|
+
symbol,
|
|
685
|
+
side: "long",
|
|
686
|
+
quantity,
|
|
687
|
+
reduceOnly: true
|
|
688
|
+
});
|
|
652
689
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
return async (event, env, ctx) => {
|
|
660
|
-
const startTime = Date.now();
|
|
661
|
-
logger?.info(`${name} cron triggered`, {
|
|
662
|
-
cron: event.cron,
|
|
663
|
-
scheduledTime: event.scheduledTime
|
|
690
|
+
async closeShort(symbol, quantity) {
|
|
691
|
+
return this.executeTrade({
|
|
692
|
+
symbol,
|
|
693
|
+
side: "short",
|
|
694
|
+
quantity,
|
|
695
|
+
reduceOnly: true
|
|
664
696
|
});
|
|
665
|
-
|
|
666
|
-
await handler(event, env, ctx);
|
|
667
|
-
const durationMs = Date.now() - startTime;
|
|
668
|
-
logger?.info(`${name} cron handler completed successfully`, {
|
|
669
|
-
cron: event.cron,
|
|
670
|
-
durationMs
|
|
671
|
-
});
|
|
672
|
-
} catch (error) {
|
|
673
|
-
const durationMs = Date.now() - startTime;
|
|
674
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
675
|
-
logger?.error(`${name} cron handler failed`, {
|
|
676
|
-
cron: event.cron,
|
|
677
|
-
durationMs,
|
|
678
|
-
error: errorMsg
|
|
679
|
-
});
|
|
680
|
-
throw error;
|
|
681
|
-
}
|
|
682
|
-
};
|
|
697
|
+
}
|
|
683
698
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
699
|
+
// src/exchanges/base-exchange-client.ts
|
|
700
|
+
class BaseExchangeClient2 {
|
|
701
|
+
apiKey;
|
|
702
|
+
apiSecret;
|
|
703
|
+
baseUrl;
|
|
704
|
+
isTestnet;
|
|
705
|
+
importedKeyPromise;
|
|
706
|
+
constructor(apiKey, apiSecret, options) {
|
|
707
|
+
if (!apiKey || !apiSecret) {
|
|
708
|
+
throw new Error(this.getErrorMessagePrefix() + "API key and secret are required.");
|
|
709
|
+
}
|
|
710
|
+
this.apiKey = apiKey;
|
|
711
|
+
this.apiSecret = apiSecret;
|
|
712
|
+
this.isTestnet = options?.testnet ?? false;
|
|
713
|
+
this.baseUrl = options?.baseUrl ?? this.getDefaultBaseUrl();
|
|
714
|
+
this.importedKeyPromise = this.importKey();
|
|
695
715
|
}
|
|
696
|
-
|
|
697
|
-
return
|
|
698
|
-
try {
|
|
699
|
-
return JSON.stringify(err) || fallback;
|
|
700
|
-
} catch {
|
|
701
|
-
return fallback;
|
|
716
|
+
get testnet() {
|
|
717
|
+
return this.isTestnet;
|
|
702
718
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
"password",
|
|
706
|
-
"token",
|
|
707
|
-
"secret",
|
|
708
|
-
"api_key",
|
|
709
|
-
"apiKey",
|
|
710
|
-
"authorization"
|
|
711
|
-
]);
|
|
712
|
-
function hasSensitiveFields(obj) {
|
|
713
|
-
for (const key of Object.keys(obj)) {
|
|
714
|
-
if (SENSITIVE_FIELDS.has(key))
|
|
715
|
-
return true;
|
|
719
|
+
getErrorMessagePrefix() {
|
|
720
|
+
return "";
|
|
716
721
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
function hasErrorValues(obj) {
|
|
720
|
-
for (const v of Object.values(obj)) {
|
|
721
|
-
if (v instanceof Error)
|
|
722
|
-
return true;
|
|
722
|
+
generateSignature(_params) {
|
|
723
|
+
throw new Error("generateSignature must be implemented by subclass");
|
|
723
724
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
function sanitizeOutput(data) {
|
|
727
|
-
if (data === null || data === undefined)
|
|
728
|
-
return data;
|
|
729
|
-
const t = typeof data;
|
|
730
|
-
if (t === "string" || t === "number" || t === "boolean")
|
|
731
|
-
return data;
|
|
732
|
-
if (data instanceof Error) {
|
|
733
|
-
return { name: data.name, message: data.message };
|
|
725
|
+
buildHeaders(_method, _path, _params) {
|
|
726
|
+
return new Headers;
|
|
734
727
|
}
|
|
735
|
-
|
|
736
|
-
|
|
728
|
+
async importKey() {
|
|
729
|
+
const encoder = new TextEncoder;
|
|
730
|
+
return crypto.subtle.importKey("raw", encoder.encode(this.apiSecret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
737
731
|
}
|
|
738
|
-
|
|
739
|
-
const
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
for (const k of keys) {
|
|
744
|
-
if (k === "stack" || k === "cause")
|
|
745
|
-
continue;
|
|
746
|
-
const v = obj[k];
|
|
747
|
-
sanitized2[k] = v instanceof Error ? { name: v.name, message: v.message } : sanitizeOutput(v);
|
|
748
|
-
}
|
|
749
|
-
return sanitized2;
|
|
750
|
-
}
|
|
751
|
-
if (!hasSensitiveFields(obj) && !hasErrorValues(obj)) {
|
|
752
|
-
return obj;
|
|
753
|
-
}
|
|
754
|
-
const sanitized = {};
|
|
755
|
-
for (const k of keys) {
|
|
756
|
-
if (k === "stack" || k === "cause")
|
|
757
|
-
continue;
|
|
758
|
-
const v = obj[k];
|
|
759
|
-
sanitized[k] = v instanceof Error ? { name: v.name, message: v.message } : sanitizeOutput(v);
|
|
760
|
-
}
|
|
761
|
-
return sanitized;
|
|
732
|
+
async cryptoSign(data) {
|
|
733
|
+
const encoder = new TextEncoder;
|
|
734
|
+
const key = await this.importedKeyPromise;
|
|
735
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
|
|
736
|
+
return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
762
737
|
}
|
|
763
|
-
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
function createSuccessResponse(result) {
|
|
772
|
-
return createJsonResponse({ success: true, result }, 200);
|
|
773
|
-
}
|
|
774
|
-
function createErrorResponse(error, status) {
|
|
775
|
-
const message = typeof error === "string" ? error : error.message;
|
|
776
|
-
const statusCode = typeof error === "string" ? status ?? 500 : error.status ?? 500;
|
|
777
|
-
const body = { success: false, error: message };
|
|
778
|
-
if (typeof error !== "string" && error.code)
|
|
779
|
-
body.code = error.code;
|
|
780
|
-
if (typeof error !== "string" && error.details !== undefined)
|
|
781
|
-
body.details = error.details;
|
|
782
|
-
return new Response(JSON.stringify(body), {
|
|
783
|
-
status: statusCode,
|
|
784
|
-
headers: { "Content-Type": "application/json" }
|
|
785
|
-
});
|
|
786
|
-
}
|
|
787
|
-
var Errors = {
|
|
788
|
-
badRequest: (message) => createErrorResponse({ message, status: 400, code: "BAD_REQUEST" }),
|
|
789
|
-
unauthorized: (message = "Unauthorized") => createErrorResponse({ message, status: 401, code: "UNAUTHORIZED" }),
|
|
790
|
-
forbidden: (message = "Forbidden") => createErrorResponse({ message, status: 403, code: "FORBIDDEN" }),
|
|
791
|
-
notFound: (message = "Not found") => createErrorResponse({ message, status: 404, code: "NOT_FOUND" }),
|
|
792
|
-
methodNotAllowed: (message = "Method not allowed") => createErrorResponse({ message, status: 405, code: "METHOD_NOT_ALLOWED" }),
|
|
793
|
-
rateLimited: (retryAfter) => {
|
|
794
|
-
const res = createErrorResponse({
|
|
795
|
-
message: "Rate limit exceeded",
|
|
796
|
-
status: 429,
|
|
797
|
-
code: "RATE_LIMITED"
|
|
798
|
-
});
|
|
799
|
-
if (retryAfter)
|
|
800
|
-
res.headers.set("Retry-After", String(retryAfter));
|
|
801
|
-
return res;
|
|
802
|
-
},
|
|
803
|
-
internal: (err) => {
|
|
804
|
-
const message = err ? toError(err, "Internal server error") : "Internal server error";
|
|
805
|
-
return createErrorResponse({
|
|
806
|
-
message,
|
|
807
|
-
status: 500,
|
|
808
|
-
code: "INTERNAL_ERROR"
|
|
738
|
+
async fetch(method, path, params) {
|
|
739
|
+
const url = `${this.baseUrl}${path}`;
|
|
740
|
+
const headers = this.buildHeaders(method, path, params);
|
|
741
|
+
const body = params ? new URLSearchParams(params).toString() : undefined;
|
|
742
|
+
const response = await fetch(url, {
|
|
743
|
+
method,
|
|
744
|
+
headers,
|
|
745
|
+
body: method !== "GET" ? body : undefined
|
|
809
746
|
});
|
|
747
|
+
if (!response.ok) {
|
|
748
|
+
const error = await response.text();
|
|
749
|
+
throw new Error(`Exchange API error: ${response.status} - ${error}`);
|
|
750
|
+
}
|
|
751
|
+
return response.json();
|
|
752
|
+
}
|
|
753
|
+
setLeverage(_symbol, _leverage) {
|
|
754
|
+
throw new Error("setLeverage must be implemented by subclass");
|
|
755
|
+
}
|
|
756
|
+
executeTrade(_params) {
|
|
757
|
+
throw new Error("executeTrade must be implemented by subclass");
|
|
758
|
+
}
|
|
759
|
+
getAccountInfo() {
|
|
760
|
+
throw new Error("getAccountInfo must be implemented by subclass");
|
|
761
|
+
}
|
|
762
|
+
getPositions(_symbol) {
|
|
763
|
+
throw new Error("getPositions must be implemented by subclass");
|
|
764
|
+
}
|
|
765
|
+
async openLong(symbol, quantity, price, orderType = "MARKET") {
|
|
766
|
+
return this.executeTrade({
|
|
767
|
+
symbol,
|
|
768
|
+
side: "BUY",
|
|
769
|
+
orderType,
|
|
770
|
+
quantity,
|
|
771
|
+
price
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
async openShort(symbol, quantity, price, orderType = "MARKET") {
|
|
775
|
+
return this.executeTrade({
|
|
776
|
+
symbol,
|
|
777
|
+
side: "SELL",
|
|
778
|
+
orderType,
|
|
779
|
+
quantity,
|
|
780
|
+
price
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
async closeLong(symbol, quantity) {
|
|
784
|
+
return this.executeTrade({
|
|
785
|
+
symbol,
|
|
786
|
+
side: "SELL",
|
|
787
|
+
orderType: "MARKET",
|
|
788
|
+
quantity,
|
|
789
|
+
reduceOnly: true
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
async closeShort(symbol, quantity) {
|
|
793
|
+
return this.executeTrade({
|
|
794
|
+
symbol,
|
|
795
|
+
side: "BUY",
|
|
796
|
+
orderType: "MARKET",
|
|
797
|
+
quantity,
|
|
798
|
+
reduceOnly: true
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
// src/kvUtils.ts
|
|
803
|
+
async function logKvTimestamp(env, prefix = "timestamp") {
|
|
804
|
+
const timestamp = new Date().toISOString();
|
|
805
|
+
const key = `${prefix}_${timestamp}`;
|
|
806
|
+
try {
|
|
807
|
+
await env.REPORT_KV.put(key, timestamp);
|
|
808
|
+
} catch (error) {
|
|
809
|
+
console.error(`Failed to log timestamp to KV: ${toError(error, "Unknown error")}`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
var KV_BULK_GET_MAX_KEYS = 100;
|
|
813
|
+
async function tryBulkGetChunk(kv, chunk) {
|
|
814
|
+
try {
|
|
815
|
+
const result = await kv.get([...chunk]);
|
|
816
|
+
if (!(result instanceof Map)) {
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
return chunk.map((key) => result.get(key) ?? null);
|
|
820
|
+
} catch {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
async function kvGetMany(kv, keys) {
|
|
825
|
+
if (keys.length === 0)
|
|
826
|
+
return [];
|
|
827
|
+
const out = new Array(keys.length);
|
|
828
|
+
let offset = 0;
|
|
829
|
+
while (offset < keys.length) {
|
|
830
|
+
const chunk = keys.slice(offset, offset + KV_BULK_GET_MAX_KEYS);
|
|
831
|
+
const bulk = await tryBulkGetChunk(kv, chunk);
|
|
832
|
+
if (bulk === null) {
|
|
833
|
+
const rest = await Promise.all(keys.slice(offset).map((key) => kv.get(key)));
|
|
834
|
+
for (let i = 0;i < rest.length; i++) {
|
|
835
|
+
out[offset + i] = rest[i] ?? null;
|
|
836
|
+
}
|
|
837
|
+
return out;
|
|
838
|
+
}
|
|
839
|
+
for (let i = 0;i < bulk.length; i++) {
|
|
840
|
+
out[offset + i] = bulk[i] ?? null;
|
|
841
|
+
}
|
|
842
|
+
offset += chunk.length;
|
|
843
|
+
}
|
|
844
|
+
return out;
|
|
845
|
+
}
|
|
846
|
+
async function kvGetManyAsRecord(kv, keys) {
|
|
847
|
+
const values = await kvGetMany(kv, keys);
|
|
848
|
+
const out = {};
|
|
849
|
+
for (let i = 0;i < keys.length; i++) {
|
|
850
|
+
out[keys[i]] = values[i] ?? null;
|
|
851
|
+
}
|
|
852
|
+
return out;
|
|
853
|
+
}
|
|
854
|
+
async function kvPutMany(kv, entries) {
|
|
855
|
+
if (entries.length === 0)
|
|
856
|
+
return;
|
|
857
|
+
await Promise.all(entries.map((entry) => kv.put(entry.key, entry.value, entry.options)));
|
|
858
|
+
}
|
|
859
|
+
function headersToObject(headers) {
|
|
860
|
+
const result = {};
|
|
861
|
+
if (!headers)
|
|
862
|
+
return result;
|
|
863
|
+
try {
|
|
864
|
+
headers.forEach((value, key) => {
|
|
865
|
+
result[key] = value;
|
|
866
|
+
});
|
|
867
|
+
} catch (error) {
|
|
868
|
+
console.error(`Error converting headers to object: ${toError(error, "Unknown error")}`);
|
|
869
|
+
}
|
|
870
|
+
return result;
|
|
871
|
+
}
|
|
872
|
+
// src/analytics.ts
|
|
873
|
+
async function trackAnalytics(env, endpoint, body, options) {
|
|
874
|
+
if (!env.ANALYTICS_SERVICE)
|
|
875
|
+
return;
|
|
876
|
+
try {
|
|
877
|
+
let tenantId = null;
|
|
878
|
+
try {
|
|
879
|
+
tenantId = effectiveTenantId(env);
|
|
880
|
+
} catch {
|
|
881
|
+
tenantId = null;
|
|
882
|
+
}
|
|
883
|
+
const rest = { ...body };
|
|
884
|
+
delete rest.tenant_id;
|
|
885
|
+
delete rest.tenantId;
|
|
886
|
+
const payload = {
|
|
887
|
+
...rest,
|
|
888
|
+
...tenantId ? { tenant_id: tenantId } : {},
|
|
889
|
+
...options?.indexes ? { indexes: [...options.indexes] } : {}
|
|
890
|
+
};
|
|
891
|
+
const headers = {
|
|
892
|
+
"Content-Type": "application/json"
|
|
893
|
+
};
|
|
894
|
+
const internalKey = env.INTERNAL_KEY_BINDING;
|
|
895
|
+
if (internalKey) {
|
|
896
|
+
headers["X-Internal-Auth-Key"] = internalKey;
|
|
897
|
+
}
|
|
898
|
+
await env.ANALYTICS_SERVICE.fetch(new Request(`http://localhost${endpoint}`, {
|
|
899
|
+
method: "POST",
|
|
900
|
+
headers,
|
|
901
|
+
body: JSON.stringify(payload)
|
|
902
|
+
}));
|
|
903
|
+
} catch (e) {
|
|
904
|
+
console.error("Analytics tracking failed:", e);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
// src/health.ts
|
|
908
|
+
function healthCheck(options) {
|
|
909
|
+
const result = {
|
|
910
|
+
status: "ok",
|
|
911
|
+
timestamp: new Date().toISOString()
|
|
912
|
+
};
|
|
913
|
+
if (options?.worker !== undefined && options?.worker !== null)
|
|
914
|
+
result.service = options.worker;
|
|
915
|
+
if (options?.version !== undefined && options?.version !== null)
|
|
916
|
+
result.version = options.version;
|
|
917
|
+
if (options?.details)
|
|
918
|
+
result.details = options.details;
|
|
919
|
+
return createSuccessResponse(result);
|
|
920
|
+
}
|
|
921
|
+
// src/queue-handler.ts
|
|
922
|
+
async function mapPool(items, concurrency, fn) {
|
|
923
|
+
if (items.length === 0)
|
|
924
|
+
return;
|
|
925
|
+
const limit = Math.min(Math.max(1, concurrency), items.length);
|
|
926
|
+
if (limit === 1) {
|
|
927
|
+
for (const item of items) {
|
|
928
|
+
await fn(item);
|
|
929
|
+
}
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
let nextIndex = 0;
|
|
933
|
+
async function worker() {
|
|
934
|
+
while (true) {
|
|
935
|
+
const i = nextIndex++;
|
|
936
|
+
if (i >= items.length)
|
|
937
|
+
return;
|
|
938
|
+
await fn(items[i]);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
await Promise.all(Array.from({ length: limit }, () => worker()));
|
|
942
|
+
}
|
|
943
|
+
function createQueueHandler(options) {
|
|
944
|
+
const {
|
|
945
|
+
maxRetries,
|
|
946
|
+
backoffDelays,
|
|
947
|
+
onMessage,
|
|
948
|
+
onRetry,
|
|
949
|
+
onDLQ,
|
|
950
|
+
logger,
|
|
951
|
+
concurrency: concurrencyOpt
|
|
952
|
+
} = options;
|
|
953
|
+
const concurrency = typeof concurrencyOpt === "number" && Number.isFinite(concurrencyOpt) && concurrencyOpt >= 1 ? Math.floor(concurrencyOpt) : 1;
|
|
954
|
+
async function processMessage(msg) {
|
|
955
|
+
const attemptNumber = msg.attempts || 0;
|
|
956
|
+
const logId = `[${msg.id}]`;
|
|
957
|
+
try {
|
|
958
|
+
logger?.info(`${logId} Processing message (attempt ${attemptNumber + 1})`);
|
|
959
|
+
await onMessage(msg.body, attemptNumber);
|
|
960
|
+
logger?.info(`${logId} Message processed successfully`);
|
|
961
|
+
} catch (error) {
|
|
962
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
963
|
+
if (attemptNumber < maxRetries) {
|
|
964
|
+
const delaySeconds = backoffDelays[attemptNumber] ?? backoffDelays[backoffDelays.length - 1] ?? 0;
|
|
965
|
+
logger?.info(`${logId} Retrying in ${delaySeconds}s (attempt ${attemptNumber + 2}/${maxRetries + 1})`);
|
|
966
|
+
await onRetry?.(msg.body, attemptNumber, errorMsg, delaySeconds);
|
|
967
|
+
msg.retry({ delaySeconds });
|
|
968
|
+
} else {
|
|
969
|
+
logger?.error(`${logId} Max retries exceeded (${maxRetries + 1} attempts), moving to DLQ`);
|
|
970
|
+
await onDLQ?.(msg.body, attemptNumber, errorMsg);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return async (batch) => {
|
|
975
|
+
await mapPool(batch.messages, concurrency, processMessage);
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
// src/cron-handler.ts
|
|
979
|
+
function createCronHandler(options) {
|
|
980
|
+
const { name, handler, logger } = options;
|
|
981
|
+
return async (event, env, ctx) => {
|
|
982
|
+
const startTime = Date.now();
|
|
983
|
+
logger?.info(`${name} cron triggered`, {
|
|
984
|
+
cron: event.cron,
|
|
985
|
+
scheduledTime: event.scheduledTime
|
|
986
|
+
});
|
|
987
|
+
try {
|
|
988
|
+
await handler(event, env, ctx);
|
|
989
|
+
const durationMs = Date.now() - startTime;
|
|
990
|
+
logger?.info(`${name} cron handler completed successfully`, {
|
|
991
|
+
cron: event.cron,
|
|
992
|
+
durationMs
|
|
993
|
+
});
|
|
994
|
+
} catch (error) {
|
|
995
|
+
const durationMs = Date.now() - startTime;
|
|
996
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
997
|
+
logger?.error(`${name} cron handler failed`, {
|
|
998
|
+
cron: event.cron,
|
|
999
|
+
durationMs,
|
|
1000
|
+
error: errorMsg
|
|
1001
|
+
});
|
|
1002
|
+
throw error;
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
// src/d1/repository.ts
|
|
1007
|
+
class D1Repository {
|
|
1008
|
+
db;
|
|
1009
|
+
constructor(db) {
|
|
1010
|
+
this.db = db;
|
|
810
1011
|
}
|
|
811
|
-
};
|
|
812
|
-
|
|
813
|
-
// src/d1/repository.ts
|
|
814
|
-
class D1Repository {
|
|
815
|
-
db;
|
|
816
|
-
constructor(db) {
|
|
817
|
-
this.db = db;
|
|
818
|
-
}
|
|
819
1012
|
async query(sql, params = []) {
|
|
820
1013
|
try {
|
|
821
1014
|
const stmt = this.db.prepare(sql).bind(...params);
|
|
@@ -961,659 +1154,445 @@ class D1Repository {
|
|
|
961
1154
|
}
|
|
962
1155
|
}
|
|
963
1156
|
}
|
|
964
|
-
|
|
965
1157
|
// src/d1/schemas.ts
|
|
966
|
-
import { z } from "zod";
|
|
967
|
-
var TradeRecordSchema =
|
|
968
|
-
id:
|
|
969
|
-
signal_id:
|
|
970
|
-
timestamp:
|
|
971
|
-
exchange:
|
|
972
|
-
symbol:
|
|
973
|
-
action:
|
|
974
|
-
quantity:
|
|
975
|
-
price:
|
|
976
|
-
leverage:
|
|
977
|
-
status:
|
|
978
|
-
error_message:
|
|
979
|
-
raw_response:
|
|
980
|
-
created_at:
|
|
1158
|
+
import { z as z2 } from "zod";
|
|
1159
|
+
var TradeRecordSchema = z2.object({
|
|
1160
|
+
id: z2.string(),
|
|
1161
|
+
signal_id: z2.string().nullable().optional(),
|
|
1162
|
+
timestamp: z2.number().int().positive(),
|
|
1163
|
+
exchange: z2.string().min(1),
|
|
1164
|
+
symbol: z2.string().min(1),
|
|
1165
|
+
action: z2.string().min(1),
|
|
1166
|
+
quantity: z2.number().nullable(),
|
|
1167
|
+
price: z2.number().nullable(),
|
|
1168
|
+
leverage: z2.number().nullable(),
|
|
1169
|
+
status: z2.string().min(1),
|
|
1170
|
+
error_message: z2.string().nullable().optional(),
|
|
1171
|
+
raw_response: z2.string().nullable().optional(),
|
|
1172
|
+
created_at: z2.number().int().optional()
|
|
981
1173
|
}).strict();
|
|
982
|
-
var PositionRecordSchema =
|
|
983
|
-
id:
|
|
984
|
-
exchange:
|
|
985
|
-
symbol:
|
|
986
|
-
side:
|
|
987
|
-
entry_price:
|
|
988
|
-
mark_price:
|
|
989
|
-
liquidation_price:
|
|
990
|
-
leverage:
|
|
991
|
-
size:
|
|
992
|
-
unrealized_pnl:
|
|
993
|
-
status:
|
|
994
|
-
updated_at:
|
|
1174
|
+
var PositionRecordSchema = z2.object({
|
|
1175
|
+
id: z2.string(),
|
|
1176
|
+
exchange: z2.string().min(1),
|
|
1177
|
+
symbol: z2.string().min(1),
|
|
1178
|
+
side: z2.enum(["LONG", "SHORT"]),
|
|
1179
|
+
entry_price: z2.number().nullable().optional(),
|
|
1180
|
+
mark_price: z2.number().nullable().optional(),
|
|
1181
|
+
liquidation_price: z2.number().nullable().optional(),
|
|
1182
|
+
leverage: z2.number().nullable().optional(),
|
|
1183
|
+
size: z2.number().nullable(),
|
|
1184
|
+
unrealized_pnl: z2.number().nullable().optional(),
|
|
1185
|
+
status: z2.enum(["OPEN", "CLOSED"]),
|
|
1186
|
+
updated_at: z2.number().int()
|
|
995
1187
|
}).strict();
|
|
996
|
-
var BalanceRecordSchema =
|
|
997
|
-
id:
|
|
998
|
-
exchange:
|
|
999
|
-
asset:
|
|
1000
|
-
free:
|
|
1001
|
-
used:
|
|
1002
|
-
total:
|
|
1003
|
-
timestamp:
|
|
1188
|
+
var BalanceRecordSchema = z2.object({
|
|
1189
|
+
id: z2.string(),
|
|
1190
|
+
exchange: z2.string().min(1),
|
|
1191
|
+
asset: z2.string().min(1),
|
|
1192
|
+
free: z2.number().nullable(),
|
|
1193
|
+
used: z2.number().nullable(),
|
|
1194
|
+
total: z2.number().nullable(),
|
|
1195
|
+
timestamp: z2.number().int().positive()
|
|
1004
1196
|
}).strict();
|
|
1005
|
-
var SystemLogRecordSchema =
|
|
1006
|
-
id:
|
|
1007
|
-
timestamp:
|
|
1008
|
-
level:
|
|
1009
|
-
service:
|
|
1010
|
-
message:
|
|
1011
|
-
details:
|
|
1197
|
+
var SystemLogRecordSchema = z2.object({
|
|
1198
|
+
id: z2.string(),
|
|
1199
|
+
timestamp: z2.number().int().optional(),
|
|
1200
|
+
level: z2.string().min(1),
|
|
1201
|
+
service: z2.string().min(1),
|
|
1202
|
+
message: z2.string().min(1),
|
|
1203
|
+
details: z2.string().nullable().optional()
|
|
1012
1204
|
}).strict();
|
|
1013
|
-
var TradeSignalRecordSchema =
|
|
1014
|
-
signal_id:
|
|
1015
|
-
timestamp:
|
|
1016
|
-
symbol:
|
|
1017
|
-
signal_type:
|
|
1018
|
-
source:
|
|
1019
|
-
raw_data:
|
|
1020
|
-
processed_at:
|
|
1205
|
+
var TradeSignalRecordSchema = z2.object({
|
|
1206
|
+
signal_id: z2.string(),
|
|
1207
|
+
timestamp: z2.number().int().positive(),
|
|
1208
|
+
symbol: z2.string().min(1),
|
|
1209
|
+
signal_type: z2.string().min(1),
|
|
1210
|
+
source: z2.string().nullable().optional(),
|
|
1211
|
+
raw_data: z2.string().nullable().optional(),
|
|
1212
|
+
processed_at: z2.number().int().optional()
|
|
1021
1213
|
}).strict();
|
|
1022
|
-
// src/
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
"pyne-worker": "pyne:"
|
|
1036
|
-
};
|
|
1037
|
-
var DASHBOARD_SECTION_PREFIX = {
|
|
1038
|
-
global: "global:",
|
|
1039
|
-
webhook: "webhook:",
|
|
1040
|
-
routing: "routing:",
|
|
1041
|
-
security: "webhook:",
|
|
1042
|
-
trade: "trade:",
|
|
1043
|
-
agent: "agent:",
|
|
1044
|
-
bot: "bot:",
|
|
1045
|
-
email: "email:",
|
|
1046
|
-
signal: "email:",
|
|
1047
|
-
database: "database:",
|
|
1048
|
-
retention: "retention:",
|
|
1049
|
-
cron: "cron:",
|
|
1050
|
-
behavior: "behavior:",
|
|
1051
|
-
risk: "trade:",
|
|
1052
|
-
exchanges: "trade:",
|
|
1053
|
-
fees: "trade:",
|
|
1054
|
-
providers: "agent:",
|
|
1055
|
-
models: "agent:",
|
|
1056
|
-
ai: "ai:",
|
|
1057
|
-
report: "report:",
|
|
1058
|
-
pyne: "pyne:",
|
|
1059
|
-
pyne_alerts: "pyne:"
|
|
1060
|
-
};
|
|
1061
|
-
var DASHBOARD_FIELD_KV_OVERRIDES = {
|
|
1062
|
-
"webhook:tradingview_ip_check_enabled": "webhook:tradingview:ip_check_enabled",
|
|
1063
|
-
"webhook:tradingview_allowed_ips": "webhook:tradingview:allowed_ips",
|
|
1064
|
-
"exchanges:binance_enabled": "exchange:binance:enabled",
|
|
1065
|
-
"exchanges:mexc_enabled": "exchange:mexc:enabled",
|
|
1066
|
-
"exchanges:bybit_enabled": "exchange:bybit:enabled"
|
|
1067
|
-
};
|
|
1068
|
-
var DASHBOARD_SECTIONS_NOT_FLAT_KV = new Set([
|
|
1069
|
-
"providers",
|
|
1070
|
-
"models",
|
|
1071
|
-
"cron",
|
|
1072
|
-
"behavior"
|
|
1073
|
-
]);
|
|
1074
|
-
var DASHBOARD_SECTIONS_UI_SKIP = new Set([
|
|
1075
|
-
"cron",
|
|
1076
|
-
"behavior"
|
|
1077
|
-
]);
|
|
1078
|
-
function isDashboardSectionFlatKv(sectionId) {
|
|
1079
|
-
return !DASHBOARD_SECTIONS_NOT_FLAT_KV.has(sectionId);
|
|
1214
|
+
// src/middleware/auth.ts
|
|
1215
|
+
function timingSafeEqual(a, b) {
|
|
1216
|
+
const encoder = new TextEncoder;
|
|
1217
|
+
const aBuf = encoder.encode(a);
|
|
1218
|
+
const bBuf = encoder.encode(b);
|
|
1219
|
+
const len = Math.max(aBuf.length, bBuf.length);
|
|
1220
|
+
let result = aBuf.length === bBuf.length ? 0 : 1;
|
|
1221
|
+
for (let i = 0;i < len; i++) {
|
|
1222
|
+
const av = aBuf[i] ?? 0;
|
|
1223
|
+
const bv = bBuf[i] ?? 0;
|
|
1224
|
+
result |= av ^ bv;
|
|
1225
|
+
}
|
|
1226
|
+
return result === 0;
|
|
1080
1227
|
}
|
|
1081
|
-
function
|
|
1082
|
-
|
|
1228
|
+
async function timingSafeEqualAsync(a, b) {
|
|
1229
|
+
const subtle = globalThis.crypto?.subtle;
|
|
1230
|
+
if (!subtle || typeof subtle.digest !== "function" || typeof subtle.timingSafeEqual !== "function") {
|
|
1231
|
+
return timingSafeEqual(a, b);
|
|
1232
|
+
}
|
|
1233
|
+
const encoder = new TextEncoder;
|
|
1234
|
+
const [providedHash, expectedHash] = await Promise.all([
|
|
1235
|
+
subtle.digest("SHA-256", encoder.encode(a)),
|
|
1236
|
+
subtle.digest("SHA-256", encoder.encode(b))
|
|
1237
|
+
]);
|
|
1238
|
+
return subtle.timingSafeEqual(providedHash, expectedHash);
|
|
1083
1239
|
}
|
|
1084
|
-
function
|
|
1085
|
-
const
|
|
1086
|
-
if (
|
|
1087
|
-
return
|
|
1088
|
-
if (fieldKey.includes(":")) {
|
|
1089
|
-
const [section, ...rest] = fieldKey.split(":");
|
|
1090
|
-
const fieldName = rest.join(":");
|
|
1091
|
-
if (section === "exchanges" && fieldName.endsWith("_enabled")) {
|
|
1092
|
-
const exchange = fieldName.slice(0, -"_enabled".length);
|
|
1093
|
-
if (exchange)
|
|
1094
|
-
return `exchange:${exchange}:enabled`;
|
|
1095
|
-
}
|
|
1096
|
-
const mapped = DASHBOARD_SECTION_PREFIX[section ?? ""];
|
|
1097
|
-
if (mapped)
|
|
1098
|
-
return `${mapped}${fieldName}`;
|
|
1099
|
-
return `${section}:${fieldName}`;
|
|
1240
|
+
function resolveOperatorApiKey(env) {
|
|
1241
|
+
const preferred = env.OPERATOR_API_KEY;
|
|
1242
|
+
if (typeof preferred === "string" && preferred.length > 0) {
|
|
1243
|
+
return preferred;
|
|
1100
1244
|
}
|
|
1101
|
-
const
|
|
1102
|
-
|
|
1245
|
+
const legacy = env.INTERNAL_API_KEY;
|
|
1246
|
+
if (typeof legacy === "string" && legacy.length > 0) {
|
|
1247
|
+
return legacy;
|
|
1248
|
+
}
|
|
1249
|
+
return;
|
|
1103
1250
|
}
|
|
1104
|
-
function
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
let j = i + 1;
|
|
1146
|
-
while (j < input.length && /\s/.test(input[j]))
|
|
1147
|
-
j++;
|
|
1148
|
-
if (j < input.length && (input[j] === "}" || input[j] === "]")) {
|
|
1149
|
-
i++;
|
|
1150
|
-
continue;
|
|
1151
|
-
}
|
|
1251
|
+
async function requireAuth(request, env) {
|
|
1252
|
+
return requireOperatorAuth(request, env);
|
|
1253
|
+
}
|
|
1254
|
+
async function requireOperatorAuth(request, env) {
|
|
1255
|
+
const apiKey = resolveOperatorApiKey(env);
|
|
1256
|
+
if (!apiKey) {
|
|
1257
|
+
return new Response(JSON.stringify({
|
|
1258
|
+
error: "Operator API key not configured",
|
|
1259
|
+
hint: "Set OPERATOR_API_KEY (preferred) or INTERNAL_API_KEY on the Worker"
|
|
1260
|
+
}), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
1261
|
+
}
|
|
1262
|
+
const authHeader = request.headers.get("Authorization");
|
|
1263
|
+
const expectedHeader = `Bearer ${apiKey}`;
|
|
1264
|
+
if (!authHeader || !await timingSafeEqualAsync(authHeader, expectedHeader)) {
|
|
1265
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
1266
|
+
status: 401,
|
|
1267
|
+
headers: { "Content-Type": "application/json" }
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
return null;
|
|
1271
|
+
}
|
|
1272
|
+
function createOperatorAuthMiddleware() {
|
|
1273
|
+
return async (request, env, _ctx) => {
|
|
1274
|
+
const denied = await requireOperatorAuth(request, env);
|
|
1275
|
+
if (denied)
|
|
1276
|
+
return denied;
|
|
1277
|
+
return;
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
function normalizeKeyNames(keyName) {
|
|
1281
|
+
if (typeof keyName === "string") {
|
|
1282
|
+
return [keyName];
|
|
1283
|
+
}
|
|
1284
|
+
return [...keyName];
|
|
1285
|
+
}
|
|
1286
|
+
function collectInternalAuthKeys(env, keyNames) {
|
|
1287
|
+
const keys = [];
|
|
1288
|
+
for (const name of normalizeKeyNames(keyNames)) {
|
|
1289
|
+
const value = env[name];
|
|
1290
|
+
if (typeof value === "string" && value.length > 0) {
|
|
1291
|
+
keys.push(value);
|
|
1152
1292
|
}
|
|
1153
|
-
out += c;
|
|
1154
|
-
i++;
|
|
1155
1293
|
}
|
|
1156
|
-
return
|
|
1294
|
+
return keys;
|
|
1157
1295
|
}
|
|
1158
|
-
function
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
return "json";
|
|
1167
|
-
if (!Number.isNaN(Number(value)) && value.trim() !== "")
|
|
1168
|
-
return "number";
|
|
1169
|
-
return "text";
|
|
1296
|
+
function matchesAnyInternalAuthKey(providedKey, expectedKeys) {
|
|
1297
|
+
let matched = false;
|
|
1298
|
+
for (const expected of expectedKeys) {
|
|
1299
|
+
if (timingSafeEqual(providedKey, expected)) {
|
|
1300
|
+
matched = true;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return matched;
|
|
1170
1304
|
}
|
|
1171
|
-
function
|
|
1172
|
-
|
|
1305
|
+
function internalAuthNotConfiguredResponse(keyNames) {
|
|
1306
|
+
const label = normalizeKeyNames(keyNames).join(" | ");
|
|
1307
|
+
return new Response(JSON.stringify({
|
|
1308
|
+
success: false,
|
|
1309
|
+
error: `Internal auth key(s) not configured: ${label}`
|
|
1310
|
+
}), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
1173
1311
|
}
|
|
1174
|
-
function
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
const field = {
|
|
1191
|
-
key: `${sectionId}:${key}`,
|
|
1192
|
-
label: labelFromKey(key),
|
|
1193
|
-
type: parseFieldType(value),
|
|
1194
|
-
default: value
|
|
1195
|
-
};
|
|
1196
|
-
if (sectionOptions[key]) {
|
|
1197
|
-
field.type = "select";
|
|
1198
|
-
field.options = sectionOptions[key].map((opt) => ({
|
|
1199
|
-
value: String(opt),
|
|
1200
|
-
label: String(opt)
|
|
1201
|
-
}));
|
|
1202
|
-
}
|
|
1203
|
-
if (sectionDescriptions[key]) {
|
|
1204
|
-
field.description = String(sectionDescriptions[key]);
|
|
1205
|
-
}
|
|
1206
|
-
if (sectionValidation[key]) {
|
|
1207
|
-
field.validation = sectionValidation[key];
|
|
1208
|
-
}
|
|
1209
|
-
if (sectionSecrets[key]) {
|
|
1210
|
-
field.kind = "secret";
|
|
1211
|
-
field.cliCommand = sectionSecretCommands[key];
|
|
1212
|
-
}
|
|
1213
|
-
if (key.includes("kill_switch") || key.includes("drawdown") || key === "enabled" && sectionId === "global") {
|
|
1214
|
-
if (field.kind !== "secret")
|
|
1215
|
-
field.kind = "dangerous";
|
|
1216
|
-
}
|
|
1217
|
-
fields.push(field);
|
|
1218
|
-
}
|
|
1219
|
-
sections.push({
|
|
1220
|
-
id: sectionId,
|
|
1221
|
-
title: sectionData.title || labelFromKey(sectionId),
|
|
1222
|
-
description: sectionData.description || `Configure ${sectionId} settings`,
|
|
1223
|
-
icon: sectionData.icon,
|
|
1224
|
-
priority: sectionData.priority !== undefined ? sectionData.priority : sections.length * 10,
|
|
1225
|
-
fields
|
|
1226
|
-
});
|
|
1227
|
-
}
|
|
1312
|
+
function requireInternalAuth(request, env, keyName = "INTERNAL_KEY_BINDING") {
|
|
1313
|
+
const expectedKeys = collectInternalAuthKeys(env, keyName);
|
|
1314
|
+
if (expectedKeys.length === 0) {
|
|
1315
|
+
return internalAuthNotConfiguredResponse(keyName);
|
|
1316
|
+
}
|
|
1317
|
+
const providedKey = request.headers.get("X-Internal-Auth-Key");
|
|
1318
|
+
if (!providedKey || !matchesAnyInternalAuthKey(providedKey, expectedKeys)) {
|
|
1319
|
+
return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
1320
|
+
}
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
function createInternalAuthMiddleware(keyName = "INTERNAL_KEY_BINDING") {
|
|
1324
|
+
return async (request, env, _ctx) => {
|
|
1325
|
+
const expectedKeys = collectInternalAuthKeys(env, keyName);
|
|
1326
|
+
if (expectedKeys.length === 0) {
|
|
1327
|
+
return internalAuthNotConfiguredResponse(keyName);
|
|
1228
1328
|
}
|
|
1329
|
+
const providedKey = request.headers.get("X-Internal-Auth-Key");
|
|
1330
|
+
if (!providedKey || !matchesAnyInternalAuthKey(providedKey, expectedKeys)) {
|
|
1331
|
+
return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
1332
|
+
}
|
|
1333
|
+
return;
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
function checkInternalAuth(request, env, keyName = "INTERNAL_KEY_BINDING") {
|
|
1337
|
+
const expectedKeys = collectInternalAuthKeys(env, keyName);
|
|
1338
|
+
if (expectedKeys.length === 0) {
|
|
1229
1339
|
return {
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
description,
|
|
1233
|
-
sections: sections.sort((a, b) => a.priority - b.priority)
|
|
1234
|
-
};
|
|
1235
|
-
} catch (err) {
|
|
1236
|
-
const message = err instanceof Error ? err.message : `parse failed: ${String(err)}`;
|
|
1237
|
-
onError?.(`dashboard.jsonc for ${workerName}: ${message}`);
|
|
1238
|
-
return {
|
|
1239
|
-
worker: workerName,
|
|
1240
|
-
displayName: workerName,
|
|
1241
|
-
description: "Failed to load configuration",
|
|
1242
|
-
sections: []
|
|
1340
|
+
authorized: false,
|
|
1341
|
+
error: `${normalizeKeyNames(keyName).join(" | ")} not configured`
|
|
1243
1342
|
};
|
|
1244
1343
|
}
|
|
1344
|
+
const providedKey = request.headers.get("X-Internal-Auth-Key");
|
|
1345
|
+
if (!providedKey || !matchesAnyInternalAuthKey(providedKey, expectedKeys)) {
|
|
1346
|
+
return { authorized: false, error: "Unauthorized" };
|
|
1347
|
+
}
|
|
1348
|
+
return { authorized: true };
|
|
1245
1349
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
"
|
|
1250
|
-
"
|
|
1251
|
-
"
|
|
1252
|
-
"
|
|
1253
|
-
"
|
|
1254
|
-
"
|
|
1255
|
-
"
|
|
1256
|
-
"
|
|
1350
|
+
|
|
1351
|
+
// src/service-bindings.ts
|
|
1352
|
+
var InternalAuthKeyFields = {
|
|
1353
|
+
DEFAULT: "INTERNAL_KEY_BINDING",
|
|
1354
|
+
D1_READ: "D1_READ_KEY_BINDING",
|
|
1355
|
+
D1_WRITE: "D1_WRITE_KEY_BINDING",
|
|
1356
|
+
TRADE_EXECUTE: "TRADE_EXECUTE_KEY_BINDING",
|
|
1357
|
+
TRADE_READ: "TRADE_READ_KEY_BINDING",
|
|
1358
|
+
TELEGRAM_ALERT: "TELEGRAM_INTERNAL_KEY_BINDING",
|
|
1359
|
+
WALLET_EXECUTE: "WALLET_EXECUTE_KEY_BINDING",
|
|
1360
|
+
AGENT: "AGENT_INTERNAL_KEY",
|
|
1361
|
+
D1_INTERNAL_ALIAS: "D1_INTERNAL_KEY",
|
|
1362
|
+
TRADE_INTERNAL_ALIAS: "TRADE_INTERNAL_KEY",
|
|
1363
|
+
TELEGRAM_INTERNAL_ALIAS: "TELEGRAM_INTERNAL_KEY"
|
|
1364
|
+
};
|
|
1365
|
+
var D1_READ_AUTH_KEY_FIELDS = [
|
|
1366
|
+
InternalAuthKeyFields.D1_READ,
|
|
1367
|
+
InternalAuthKeyFields.DEFAULT
|
|
1257
1368
|
];
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1369
|
+
var D1_WRITE_AUTH_KEY_FIELDS = [
|
|
1370
|
+
InternalAuthKeyFields.D1_WRITE,
|
|
1371
|
+
InternalAuthKeyFields.DEFAULT
|
|
1372
|
+
];
|
|
1373
|
+
var TRADE_EXECUTE_AUTH_KEY_FIELDS = [
|
|
1374
|
+
InternalAuthKeyFields.TRADE_EXECUTE,
|
|
1375
|
+
InternalAuthKeyFields.DEFAULT,
|
|
1376
|
+
InternalAuthKeyFields.AGENT
|
|
1377
|
+
];
|
|
1378
|
+
var TRADE_READ_AUTH_KEY_FIELDS = [
|
|
1379
|
+
InternalAuthKeyFields.TRADE_READ,
|
|
1380
|
+
InternalAuthKeyFields.DEFAULT
|
|
1381
|
+
];
|
|
1382
|
+
var TELEGRAM_ALERT_AUTH_KEY_FIELDS = [
|
|
1383
|
+
InternalAuthKeyFields.TELEGRAM_ALERT,
|
|
1384
|
+
InternalAuthKeyFields.DEFAULT
|
|
1385
|
+
];
|
|
1386
|
+
var WALLET_EXECUTE_AUTH_KEY_FIELDS = [
|
|
1387
|
+
InternalAuthKeyFields.WALLET_EXECUTE,
|
|
1388
|
+
InternalAuthKeyFields.DEFAULT
|
|
1389
|
+
];
|
|
1390
|
+
var DASHBOARD_D1_READ_AUTH_KEY_FIELDS = [
|
|
1391
|
+
InternalAuthKeyFields.D1_READ,
|
|
1392
|
+
InternalAuthKeyFields.D1_INTERNAL_ALIAS,
|
|
1393
|
+
InternalAuthKeyFields.DEFAULT
|
|
1394
|
+
];
|
|
1395
|
+
var DASHBOARD_TRADE_EXECUTE_AUTH_KEY_FIELDS = [
|
|
1396
|
+
InternalAuthKeyFields.TRADE_EXECUTE,
|
|
1397
|
+
InternalAuthKeyFields.TRADE_INTERNAL_ALIAS,
|
|
1398
|
+
InternalAuthKeyFields.DEFAULT
|
|
1399
|
+
];
|
|
1400
|
+
var DASHBOARD_TELEGRAM_ALERT_AUTH_KEY_FIELDS = [
|
|
1401
|
+
InternalAuthKeyFields.TELEGRAM_ALERT,
|
|
1402
|
+
InternalAuthKeyFields.TELEGRAM_INTERNAL_ALIAS,
|
|
1403
|
+
InternalAuthKeyFields.DEFAULT
|
|
1404
|
+
];
|
|
1405
|
+
function resolveInternalAuthKey(env, keyFields) {
|
|
1406
|
+
return collectInternalAuthKeys(env, keyFields)[0];
|
|
1278
1407
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
continue;
|
|
1285
|
-
for (const field of section.fields) {
|
|
1286
|
-
if (isDocumentationField(field.key))
|
|
1287
|
-
continue;
|
|
1288
|
-
const key = buildDashboardKvKey(manifest.worker, field.key);
|
|
1289
|
-
if (!key || key.endsWith(":"))
|
|
1290
|
-
continue;
|
|
1291
|
-
const next = {
|
|
1292
|
-
key,
|
|
1293
|
-
type: fieldTypeToKv(field.type),
|
|
1294
|
-
default: defaultToString(field.default),
|
|
1295
|
-
description: field.description || `${manifest.displayName}: ${field.label}`,
|
|
1296
|
-
secret: field.kind === "secret" ? true : undefined,
|
|
1297
|
-
worker: manifest.worker
|
|
1298
|
-
};
|
|
1299
|
-
const prev = byKey.get(key);
|
|
1300
|
-
if (!prev) {
|
|
1301
|
-
byKey.set(key, next);
|
|
1302
|
-
continue;
|
|
1303
|
-
}
|
|
1304
|
-
if (!prev.secret && next.secret)
|
|
1305
|
-
prev.secret = true;
|
|
1306
|
-
if ((!prev.description || prev.description.length < 8) && next.description) {
|
|
1307
|
-
prev.description = next.description;
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1408
|
+
|
|
1409
|
+
class ServiceAuthError extends Error {
|
|
1410
|
+
constructor(message) {
|
|
1411
|
+
super(message);
|
|
1412
|
+
this.name = "ServiceAuthError";
|
|
1311
1413
|
}
|
|
1312
|
-
const keys = [...byKey.values()].sort((a, b) => a.key.localeCompare(b.key));
|
|
1313
|
-
return { namespace: "CONFIG_KV", keys };
|
|
1314
|
-
}
|
|
1315
|
-
function loadDashboardKvManifestFromRoot(root, onError) {
|
|
1316
|
-
return kvManifestFromDashboardManifests(loadDashboardManifestsFromRoot(root, onError));
|
|
1317
1414
|
}
|
|
1318
|
-
function
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
return
|
|
1323
|
-
} catch {
|
|
1324
|
-
return null;
|
|
1415
|
+
function authenticatedServiceFetch(binding, env, path, body, options) {
|
|
1416
|
+
const key = options?.internalKey ?? (options?.internalKeyFields ? resolveInternalAuthKey(env, options.internalKeyFields) : env.INTERNAL_KEY_BINDING);
|
|
1417
|
+
if (!key) {
|
|
1418
|
+
const label = options?.internalKeyFields ? Array.isArray(options.internalKeyFields) ? options.internalKeyFields.join(" | ") : options.internalKeyFields : "INTERNAL_KEY_BINDING";
|
|
1419
|
+
return Promise.reject(new ServiceAuthError(`${label} not configured`));
|
|
1325
1420
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
const warn = onError ?? ((message) => {
|
|
1334
|
-
console.warn(`[dashboard-manifest] ${message}`);
|
|
1421
|
+
return serviceFetch(binding, path, body, {
|
|
1422
|
+
method: options?.method,
|
|
1423
|
+
timeout: options?.timeout,
|
|
1424
|
+
headers: {
|
|
1425
|
+
...options?.headers,
|
|
1426
|
+
"X-Internal-Auth-Key": key
|
|
1427
|
+
}
|
|
1335
1428
|
});
|
|
1336
|
-
for (const workerId of DASHBOARD_WORKER_IDS) {
|
|
1337
|
-
const dir = dashboardWorkerDir(workerId);
|
|
1338
|
-
const path = join2(workersDir, dir, "dashboard.jsonc");
|
|
1339
|
-
const content = tryReadFile(path);
|
|
1340
|
-
if (!content)
|
|
1341
|
-
continue;
|
|
1342
|
-
const m = parseDashboardManifest(content, workerId, warn);
|
|
1343
|
-
if (m.sections.length > 0) {
|
|
1344
|
-
manifests.push(m);
|
|
1345
|
-
seen.add(workerId);
|
|
1346
|
-
} else if (m.description === "Failed to load configuration") {}
|
|
1347
|
-
}
|
|
1348
|
-
try {
|
|
1349
|
-
for (const entry of readdirSync(workersDir)) {
|
|
1350
|
-
if (entry === "dashboard")
|
|
1351
|
-
continue;
|
|
1352
|
-
const workerId = entry === "hoox-worker" || entry === "hoox" ? "hoox" : entry;
|
|
1353
|
-
if (seen.has(workerId))
|
|
1354
|
-
continue;
|
|
1355
|
-
const path = join2(workersDir, entry, "dashboard.jsonc");
|
|
1356
|
-
const content = tryReadFile(path);
|
|
1357
|
-
if (!content)
|
|
1358
|
-
continue;
|
|
1359
|
-
const m = parseDashboardManifest(content, workerId, warn);
|
|
1360
|
-
if (m.sections.length > 0) {
|
|
1361
|
-
manifests.push(m);
|
|
1362
|
-
seen.add(workerId);
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
} catch {}
|
|
1366
|
-
const publicDir = join2(workersDir, "dashboard", "public", "workers");
|
|
1367
|
-
if (existsSync2(publicDir)) {
|
|
1368
|
-
try {
|
|
1369
|
-
for (const file of readdirSync(publicDir)) {
|
|
1370
|
-
if (!file.endsWith(".jsonc") && !file.endsWith(".json"))
|
|
1371
|
-
continue;
|
|
1372
|
-
const base = file.replace(/\.jsonc?$/, "");
|
|
1373
|
-
const workerId = base === "hoox-worker" || base === "hoox" ? "hoox" : base;
|
|
1374
|
-
if (seen.has(workerId))
|
|
1375
|
-
continue;
|
|
1376
|
-
const content = tryReadFile(join2(publicDir, file));
|
|
1377
|
-
if (!content)
|
|
1378
|
-
continue;
|
|
1379
|
-
const m = parseDashboardManifest(content, workerId, warn);
|
|
1380
|
-
if (m.sections.length > 0) {
|
|
1381
|
-
manifests.push(m);
|
|
1382
|
-
seen.add(workerId);
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
} catch {}
|
|
1386
|
-
}
|
|
1387
|
-
return manifests;
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
// src/exchange-client.ts
|
|
1391
|
-
class ExchangeRouter {
|
|
1392
|
-
providers = new Map;
|
|
1393
|
-
registerProvider(provider) {
|
|
1394
|
-
this.providers.set(provider.name.toLowerCase(), provider);
|
|
1395
|
-
}
|
|
1396
|
-
route(exchange, env, options) {
|
|
1397
|
-
const key = exchange.toLowerCase();
|
|
1398
|
-
const provider = this.providers.get(key);
|
|
1399
|
-
if (!provider) {
|
|
1400
|
-
throw new Error(`Unsupported exchange: ${exchange}`);
|
|
1401
|
-
}
|
|
1402
|
-
if (options?.testnet && provider.supportsTestTrading === false) {
|
|
1403
|
-
throw new Error(`TEST_TRADING_UNSUPPORTED: ${exchange} does not support test/sandbox trading via API`);
|
|
1404
|
-
}
|
|
1405
|
-
if (!provider.hasCredentials(env)) {
|
|
1406
|
-
throw new Error(`API secret bindings not configured or accessible for ${exchange}`);
|
|
1407
|
-
}
|
|
1408
|
-
return { exchange: key, client: provider.createClient(env, options) };
|
|
1409
|
-
}
|
|
1410
1429
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
this.apiSecret = config.apiSecret;
|
|
1423
|
-
this.baseUrl = config.baseUrl || this.getDefaultBaseUrl();
|
|
1424
|
-
this.isTestnet = config.testnet || false;
|
|
1425
|
-
}
|
|
1426
|
-
async fetch(method, path, params) {
|
|
1427
|
-
const url = `${this.baseUrl}${path}`;
|
|
1428
|
-
const headers = this.buildHeaders(method, path, params);
|
|
1429
|
-
const body = params ? new URLSearchParams(params).toString() : undefined;
|
|
1430
|
-
const response = await fetch(url, {
|
|
1431
|
-
method,
|
|
1432
|
-
headers,
|
|
1433
|
-
body: method !== "GET" ? body : undefined
|
|
1434
|
-
});
|
|
1435
|
-
if (!response.ok) {
|
|
1436
|
-
const error = await response.text();
|
|
1437
|
-
throw new Error(`Exchange API error: ${response.status} - ${error}`);
|
|
1438
|
-
}
|
|
1439
|
-
return response.json();
|
|
1440
|
-
}
|
|
1441
|
-
async openLong(symbol, quantity, price, orderType = "MARKET") {
|
|
1442
|
-
return this.executeTrade({
|
|
1443
|
-
symbol,
|
|
1444
|
-
side: "long",
|
|
1445
|
-
orderType,
|
|
1446
|
-
quantity,
|
|
1447
|
-
price
|
|
1448
|
-
});
|
|
1449
|
-
}
|
|
1450
|
-
async openShort(symbol, quantity, price, orderType = "MARKET") {
|
|
1451
|
-
return this.executeTrade({
|
|
1452
|
-
symbol,
|
|
1453
|
-
side: "short",
|
|
1454
|
-
orderType,
|
|
1455
|
-
quantity,
|
|
1456
|
-
price
|
|
1457
|
-
});
|
|
1458
|
-
}
|
|
1459
|
-
async closeLong(symbol, quantity) {
|
|
1460
|
-
return this.executeTrade({
|
|
1461
|
-
symbol,
|
|
1462
|
-
side: "long",
|
|
1463
|
-
quantity,
|
|
1464
|
-
reduceOnly: true
|
|
1465
|
-
});
|
|
1466
|
-
}
|
|
1467
|
-
async closeShort(symbol, quantity) {
|
|
1468
|
-
return this.executeTrade({
|
|
1469
|
-
symbol,
|
|
1470
|
-
side: "short",
|
|
1471
|
-
quantity,
|
|
1472
|
-
reduceOnly: true
|
|
1473
|
-
});
|
|
1474
|
-
}
|
|
1430
|
+
function serviceFetch(binding, path, body, options) {
|
|
1431
|
+
const timeout = options?.timeout ?? 30000;
|
|
1432
|
+
return binding.fetch(`http://internal${path}`, {
|
|
1433
|
+
method: options?.method ?? "POST",
|
|
1434
|
+
headers: {
|
|
1435
|
+
"Content-Type": "application/json",
|
|
1436
|
+
...options?.headers
|
|
1437
|
+
},
|
|
1438
|
+
body: body != null ? JSON.stringify(body) : undefined,
|
|
1439
|
+
signal: AbortSignal.timeout(timeout)
|
|
1440
|
+
});
|
|
1475
1441
|
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
}
|
|
1488
|
-
this.apiKey = apiKey;
|
|
1489
|
-
this.apiSecret = apiSecret;
|
|
1490
|
-
this.isTestnet = options?.testnet ?? false;
|
|
1491
|
-
this.baseUrl = options?.baseUrl ?? this.getDefaultBaseUrl();
|
|
1492
|
-
this.importedKeyPromise = this.importKey();
|
|
1493
|
-
}
|
|
1494
|
-
get testnet() {
|
|
1495
|
-
return this.isTestnet;
|
|
1496
|
-
}
|
|
1497
|
-
getErrorMessagePrefix() {
|
|
1498
|
-
return "";
|
|
1499
|
-
}
|
|
1500
|
-
generateSignature(_params) {
|
|
1501
|
-
throw new Error("generateSignature must be implemented by subclass");
|
|
1502
|
-
}
|
|
1503
|
-
buildHeaders(_method, _path, _params) {
|
|
1504
|
-
return new Headers;
|
|
1505
|
-
}
|
|
1506
|
-
async importKey() {
|
|
1507
|
-
const encoder = new TextEncoder;
|
|
1508
|
-
return crypto.subtle.importKey("raw", encoder.encode(this.apiSecret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
1509
|
-
}
|
|
1510
|
-
async cryptoSign(data) {
|
|
1511
|
-
const encoder = new TextEncoder;
|
|
1512
|
-
const key = await this.importedKeyPromise;
|
|
1513
|
-
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
|
|
1514
|
-
return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1515
|
-
}
|
|
1516
|
-
async fetch(method, path, params) {
|
|
1517
|
-
const url = `${this.baseUrl}${path}`;
|
|
1518
|
-
const headers = this.buildHeaders(method, path, params);
|
|
1519
|
-
const body = params ? new URLSearchParams(params).toString() : undefined;
|
|
1520
|
-
const response = await fetch(url, {
|
|
1521
|
-
method,
|
|
1522
|
-
headers,
|
|
1523
|
-
body: method !== "GET" ? body : undefined
|
|
1524
|
-
});
|
|
1525
|
-
if (!response.ok) {
|
|
1526
|
-
const error = await response.text();
|
|
1527
|
-
throw new Error(`Exchange API error: ${response.status} - ${error}`);
|
|
1528
|
-
}
|
|
1529
|
-
return response.json();
|
|
1530
|
-
}
|
|
1531
|
-
setLeverage(_symbol, _leverage) {
|
|
1532
|
-
throw new Error("setLeverage must be implemented by subclass");
|
|
1533
|
-
}
|
|
1534
|
-
executeTrade(_params) {
|
|
1535
|
-
throw new Error("executeTrade must be implemented by subclass");
|
|
1536
|
-
}
|
|
1537
|
-
getAccountInfo() {
|
|
1538
|
-
throw new Error("getAccountInfo must be implemented by subclass");
|
|
1539
|
-
}
|
|
1540
|
-
getPositions(_symbol) {
|
|
1541
|
-
throw new Error("getPositions must be implemented by subclass");
|
|
1542
|
-
}
|
|
1543
|
-
async openLong(symbol, quantity, price, orderType = "MARKET") {
|
|
1544
|
-
return this.executeTrade({
|
|
1545
|
-
symbol,
|
|
1546
|
-
side: "BUY",
|
|
1547
|
-
orderType,
|
|
1548
|
-
quantity,
|
|
1549
|
-
price
|
|
1550
|
-
});
|
|
1551
|
-
}
|
|
1552
|
-
async openShort(symbol, quantity, price, orderType = "MARKET") {
|
|
1553
|
-
return this.executeTrade({
|
|
1554
|
-
symbol,
|
|
1555
|
-
side: "SELL",
|
|
1556
|
-
orderType,
|
|
1557
|
-
quantity,
|
|
1558
|
-
price
|
|
1559
|
-
});
|
|
1560
|
-
}
|
|
1561
|
-
async closeLong(symbol, quantity) {
|
|
1562
|
-
return this.executeTrade({
|
|
1563
|
-
symbol,
|
|
1564
|
-
side: "SELL",
|
|
1565
|
-
orderType: "MARKET",
|
|
1566
|
-
quantity,
|
|
1567
|
-
reduceOnly: true
|
|
1568
|
-
});
|
|
1569
|
-
}
|
|
1570
|
-
async closeShort(symbol, quantity) {
|
|
1571
|
-
return this.executeTrade({
|
|
1572
|
-
symbol,
|
|
1573
|
-
side: "BUY",
|
|
1574
|
-
orderType: "MARKET",
|
|
1575
|
-
quantity,
|
|
1576
|
-
reduceOnly: true
|
|
1577
|
-
});
|
|
1578
|
-
}
|
|
1442
|
+
// src/test-utils.ts
|
|
1443
|
+
function createMockKV(overrides) {
|
|
1444
|
+
return {
|
|
1445
|
+
get: async (_key) => null,
|
|
1446
|
+
put: async (_key, _value, _options) => {},
|
|
1447
|
+
delete: async (_key) => {},
|
|
1448
|
+
list: async (_options) => ({
|
|
1449
|
+
keys: []
|
|
1450
|
+
}),
|
|
1451
|
+
...overrides
|
|
1452
|
+
};
|
|
1579
1453
|
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
if (seconds < 60)
|
|
1593
|
-
return "< 1m ago";
|
|
1594
|
-
if (minutes < 60)
|
|
1595
|
-
return `${minutes}m ago`;
|
|
1596
|
-
if (hours < 24)
|
|
1597
|
-
return `${hours}h ago`;
|
|
1598
|
-
if (days < 30)
|
|
1599
|
-
return `${days}d ago`;
|
|
1600
|
-
return `> 30d ago`;
|
|
1454
|
+
function createMockR2(overrides) {
|
|
1455
|
+
return {
|
|
1456
|
+
put: async (key, _value, _options) => ({
|
|
1457
|
+
key
|
|
1458
|
+
}),
|
|
1459
|
+
get: async (_key) => null,
|
|
1460
|
+
delete: async (_key) => {},
|
|
1461
|
+
list: async (_options) => ({
|
|
1462
|
+
objects: []
|
|
1463
|
+
}),
|
|
1464
|
+
...overrides
|
|
1465
|
+
};
|
|
1601
1466
|
}
|
|
1602
|
-
function
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
}
|
|
1610
|
-
if (minutes > 0) {
|
|
1611
|
-
const remainSec = seconds % 60;
|
|
1612
|
-
return remainSec > 0 ? `${minutes}m ${remainSec}s` : `${minutes}m`;
|
|
1613
|
-
}
|
|
1614
|
-
return `${seconds}s`;
|
|
1467
|
+
function createMockPreparedStatement() {
|
|
1468
|
+
return {
|
|
1469
|
+
bind: (..._params) => createMockPreparedStatement(),
|
|
1470
|
+
run: async () => ({ success: true }),
|
|
1471
|
+
all: async () => ({ success: true, results: [] }),
|
|
1472
|
+
first: async () => null
|
|
1473
|
+
};
|
|
1615
1474
|
}
|
|
1616
|
-
|
|
1475
|
+
function createMockD1(overrides) {
|
|
1476
|
+
return {
|
|
1477
|
+
prepare: (_sql) => createMockPreparedStatement(),
|
|
1478
|
+
dump: async () => new ArrayBuffer(0),
|
|
1479
|
+
batch: async (_statements) => [],
|
|
1480
|
+
exec: async (_sql) => ({ success: true }),
|
|
1481
|
+
...overrides
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
function createMockFetcher(response) {
|
|
1485
|
+
const defaultResponse = response ?? new Response("OK", { status: 200 });
|
|
1486
|
+
return {
|
|
1487
|
+
fetch: async (_request, _init) => defaultResponse
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
function createMockAnalyticsEngine() {
|
|
1491
|
+
return {
|
|
1492
|
+
writeDataPoint: (_data) => {}
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
function createMockAnalyticsEngineDataset() {
|
|
1496
|
+
return createMockAnalyticsEngine();
|
|
1497
|
+
}
|
|
1498
|
+
function createMockQueue() {
|
|
1499
|
+
return {
|
|
1500
|
+
send: async (_body, _options) => {},
|
|
1501
|
+
sendBatch: async (_messages) => {}
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
function createMockVectorizeIndex(overrides) {
|
|
1505
|
+
return {
|
|
1506
|
+
query: async (_vector, _options) => ({
|
|
1507
|
+
matches: []
|
|
1508
|
+
}),
|
|
1509
|
+
upsert: async (_vectors) => {},
|
|
1510
|
+
deleteByIds: async (_ids) => {},
|
|
1511
|
+
describe: async () => ({ dimension: 384, totalVectors: 0 }),
|
|
1512
|
+
...overrides
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
function createMockAi(overrides) {
|
|
1516
|
+
return {
|
|
1517
|
+
run: async (_model, _input) => ({}),
|
|
1518
|
+
...overrides
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
function createMockExecutionContext(overrides) {
|
|
1522
|
+
return {
|
|
1523
|
+
waitUntil: (_promise) => {},
|
|
1524
|
+
passThroughOnException: () => {},
|
|
1525
|
+
...overrides
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
function createMockSecretsStore(overrides) {
|
|
1529
|
+
return {
|
|
1530
|
+
get: async (_key) => null,
|
|
1531
|
+
put: async (_key, _value) => {},
|
|
1532
|
+
delete: async (_key) => {},
|
|
1533
|
+
...overrides
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
function createMockEnv(overrides) {
|
|
1537
|
+
return overrides;
|
|
1538
|
+
}
|
|
1539
|
+
// src/colors.ts
|
|
1540
|
+
var Colors = {
|
|
1541
|
+
background: "#050508",
|
|
1542
|
+
foreground: "#E8E8F0",
|
|
1543
|
+
card: "#0A0A0F",
|
|
1544
|
+
border: "#232330",
|
|
1545
|
+
muted: "#8B8B9E",
|
|
1546
|
+
"muted-foreground": "#6E6E84",
|
|
1547
|
+
dim: "#3C3C50",
|
|
1548
|
+
accent: "#818CF8",
|
|
1549
|
+
"accent-dim": "#6366F1",
|
|
1550
|
+
success: "#34D399",
|
|
1551
|
+
warning: "#FBBF24",
|
|
1552
|
+
error: "#FB7185",
|
|
1553
|
+
info: "#38BDF8",
|
|
1554
|
+
text: "#E8E8F0",
|
|
1555
|
+
"text-muted": "#8B8B9E",
|
|
1556
|
+
panel: "#0A0A0F",
|
|
1557
|
+
divider: "#232330",
|
|
1558
|
+
highlight: "#22D3EE",
|
|
1559
|
+
backdrop: "#000000"
|
|
1560
|
+
};
|
|
1561
|
+
var CoolBracketPalette = [
|
|
1562
|
+
"#22D3EE",
|
|
1563
|
+
"#38BDF8",
|
|
1564
|
+
"#60A5FA",
|
|
1565
|
+
"#818CF8",
|
|
1566
|
+
"#A78BFA",
|
|
1567
|
+
"#C084FC",
|
|
1568
|
+
"#E879F9",
|
|
1569
|
+
"#A78BFA",
|
|
1570
|
+
"#818CF8",
|
|
1571
|
+
"#38BDF8"
|
|
1572
|
+
];
|
|
1573
|
+
var ConnectionStatusColor = {
|
|
1574
|
+
connected: Colors.success,
|
|
1575
|
+
polling: Colors.highlight,
|
|
1576
|
+
reconnecting: Colors.warning,
|
|
1577
|
+
offline: Colors.error
|
|
1578
|
+
};
|
|
1579
|
+
var WorkerStatusColor = {
|
|
1580
|
+
operational: Colors.success,
|
|
1581
|
+
degraded: Colors.warning,
|
|
1582
|
+
down: Colors.error
|
|
1583
|
+
};
|
|
1584
|
+
var LogLevelColor = {
|
|
1585
|
+
error: Colors.error,
|
|
1586
|
+
warn: Colors.warning,
|
|
1587
|
+
info: Colors.foreground,
|
|
1588
|
+
debug: Colors.muted
|
|
1589
|
+
};
|
|
1590
|
+
var AlertSeverityColor = {
|
|
1591
|
+
info: Colors.info,
|
|
1592
|
+
warning: Colors.warning,
|
|
1593
|
+
error: Colors.error,
|
|
1594
|
+
critical: Colors.error
|
|
1595
|
+
};
|
|
1617
1596
|
// src/formatters.ts
|
|
1618
1597
|
function formatNumber(n) {
|
|
1619
1598
|
if (!isFinite(n))
|
|
@@ -1648,7 +1627,7 @@ function formatCompactCurrency(n) {
|
|
|
1648
1627
|
return `${sign}$${(abs / 1000).toFixed(2)}K`;
|
|
1649
1628
|
return `${sign}$${abs.toFixed(2)}`;
|
|
1650
1629
|
}
|
|
1651
|
-
function
|
|
1630
|
+
function formatDuration(seconds) {
|
|
1652
1631
|
if (!isFinite(seconds) || seconds < 0)
|
|
1653
1632
|
return "\u2014";
|
|
1654
1633
|
const d = Math.floor(seconds / 86400);
|
|
@@ -1689,7 +1668,7 @@ function formatTimestamp(iso) {
|
|
|
1689
1668
|
return iso;
|
|
1690
1669
|
}
|
|
1691
1670
|
}
|
|
1692
|
-
function
|
|
1671
|
+
function formatRelativeTime(timestamp) {
|
|
1693
1672
|
const now = Date.now();
|
|
1694
1673
|
const diff = now - timestamp;
|
|
1695
1674
|
if (diff < 0)
|
|
@@ -1713,7 +1692,7 @@ function formatPercent(n, decimals = 2) {
|
|
|
1713
1692
|
return `${sign}${n.toFixed(decimals)}%`;
|
|
1714
1693
|
}
|
|
1715
1694
|
function formatUptime(seconds) {
|
|
1716
|
-
return
|
|
1695
|
+
return formatDuration(seconds);
|
|
1717
1696
|
}
|
|
1718
1697
|
function formatLatency(ms) {
|
|
1719
1698
|
if (!isFinite(ms))
|
|
@@ -1737,906 +1716,996 @@ function formatCpu(ms) {
|
|
|
1737
1716
|
return "\u2014";
|
|
1738
1717
|
return `${ms.toFixed(1)}ms`;
|
|
1739
1718
|
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1719
|
+
// src/config.ts
|
|
1720
|
+
import {
|
|
1721
|
+
readFileSync,
|
|
1722
|
+
writeFileSync,
|
|
1723
|
+
existsSync,
|
|
1724
|
+
mkdirSync,
|
|
1725
|
+
chmodSync,
|
|
1726
|
+
statSync
|
|
1727
|
+
} from "fs";
|
|
1728
|
+
import { join } from "path";
|
|
1729
|
+
import { homedir } from "os";
|
|
1730
|
+
var HOX_DIR = join(homedir(), ".hoox");
|
|
1731
|
+
var CONFIG_PATH = join(HOX_DIR, "config.json");
|
|
1732
|
+
var HOOX_DIR_MODE = 448;
|
|
1733
|
+
var HOOX_CONFIG_FILE_MODE = 384;
|
|
1734
|
+
var DEFAULT_CONFIG = {
|
|
1735
|
+
apiUrl: "http://localhost:8787",
|
|
1736
|
+
apiToken: "",
|
|
1737
|
+
transport: "public",
|
|
1738
|
+
refreshIntervalMs: 500,
|
|
1739
|
+
theme: "dark",
|
|
1740
|
+
activeExchanges: ["binance", "bybit", "mexc"],
|
|
1741
|
+
notifications: {
|
|
1742
|
+
alerts: true,
|
|
1743
|
+
trades: true,
|
|
1744
|
+
debug: false,
|
|
1745
|
+
system: true
|
|
1746
|
+
},
|
|
1747
|
+
soundEnabled: true,
|
|
1748
|
+
defaultView: "dashboard"
|
|
1749
|
+
};
|
|
1750
|
+
function readConfigSync() {
|
|
1751
|
+
const envConfig = {
|
|
1752
|
+
apiUrl: process.env.HOOX_API_URL,
|
|
1753
|
+
apiToken: process.env.HOOX_API_TOKEN
|
|
1754
|
+
};
|
|
1755
|
+
let fileConfig = {};
|
|
1756
|
+
try {
|
|
1757
|
+
if (existsSync(CONFIG_PATH)) {
|
|
1758
|
+
fileConfig = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
1759
|
+
}
|
|
1760
|
+
} catch {}
|
|
1761
|
+
return {
|
|
1762
|
+
...DEFAULT_CONFIG,
|
|
1763
|
+
...fileConfig,
|
|
1764
|
+
...Object.fromEntries(Object.entries(envConfig).filter(([_, v]) => v !== undefined))
|
|
1746
1765
|
};
|
|
1747
|
-
if (options?.worker !== undefined && options?.worker !== null)
|
|
1748
|
-
result.service = options.worker;
|
|
1749
|
-
if (options?.version !== undefined && options?.version !== null)
|
|
1750
|
-
result.version = options.version;
|
|
1751
|
-
if (options?.details)
|
|
1752
|
-
result.details = options.details;
|
|
1753
|
-
return createSuccessResponse(result);
|
|
1754
1766
|
}
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
var COPYRIGHT = `Copyright (C) ${new Date().getFullYear()} Hoox Team. All rights reserved.`;
|
|
1758
|
-
var TRADEMARKS = [
|
|
1759
|
-
"TradingView\xAE is a registered trademark of TradingView, Inc.",
|
|
1760
|
-
"Cloudflare\xAE is a registered trademark of Cloudflare, Inc."
|
|
1761
|
-
];
|
|
1762
|
-
var TRADEMARK_NOTICE = TRADEMARKS.join(`
|
|
1763
|
-
`);
|
|
1764
|
-
var DISCLAIMER = "DISCLAIMER: This software is provided for educational and informational purposes only. " + "Trading cryptocurrencies and other financial instruments involves substantial risk of loss. " + "No guarantee of profits or protection against losses is made or implied. " + "Past performance does not indicate future results. Use at your own risk.";
|
|
1765
|
-
var FULL_LEGAL_NOTICE = `${COPYRIGHT}
|
|
1766
|
-
|
|
1767
|
-
${TRADEMARK_NOTICE}
|
|
1768
|
-
|
|
1769
|
-
${DISCLAIMER}`;
|
|
1770
|
-
var DISCLAIMER_HEADER = "X-Disclaimer";
|
|
1771
|
-
|
|
1772
|
-
// src/types.ts
|
|
1773
|
-
import { z as z2 } from "zod";
|
|
1774
|
-
var TradeActionSchema = z2.enum([
|
|
1775
|
-
"LONG",
|
|
1776
|
-
"SHORT",
|
|
1777
|
-
"CLOSE_LONG",
|
|
1778
|
-
"CLOSE_SHORT"
|
|
1779
|
-
]);
|
|
1780
|
-
var MAX_WEBHOOK_LEVERAGE = 125;
|
|
1781
|
-
var MAX_WEBHOOK_QUANTITY = 1e6;
|
|
1782
|
-
var WebhookPayloadSchema = z2.object({
|
|
1783
|
-
exchange: z2.string().min(1),
|
|
1784
|
-
action: TradeActionSchema,
|
|
1785
|
-
symbol: z2.string().min(1).max(20),
|
|
1786
|
-
quantity: z2.number().positive().finite().max(MAX_WEBHOOK_QUANTITY),
|
|
1787
|
-
price: z2.number().positive().finite().optional(),
|
|
1788
|
-
orderType: z2.string().optional(),
|
|
1789
|
-
leverage: z2.number().int().positive().max(MAX_WEBHOOK_LEVERAGE).optional(),
|
|
1790
|
-
test: z2.boolean().optional()
|
|
1791
|
-
}).strict();
|
|
1792
|
-
var TradeQueueMessageSchema = z2.object({
|
|
1793
|
-
requestId: z2.string().min(1).max(128),
|
|
1794
|
-
exchange: z2.string().min(1).max(64),
|
|
1795
|
-
action: TradeActionSchema,
|
|
1796
|
-
symbol: z2.string().min(1).max(32),
|
|
1797
|
-
quantity: z2.number().positive().finite().max(MAX_WEBHOOK_QUANTITY),
|
|
1798
|
-
price: z2.number().positive().finite().optional(),
|
|
1799
|
-
leverage: z2.number().int().positive().max(MAX_WEBHOOK_LEVERAGE).optional(),
|
|
1800
|
-
test: z2.boolean().optional(),
|
|
1801
|
-
idempotencyKey: z2.string().min(1).max(512).optional(),
|
|
1802
|
-
queuedAt: z2.string().min(1).max(64)
|
|
1803
|
-
}).strict();
|
|
1804
|
-
var TradeSignalSchema = z2.object({
|
|
1805
|
-
id: z2.number().int().positive().optional(),
|
|
1806
|
-
source: z2.string().min(1),
|
|
1807
|
-
symbol: z2.string().min(1),
|
|
1808
|
-
action: TradeActionSchema,
|
|
1809
|
-
price: z2.number().positive().finite().optional(),
|
|
1810
|
-
quantity: z2.number().positive().finite(),
|
|
1811
|
-
leverage: z2.number().int().positive().optional(),
|
|
1812
|
-
status: z2.enum(["pending", "executed", "failed", "skipped"]).optional(),
|
|
1813
|
-
createdAt: z2.string().optional(),
|
|
1814
|
-
executedAt: z2.string().optional(),
|
|
1815
|
-
error: z2.string().optional()
|
|
1816
|
-
}).strict();
|
|
1817
|
-
var PositionSchema = z2.object({
|
|
1818
|
-
id: z2.string(),
|
|
1819
|
-
symbol: z2.string().min(1),
|
|
1820
|
-
side: TradeActionSchema,
|
|
1821
|
-
quantity: z2.number().positive(),
|
|
1822
|
-
entry_price: z2.number().positive(),
|
|
1823
|
-
current_price: z2.number().positive(),
|
|
1824
|
-
unrealized_pnl: z2.number(),
|
|
1825
|
-
timestamp: z2.number().int().positive()
|
|
1826
|
-
}).strict();
|
|
1827
|
-
var BalanceSchema = z2.object({
|
|
1828
|
-
asset: z2.string().min(1),
|
|
1829
|
-
free: z2.number().min(0),
|
|
1830
|
-
locked: z2.number().min(0),
|
|
1831
|
-
timestamp: z2.number().int().positive()
|
|
1832
|
-
}).strict();
|
|
1833
|
-
|
|
1834
|
-
// src/kill-switch.ts
|
|
1835
|
-
var KILL_SWITCH_ACTIVE_PREFIX = "KILL_SWITCH_ACTIVE";
|
|
1836
|
-
var KILL_SWITCH_KEYS = [
|
|
1837
|
-
KV_TRADE_KILL_SWITCH,
|
|
1838
|
-
KV_GLOBAL_KILL_SWITCH
|
|
1839
|
-
];
|
|
1840
|
-
function isTruthyKillSwitchFlag(value) {
|
|
1841
|
-
if (value == null)
|
|
1842
|
-
return false;
|
|
1843
|
-
const v = value.trim().toLowerCase();
|
|
1844
|
-
return v === "true" || v === "1" || v === "yes" || v === "on";
|
|
1767
|
+
async function readConfig() {
|
|
1768
|
+
return readConfigSync();
|
|
1845
1769
|
}
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
if (!kv) {
|
|
1850
|
-
if (onMissingKv === "closed") {
|
|
1851
|
-
return { enabled: true, source: "missing_kv" };
|
|
1852
|
-
}
|
|
1853
|
-
return { enabled: false };
|
|
1770
|
+
function ensureHooxDirSecure(dir = HOX_DIR) {
|
|
1771
|
+
if (!existsSync(dir)) {
|
|
1772
|
+
mkdirSync(dir, { recursive: true, mode: HOOX_DIR_MODE });
|
|
1854
1773
|
}
|
|
1855
1774
|
try {
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
value: await kv.get(key)
|
|
1859
|
-
})));
|
|
1860
|
-
for (const { key, value } of values) {
|
|
1861
|
-
if (isTruthyKillSwitchFlag(value)) {
|
|
1862
|
-
return { enabled: true, source: key };
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
return { enabled: false };
|
|
1866
|
-
} catch (error) {
|
|
1867
|
-
const errorStr = String(error);
|
|
1868
|
-
if (onReadError === "closed") {
|
|
1869
|
-
return { enabled: true, source: "read_error", error: errorStr };
|
|
1870
|
-
}
|
|
1871
|
-
return { enabled: false, error: errorStr };
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
async function isTradingPaused(kv, options) {
|
|
1875
|
-
const result = await checkKillSwitch(kv, options);
|
|
1876
|
-
return result.enabled;
|
|
1775
|
+
chmodSync(dir, HOOX_DIR_MODE);
|
|
1776
|
+
} catch {}
|
|
1877
1777
|
}
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
async function logKvTimestamp(env, prefix = "timestamp") {
|
|
1881
|
-
const timestamp = new Date().toISOString();
|
|
1882
|
-
const key = `${prefix}_${timestamp}`;
|
|
1778
|
+
function writeSecureFileSync(filePath, contents, mode = HOOX_CONFIG_FILE_MODE) {
|
|
1779
|
+
writeFileSync(filePath, contents, { encoding: "utf-8", mode });
|
|
1883
1780
|
try {
|
|
1884
|
-
|
|
1885
|
-
} catch
|
|
1886
|
-
console.error(`Failed to log timestamp to KV: ${toError(error, "Unknown error")}`);
|
|
1887
|
-
}
|
|
1781
|
+
chmodSync(filePath, mode);
|
|
1782
|
+
} catch {}
|
|
1888
1783
|
}
|
|
1889
|
-
|
|
1890
|
-
|
|
1784
|
+
function isUnixModeGroupOrWorldAccessible(mode) {
|
|
1785
|
+
return (mode & 63) !== 0;
|
|
1786
|
+
}
|
|
1787
|
+
function isConfigWorldOrGroupReadable(filePath = CONFIG_PATH) {
|
|
1891
1788
|
try {
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
}
|
|
1896
|
-
return chunk.map((key) => result.get(key) ?? null);
|
|
1789
|
+
if (!existsSync(filePath))
|
|
1790
|
+
return false;
|
|
1791
|
+
return isUnixModeGroupOrWorldAccessible(statSync(filePath).mode & 511);
|
|
1897
1792
|
} catch {
|
|
1898
|
-
return
|
|
1793
|
+
return false;
|
|
1899
1794
|
}
|
|
1900
1795
|
}
|
|
1901
|
-
|
|
1902
|
-
if (
|
|
1903
|
-
return
|
|
1904
|
-
|
|
1905
|
-
let offset = 0;
|
|
1906
|
-
while (offset < keys.length) {
|
|
1907
|
-
const chunk = keys.slice(offset, offset + KV_BULK_GET_MAX_KEYS);
|
|
1908
|
-
const bulk = await tryBulkGetChunk(kv, chunk);
|
|
1909
|
-
if (bulk === null) {
|
|
1910
|
-
const rest = await Promise.all(keys.slice(offset).map((key) => kv.get(key)));
|
|
1911
|
-
for (let i = 0;i < rest.length; i++) {
|
|
1912
|
-
out[offset + i] = rest[i] ?? null;
|
|
1913
|
-
}
|
|
1914
|
-
return out;
|
|
1915
|
-
}
|
|
1916
|
-
for (let i = 0;i < bulk.length; i++) {
|
|
1917
|
-
out[offset + i] = bulk[i] ?? null;
|
|
1918
|
-
}
|
|
1919
|
-
offset += chunk.length;
|
|
1920
|
-
}
|
|
1921
|
-
return out;
|
|
1796
|
+
function formatConfigPermissionWarning(filePath = CONFIG_PATH) {
|
|
1797
|
+
if (!isConfigWorldOrGroupReadable(filePath))
|
|
1798
|
+
return null;
|
|
1799
|
+
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.`;
|
|
1922
1800
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1801
|
+
function writeConfigSync(config) {
|
|
1802
|
+
try {
|
|
1803
|
+
ensureHooxDirSecure(HOX_DIR);
|
|
1804
|
+
writeSecureFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), HOOX_CONFIG_FILE_MODE);
|
|
1805
|
+
} catch (err) {
|
|
1806
|
+
console.error("Failed to write config:", err);
|
|
1928
1807
|
}
|
|
1929
|
-
return out;
|
|
1930
1808
|
}
|
|
1931
|
-
async function
|
|
1932
|
-
|
|
1933
|
-
return;
|
|
1934
|
-
await Promise.all(entries.map((entry) => kv.put(entry.key, entry.value, entry.options)));
|
|
1809
|
+
async function writeConfig(config) {
|
|
1810
|
+
writeConfigSync(config);
|
|
1935
1811
|
}
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1812
|
+
var VALID_TRANSPORTS = new Set([
|
|
1813
|
+
"public",
|
|
1814
|
+
"access",
|
|
1815
|
+
"mtls",
|
|
1816
|
+
"tunnel"
|
|
1817
|
+
]);
|
|
1818
|
+
function validateConfig(config) {
|
|
1819
|
+
const errors = [];
|
|
1820
|
+
if (config.apiUrl && !config.apiUrl.startsWith("http")) {
|
|
1821
|
+
errors.push("apiUrl must start with http:// or https://");
|
|
1946
1822
|
}
|
|
1947
|
-
|
|
1823
|
+
if (config.refreshIntervalMs !== undefined && config.refreshIntervalMs < 100) {
|
|
1824
|
+
errors.push("refreshIntervalMs must be >= 100ms");
|
|
1825
|
+
}
|
|
1826
|
+
if (config.theme && !["dark", "light"].includes(config.theme)) {
|
|
1827
|
+
errors.push('theme must be "dark" or "light"');
|
|
1828
|
+
}
|
|
1829
|
+
if (config.transport !== undefined && !VALID_TRANSPORTS.has(config.transport)) {
|
|
1830
|
+
errors.push('transport must be "public" | "access" | "mtls" | "tunnel"');
|
|
1831
|
+
}
|
|
1832
|
+
return errors;
|
|
1948
1833
|
}
|
|
1949
1834
|
|
|
1950
|
-
// src/
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1835
|
+
// src/index.ts
|
|
1836
|
+
init_api_client();
|
|
1837
|
+
init_sse();
|
|
1838
|
+
init_operator_transport();
|
|
1839
|
+
|
|
1840
|
+
// src/path-utils.ts
|
|
1841
|
+
import {
|
|
1842
|
+
existsSync as existsSync2,
|
|
1843
|
+
mkdirSync as mkdirSync2,
|
|
1844
|
+
readFileSync as readFileSync2,
|
|
1845
|
+
writeFileSync as writeFileSync2,
|
|
1846
|
+
chmodSync as chmodSync2
|
|
1847
|
+
} from "fs";
|
|
1848
|
+
import { homedir as homedir2 } from "os";
|
|
1849
|
+
import { dirname, join as join2, resolve } from "path";
|
|
1850
|
+
function createHooxPath(path) {
|
|
1851
|
+
return path;
|
|
1852
|
+
}
|
|
1853
|
+
function getHooxHome() {
|
|
1854
|
+
try {
|
|
1855
|
+
const override = process.env.HOOX_HOME?.trim();
|
|
1856
|
+
if (override) {
|
|
1857
|
+
return createHooxPath(resolve(override));
|
|
1958
1858
|
}
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
async function worker() {
|
|
1963
|
-
while (true) {
|
|
1964
|
-
const i = nextIndex++;
|
|
1965
|
-
if (i >= items.length)
|
|
1966
|
-
return;
|
|
1967
|
-
await fn(items[i]);
|
|
1859
|
+
const home = homedir2();
|
|
1860
|
+
if (!home || home.length === 0) {
|
|
1861
|
+
return createHooxPath(resolve(process.cwd(), ".hoox"));
|
|
1968
1862
|
}
|
|
1863
|
+
return createHooxPath(join2(home, ".hoox"));
|
|
1864
|
+
} catch {
|
|
1865
|
+
return createHooxPath(resolve(process.cwd(), ".hoox"));
|
|
1969
1866
|
}
|
|
1970
|
-
await Promise.all(Array.from({ length: limit }, () => worker()));
|
|
1971
1867
|
}
|
|
1972
|
-
function
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
logger,
|
|
1980
|
-
concurrency: concurrencyOpt
|
|
1981
|
-
} = options;
|
|
1982
|
-
const concurrency = typeof concurrencyOpt === "number" && Number.isFinite(concurrencyOpt) && concurrencyOpt >= 1 ? Math.floor(concurrencyOpt) : 1;
|
|
1983
|
-
async function processMessage(msg) {
|
|
1984
|
-
const attemptNumber = msg.attempts || 0;
|
|
1985
|
-
const logId = `[${msg.id}]`;
|
|
1986
|
-
try {
|
|
1987
|
-
logger?.info(`${logId} Processing message (attempt ${attemptNumber + 1})`);
|
|
1988
|
-
await onMessage(msg.body, attemptNumber);
|
|
1989
|
-
logger?.info(`${logId} Message processed successfully`);
|
|
1990
|
-
} catch (error) {
|
|
1991
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1992
|
-
if (attemptNumber < maxRetries) {
|
|
1993
|
-
const delaySeconds = backoffDelays[attemptNumber] ?? backoffDelays[backoffDelays.length - 1] ?? 0;
|
|
1994
|
-
logger?.info(`${logId} Retrying in ${delaySeconds}s (attempt ${attemptNumber + 2}/${maxRetries + 1})`);
|
|
1995
|
-
await onRetry?.(msg.body, attemptNumber, errorMsg, delaySeconds);
|
|
1996
|
-
msg.retry({ delaySeconds });
|
|
1997
|
-
} else {
|
|
1998
|
-
logger?.error(`${logId} Max retries exceeded (${maxRetries + 1} attempts), moving to DLQ`);
|
|
1999
|
-
await onDLQ?.(msg.body, attemptNumber, errorMsg);
|
|
2000
|
-
}
|
|
1868
|
+
function isHooxSetupRoot(dir) {
|
|
1869
|
+
if (!dir)
|
|
1870
|
+
return false;
|
|
1871
|
+
try {
|
|
1872
|
+
const root = resolve(dir);
|
|
1873
|
+
if (!existsSync2(join2(root, "packages", "cli", "package.json"))) {
|
|
1874
|
+
return false;
|
|
2001
1875
|
}
|
|
1876
|
+
return existsSync2(join2(root, "wrangler.jsonc")) || existsSync2(join2(root, "wrangler.jsonc.example")) || existsSync2(join2(root, "workers")) || existsSync2(join2(root, ".gitmodules"));
|
|
1877
|
+
} catch {
|
|
1878
|
+
return false;
|
|
2002
1879
|
}
|
|
2003
|
-
return async (batch) => {
|
|
2004
|
-
await mapPool(batch.messages, concurrency, processMessage);
|
|
2005
|
-
};
|
|
2006
1880
|
}
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
function timingSafeEqual(a, b) {
|
|
2010
|
-
const encoder = new TextEncoder;
|
|
2011
|
-
const aBuf = encoder.encode(a);
|
|
2012
|
-
const bBuf = encoder.encode(b);
|
|
2013
|
-
const len = Math.max(aBuf.length, bBuf.length);
|
|
2014
|
-
let result = aBuf.length === bBuf.length ? 0 : 1;
|
|
2015
|
-
for (let i = 0;i < len; i++) {
|
|
2016
|
-
const av = aBuf[i] ?? 0;
|
|
2017
|
-
const bv = bBuf[i] ?? 0;
|
|
2018
|
-
result |= av ^ bv;
|
|
2019
|
-
}
|
|
2020
|
-
return result === 0;
|
|
1881
|
+
function getRememberedMonorepoPath() {
|
|
1882
|
+
return resolveHooxPath("config/monorepo.json");
|
|
2021
1883
|
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
1884
|
+
function getRememberedMonorepoRoot() {
|
|
1885
|
+
try {
|
|
1886
|
+
const path = getRememberedMonorepoPath();
|
|
1887
|
+
if (!existsSync2(path))
|
|
1888
|
+
return null;
|
|
1889
|
+
const raw = readFileSync2(path, "utf-8");
|
|
1890
|
+
const data = JSON.parse(raw);
|
|
1891
|
+
if (!data.root || typeof data.root !== "string")
|
|
1892
|
+
return null;
|
|
1893
|
+
const resolved = resolve(data.root);
|
|
1894
|
+
return isHooxSetupRoot(resolved) ? resolved : null;
|
|
1895
|
+
} catch {
|
|
1896
|
+
return null;
|
|
2026
1897
|
}
|
|
2027
|
-
const encoder = new TextEncoder;
|
|
2028
|
-
const [providedHash, expectedHash] = await Promise.all([
|
|
2029
|
-
subtle.digest("SHA-256", encoder.encode(a)),
|
|
2030
|
-
subtle.digest("SHA-256", encoder.encode(b))
|
|
2031
|
-
]);
|
|
2032
|
-
return subtle.timingSafeEqual(providedHash, expectedHash);
|
|
2033
1898
|
}
|
|
2034
|
-
function
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
1899
|
+
function rememberMonorepoRoot(root) {
|
|
1900
|
+
try {
|
|
1901
|
+
const resolved = resolve(root);
|
|
1902
|
+
if (!isHooxSetupRoot(resolved))
|
|
1903
|
+
return false;
|
|
1904
|
+
const configDir = getHooxConfigDir();
|
|
1905
|
+
mkdirSync2(configDir, { recursive: true, mode: 448 });
|
|
1906
|
+
try {
|
|
1907
|
+
chmodSync2(configDir, 448);
|
|
1908
|
+
} catch {}
|
|
1909
|
+
const payload = {
|
|
1910
|
+
root: resolved,
|
|
1911
|
+
updatedAt: new Date().toISOString()
|
|
1912
|
+
};
|
|
1913
|
+
const path = getRememberedMonorepoPath();
|
|
1914
|
+
writeFileSync2(path, JSON.stringify(payload, null, 2) + `
|
|
1915
|
+
`, {
|
|
1916
|
+
mode: 384
|
|
1917
|
+
});
|
|
1918
|
+
try {
|
|
1919
|
+
chmodSync2(path, 384);
|
|
1920
|
+
} catch {}
|
|
1921
|
+
return true;
|
|
1922
|
+
} catch {
|
|
1923
|
+
return false;
|
|
2042
1924
|
}
|
|
2043
|
-
return;
|
|
2044
1925
|
}
|
|
2045
|
-
|
|
2046
|
-
|
|
1926
|
+
function findHooxSetupRoot(startDir = process.cwd()) {
|
|
1927
|
+
let dir = resolve(startDir);
|
|
1928
|
+
for (;; ) {
|
|
1929
|
+
if (isHooxSetupRoot(dir))
|
|
1930
|
+
return dir;
|
|
1931
|
+
const parent = dirname(dir);
|
|
1932
|
+
if (parent === dir)
|
|
1933
|
+
return null;
|
|
1934
|
+
dir = parent;
|
|
1935
|
+
}
|
|
2047
1936
|
}
|
|
2048
|
-
|
|
2049
|
-
const
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
1937
|
+
function resolveHooxRuntimeRoot(options) {
|
|
1938
|
+
const env = options?.env ?? process.env;
|
|
1939
|
+
const cwd = resolve(options?.cwd ?? process.cwd());
|
|
1940
|
+
const globalRepo = getHooxRepoPath();
|
|
1941
|
+
const remembered = options?.skipRemembered ? null : getRememberedMonorepoRoot();
|
|
1942
|
+
const envRepo = env.HOOX_REPO?.trim();
|
|
1943
|
+
if (envRepo) {
|
|
1944
|
+
const resolved = resolve(envRepo);
|
|
1945
|
+
if (isHooxSetupRoot(resolved)) {
|
|
1946
|
+
return {
|
|
1947
|
+
root: resolved,
|
|
1948
|
+
source: "env",
|
|
1949
|
+
checked: {
|
|
1950
|
+
env: resolved,
|
|
1951
|
+
cwd: findHooxSetupRoot(cwd),
|
|
1952
|
+
remembered,
|
|
1953
|
+
global: globalRepo
|
|
1954
|
+
}
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
return {
|
|
1958
|
+
root: null,
|
|
1959
|
+
source: "env",
|
|
1960
|
+
checked: {
|
|
1961
|
+
env: resolved,
|
|
1962
|
+
cwd: findHooxSetupRoot(cwd),
|
|
1963
|
+
remembered,
|
|
1964
|
+
global: globalRepo
|
|
1965
|
+
}
|
|
1966
|
+
};
|
|
2055
1967
|
}
|
|
2056
|
-
const
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
}
|
|
1968
|
+
const local = findHooxSetupRoot(cwd);
|
|
1969
|
+
if (local) {
|
|
1970
|
+
return {
|
|
1971
|
+
root: local,
|
|
1972
|
+
source: "cwd",
|
|
1973
|
+
checked: { cwd: local, remembered, global: globalRepo }
|
|
1974
|
+
};
|
|
2063
1975
|
}
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
1976
|
+
if (remembered) {
|
|
1977
|
+
return {
|
|
1978
|
+
root: remembered,
|
|
1979
|
+
source: "remembered",
|
|
1980
|
+
checked: { cwd: null, remembered, global: globalRepo }
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
if (isHooxSetupRoot(globalRepo)) {
|
|
1984
|
+
return {
|
|
1985
|
+
root: globalRepo,
|
|
1986
|
+
source: "global",
|
|
1987
|
+
checked: { cwd: null, remembered: null, global: globalRepo }
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
return {
|
|
1991
|
+
root: null,
|
|
1992
|
+
source: "none",
|
|
1993
|
+
checked: { cwd: null, remembered: null, global: globalRepo }
|
|
2072
1994
|
};
|
|
2073
1995
|
}
|
|
2074
|
-
function
|
|
2075
|
-
|
|
2076
|
-
|
|
1996
|
+
function getTuiEntryCandidates(runtimeRoot) {
|
|
1997
|
+
const root = resolve(runtimeRoot);
|
|
1998
|
+
return [
|
|
1999
|
+
join2(root, "packages", "tui", "src", "main.tsx"),
|
|
2000
|
+
join2(root, "packages", "tui", "dist", "main.js"),
|
|
2001
|
+
join2(root, "packages", "tui", "src", "main.ts")
|
|
2002
|
+
];
|
|
2003
|
+
}
|
|
2004
|
+
function resolveHooxPath(relativePath) {
|
|
2005
|
+
if (!relativePath || typeof relativePath !== "string") {
|
|
2006
|
+
throw new Error("relativePath must be a non-empty string");
|
|
2077
2007
|
}
|
|
2078
|
-
|
|
2008
|
+
if (relativePath.includes("..")) {
|
|
2009
|
+
throw new Error(`Path traversal detected in relativePath: "${relativePath}"`);
|
|
2010
|
+
}
|
|
2011
|
+
const normalizedPath = relativePath.replace(/\\/g, "/");
|
|
2012
|
+
const hooxHome = getHooxHome();
|
|
2013
|
+
const fullPath = join2(hooxHome, normalizedPath);
|
|
2014
|
+
const resolvedPath = resolve(fullPath);
|
|
2015
|
+
const resolvedHome = resolve(hooxHome);
|
|
2016
|
+
if (!resolvedPath.startsWith(resolvedHome)) {
|
|
2017
|
+
throw new Error(`Resolved path "${resolvedPath}" is outside Hoox home directory`);
|
|
2018
|
+
}
|
|
2019
|
+
return createHooxPath(resolvedPath);
|
|
2079
2020
|
}
|
|
2080
|
-
function
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
const
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2021
|
+
function isWithinHooxHome(path) {
|
|
2022
|
+
try {
|
|
2023
|
+
const hooxHome = getHooxHome();
|
|
2024
|
+
const resolvedPath = resolve(path);
|
|
2025
|
+
const resolvedHome = resolve(hooxHome);
|
|
2026
|
+
return resolvedPath.startsWith(resolvedHome);
|
|
2027
|
+
} catch {
|
|
2028
|
+
return false;
|
|
2087
2029
|
}
|
|
2088
|
-
return keys;
|
|
2089
2030
|
}
|
|
2090
|
-
function
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
matched = true;
|
|
2031
|
+
function getRelativeHooxPath(absolutePath) {
|
|
2032
|
+
try {
|
|
2033
|
+
if (!isWithinHooxHome(absolutePath)) {
|
|
2034
|
+
return null;
|
|
2095
2035
|
}
|
|
2036
|
+
const hooxHome = getHooxHome();
|
|
2037
|
+
const resolvedPath = resolve(absolutePath);
|
|
2038
|
+
const resolvedHome = resolve(hooxHome);
|
|
2039
|
+
const relativePath = resolvedPath.slice(resolvedHome.length);
|
|
2040
|
+
return relativePath.startsWith("/") || relativePath.startsWith("\\") ? relativePath.slice(1) : relativePath;
|
|
2041
|
+
} catch {
|
|
2042
|
+
return null;
|
|
2096
2043
|
}
|
|
2097
|
-
return matched;
|
|
2098
2044
|
}
|
|
2099
|
-
function
|
|
2100
|
-
|
|
2101
|
-
return new Response(JSON.stringify({
|
|
2102
|
-
success: false,
|
|
2103
|
-
error: `Internal auth key(s) not configured: ${label}`
|
|
2104
|
-
}), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
2045
|
+
function getHooxRepoPath() {
|
|
2046
|
+
return resolveHooxPath("repo");
|
|
2105
2047
|
}
|
|
2106
|
-
function
|
|
2107
|
-
|
|
2108
|
-
if (expectedKeys.length === 0) {
|
|
2109
|
-
return internalAuthNotConfiguredResponse(keyName);
|
|
2110
|
-
}
|
|
2111
|
-
const providedKey = request.headers.get("X-Internal-Auth-Key");
|
|
2112
|
-
if (!providedKey || !matchesAnyInternalAuthKey(providedKey, expectedKeys)) {
|
|
2113
|
-
return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
2114
|
-
}
|
|
2115
|
-
return null;
|
|
2048
|
+
function getHooxConfigDir() {
|
|
2049
|
+
return resolveHooxPath("config");
|
|
2116
2050
|
}
|
|
2117
|
-
function
|
|
2118
|
-
return
|
|
2119
|
-
const expectedKeys = collectInternalAuthKeys(env, keyName);
|
|
2120
|
-
if (expectedKeys.length === 0) {
|
|
2121
|
-
return internalAuthNotConfiguredResponse(keyName);
|
|
2122
|
-
}
|
|
2123
|
-
const providedKey = request.headers.get("X-Internal-Auth-Key");
|
|
2124
|
-
if (!providedKey || !matchesAnyInternalAuthKey(providedKey, expectedKeys)) {
|
|
2125
|
-
return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
2126
|
-
}
|
|
2127
|
-
return;
|
|
2128
|
-
};
|
|
2051
|
+
function getHooxDataDir() {
|
|
2052
|
+
return resolveHooxPath("data");
|
|
2129
2053
|
}
|
|
2130
|
-
function
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
error: `${normalizeKeyNames(keyName).join(" | ")} not configured`
|
|
2136
|
-
};
|
|
2137
|
-
}
|
|
2138
|
-
const providedKey = request.headers.get("X-Internal-Auth-Key");
|
|
2139
|
-
if (!providedKey || !matchesAnyInternalAuthKey(providedKey, expectedKeys)) {
|
|
2140
|
-
return { authorized: false, error: "Unauthorized" };
|
|
2141
|
-
}
|
|
2142
|
-
return { authorized: true };
|
|
2054
|
+
function getHooxWranglerPath() {
|
|
2055
|
+
return resolveHooxPath("config/wrangler.jsonc");
|
|
2056
|
+
}
|
|
2057
|
+
function getHooxStatePath() {
|
|
2058
|
+
return resolveHooxPath("data/state.json");
|
|
2143
2059
|
}
|
|
2144
2060
|
|
|
2145
|
-
// src/
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
InternalAuthKeyFields.D1_WRITE,
|
|
2165
|
-
InternalAuthKeyFields.DEFAULT
|
|
2166
|
-
];
|
|
2167
|
-
var TRADE_EXECUTE_AUTH_KEY_FIELDS = [
|
|
2168
|
-
InternalAuthKeyFields.TRADE_EXECUTE,
|
|
2169
|
-
InternalAuthKeyFields.DEFAULT,
|
|
2170
|
-
InternalAuthKeyFields.AGENT
|
|
2171
|
-
];
|
|
2172
|
-
var TRADE_READ_AUTH_KEY_FIELDS = [
|
|
2173
|
-
InternalAuthKeyFields.TRADE_READ,
|
|
2174
|
-
InternalAuthKeyFields.DEFAULT
|
|
2175
|
-
];
|
|
2176
|
-
var TELEGRAM_ALERT_AUTH_KEY_FIELDS = [
|
|
2177
|
-
InternalAuthKeyFields.TELEGRAM_ALERT,
|
|
2178
|
-
InternalAuthKeyFields.DEFAULT
|
|
2179
|
-
];
|
|
2180
|
-
var WALLET_EXECUTE_AUTH_KEY_FIELDS = [
|
|
2181
|
-
InternalAuthKeyFields.WALLET_EXECUTE,
|
|
2182
|
-
InternalAuthKeyFields.DEFAULT
|
|
2183
|
-
];
|
|
2184
|
-
var DASHBOARD_D1_READ_AUTH_KEY_FIELDS = [
|
|
2185
|
-
InternalAuthKeyFields.D1_READ,
|
|
2186
|
-
InternalAuthKeyFields.D1_INTERNAL_ALIAS,
|
|
2187
|
-
InternalAuthKeyFields.DEFAULT
|
|
2188
|
-
];
|
|
2189
|
-
var DASHBOARD_TRADE_EXECUTE_AUTH_KEY_FIELDS = [
|
|
2190
|
-
InternalAuthKeyFields.TRADE_EXECUTE,
|
|
2191
|
-
InternalAuthKeyFields.TRADE_INTERNAL_ALIAS,
|
|
2192
|
-
InternalAuthKeyFields.DEFAULT
|
|
2193
|
-
];
|
|
2194
|
-
var DASHBOARD_TELEGRAM_ALERT_AUTH_KEY_FIELDS = [
|
|
2195
|
-
InternalAuthKeyFields.TELEGRAM_ALERT,
|
|
2196
|
-
InternalAuthKeyFields.TELEGRAM_INTERNAL_ALIAS,
|
|
2197
|
-
InternalAuthKeyFields.DEFAULT
|
|
2198
|
-
];
|
|
2199
|
-
function resolveInternalAuthKey(env, keyFields) {
|
|
2200
|
-
return collectInternalAuthKeys(env, keyFields)[0];
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
class ServiceAuthError extends Error {
|
|
2204
|
-
constructor(message) {
|
|
2205
|
-
super(message);
|
|
2206
|
-
this.name = "ServiceAuthError";
|
|
2061
|
+
// src/session.ts
|
|
2062
|
+
import { z as z3 } from "zod";
|
|
2063
|
+
var SessionStateFileSchema = z3.object({
|
|
2064
|
+
activeView: z3.string().optional(),
|
|
2065
|
+
sidebarExpanded: z3.boolean().optional(),
|
|
2066
|
+
windowSize: z3.object({
|
|
2067
|
+
cols: z3.number().int().positive(),
|
|
2068
|
+
rows: z3.number().int().positive()
|
|
2069
|
+
}).optional(),
|
|
2070
|
+
lastData: z3.number().nonnegative().optional(),
|
|
2071
|
+
savedAt: z3.string().optional()
|
|
2072
|
+
}).strict();
|
|
2073
|
+
var TUI_STATE_DIR = ".tui-state";
|
|
2074
|
+
function getSessionFilePath() {
|
|
2075
|
+
try {
|
|
2076
|
+
const hooxHome = getHooxHome();
|
|
2077
|
+
return `${hooxHome}/${TUI_STATE_DIR}/session.json`;
|
|
2078
|
+
} catch {
|
|
2079
|
+
return `${process.cwd()}/${TUI_STATE_DIR}/session.json`;
|
|
2207
2080
|
}
|
|
2208
2081
|
}
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2082
|
+
var DEFAULT_SESSION = {
|
|
2083
|
+
activeView: "dashboard",
|
|
2084
|
+
sidebarExpanded: true,
|
|
2085
|
+
windowSize: { cols: 80, rows: 24 },
|
|
2086
|
+
lastData: 0,
|
|
2087
|
+
savedAt: new Date().toISOString()
|
|
2088
|
+
};
|
|
2089
|
+
async function saveSession(activeView, sidebarExpanded, windowSize, lastData) {
|
|
2090
|
+
try {
|
|
2091
|
+
const session = {
|
|
2092
|
+
activeView,
|
|
2093
|
+
sidebarExpanded,
|
|
2094
|
+
windowSize,
|
|
2095
|
+
lastData,
|
|
2096
|
+
savedAt: new Date().toISOString()
|
|
2097
|
+
};
|
|
2098
|
+
await Bun.write(getSessionFilePath(), JSON.stringify(session, null, 2));
|
|
2099
|
+
} catch (error) {
|
|
2100
|
+
console.error("[session] Failed to save session:", error.message);
|
|
2214
2101
|
}
|
|
2215
|
-
return serviceFetch(binding, path, body, {
|
|
2216
|
-
method: options?.method,
|
|
2217
|
-
timeout: options?.timeout,
|
|
2218
|
-
headers: {
|
|
2219
|
-
...options?.headers,
|
|
2220
|
-
"X-Internal-Auth-Key": key
|
|
2221
|
-
}
|
|
2222
|
-
});
|
|
2223
|
-
}
|
|
2224
|
-
function serviceFetch(binding, path, body, options) {
|
|
2225
|
-
const timeout = options?.timeout ?? 30000;
|
|
2226
|
-
return binding.fetch(`http://internal${path}`, {
|
|
2227
|
-
method: options?.method ?? "POST",
|
|
2228
|
-
headers: {
|
|
2229
|
-
"Content-Type": "application/json",
|
|
2230
|
-
...options?.headers
|
|
2231
|
-
},
|
|
2232
|
-
body: body != null ? JSON.stringify(body) : undefined,
|
|
2233
|
-
signal: AbortSignal.timeout(timeout)
|
|
2234
|
-
});
|
|
2235
|
-
}
|
|
2236
|
-
|
|
2237
|
-
// src/test-utils.ts
|
|
2238
|
-
function createMockKV(overrides) {
|
|
2239
|
-
return {
|
|
2240
|
-
get: async (_key) => null,
|
|
2241
|
-
put: async (_key, _value, _options) => {},
|
|
2242
|
-
delete: async (_key) => {},
|
|
2243
|
-
list: async (_options) => ({
|
|
2244
|
-
keys: []
|
|
2245
|
-
}),
|
|
2246
|
-
...overrides
|
|
2247
|
-
};
|
|
2248
|
-
}
|
|
2249
|
-
function createMockR2(overrides) {
|
|
2250
|
-
return {
|
|
2251
|
-
put: async (key, _value, _options) => ({
|
|
2252
|
-
key
|
|
2253
|
-
}),
|
|
2254
|
-
get: async (_key) => null,
|
|
2255
|
-
delete: async (_key) => {},
|
|
2256
|
-
list: async (_options) => ({
|
|
2257
|
-
objects: []
|
|
2258
|
-
}),
|
|
2259
|
-
...overrides
|
|
2260
|
-
};
|
|
2261
2102
|
}
|
|
2262
|
-
function
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2103
|
+
async function restoreSession() {
|
|
2104
|
+
try {
|
|
2105
|
+
const file = Bun.file(getSessionFilePath());
|
|
2106
|
+
const exists = await file.exists();
|
|
2107
|
+
if (!exists)
|
|
2108
|
+
return { ...DEFAULT_SESSION };
|
|
2109
|
+
const raw = await file.text();
|
|
2110
|
+
const result = SessionStateFileSchema.safeParse(JSON.parse(raw));
|
|
2111
|
+
const parsed = result.success ? result.data : {};
|
|
2112
|
+
return {
|
|
2113
|
+
activeView: validateViewId(parsed.activeView) ? parsed.activeView : DEFAULT_SESSION.activeView,
|
|
2114
|
+
sidebarExpanded: typeof parsed.sidebarExpanded === "boolean" ? parsed.sidebarExpanded : DEFAULT_SESSION.sidebarExpanded,
|
|
2115
|
+
windowSize: parsed.windowSize ?? DEFAULT_SESSION.windowSize,
|
|
2116
|
+
lastData: typeof parsed.lastData === "number" ? parsed.lastData : DEFAULT_SESSION.lastData,
|
|
2117
|
+
savedAt: parsed.savedAt ?? DEFAULT_SESSION.savedAt
|
|
2118
|
+
};
|
|
2119
|
+
} catch {
|
|
2120
|
+
return { ...DEFAULT_SESSION };
|
|
2121
|
+
}
|
|
2269
2122
|
}
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2123
|
+
var VALID_VIEW_IDS = new Set([
|
|
2124
|
+
"dashboard",
|
|
2125
|
+
"workers",
|
|
2126
|
+
"worker-detail",
|
|
2127
|
+
"trade-monitor",
|
|
2128
|
+
"logs-viewer",
|
|
2129
|
+
"service-manager",
|
|
2130
|
+
"config-editor",
|
|
2131
|
+
"setup-wizard",
|
|
2132
|
+
"settings",
|
|
2133
|
+
"queue-depth",
|
|
2134
|
+
"kv-viewer",
|
|
2135
|
+
"secrets-viewer",
|
|
2136
|
+
"db-query",
|
|
2137
|
+
"ai-chat",
|
|
2138
|
+
"edge-topology",
|
|
2139
|
+
"worker-settings"
|
|
2140
|
+
]);
|
|
2141
|
+
function validateViewId(id) {
|
|
2142
|
+
return typeof id === "string" && VALID_VIEW_IDS.has(id);
|
|
2278
2143
|
}
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2144
|
+
// src/format-time.ts
|
|
2145
|
+
function formatRelativeTime2(timestampMs, nowMs) {
|
|
2146
|
+
const now = nowMs ?? Date.now();
|
|
2147
|
+
const diffMs = now - timestampMs;
|
|
2148
|
+
if (diffMs < 0)
|
|
2149
|
+
return "just now";
|
|
2150
|
+
if (timestampMs === 0)
|
|
2151
|
+
return "\u2014";
|
|
2152
|
+
const seconds = Math.floor(diffMs / 1000);
|
|
2153
|
+
const minutes = Math.floor(seconds / 60);
|
|
2154
|
+
const hours = Math.floor(minutes / 60);
|
|
2155
|
+
const days = Math.floor(hours / 24);
|
|
2156
|
+
if (seconds < 60)
|
|
2157
|
+
return "< 1m ago";
|
|
2158
|
+
if (minutes < 60)
|
|
2159
|
+
return `${minutes}m ago`;
|
|
2160
|
+
if (hours < 24)
|
|
2161
|
+
return `${hours}h ago`;
|
|
2162
|
+
if (days < 30)
|
|
2163
|
+
return `${days}d ago`;
|
|
2164
|
+
return `> 30d ago`;
|
|
2284
2165
|
}
|
|
2285
|
-
function
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2166
|
+
function formatDuration2(durationMs) {
|
|
2167
|
+
const seconds = Math.floor(durationMs / 1000);
|
|
2168
|
+
const minutes = Math.floor(seconds / 60);
|
|
2169
|
+
const hours = Math.floor(minutes / 60);
|
|
2170
|
+
if (hours > 0) {
|
|
2171
|
+
const remainMin = minutes % 60;
|
|
2172
|
+
return remainMin > 0 ? `${hours}h ${remainMin}m` : `${hours}h`;
|
|
2173
|
+
}
|
|
2174
|
+
if (minutes > 0) {
|
|
2175
|
+
const remainSec = seconds % 60;
|
|
2176
|
+
return remainSec > 0 ? `${minutes}m ${remainSec}s` : `${minutes}m`;
|
|
2177
|
+
}
|
|
2178
|
+
return `${seconds}s`;
|
|
2289
2179
|
}
|
|
2290
|
-
|
|
2291
|
-
|
|
2180
|
+
// src/entitlements.ts
|
|
2181
|
+
function hasScope(ctx, scope) {
|
|
2182
|
+
if (!ctx || !Array.isArray(ctx.scopes))
|
|
2183
|
+
return false;
|
|
2184
|
+
return ctx.scopes.includes(scope);
|
|
2292
2185
|
}
|
|
2293
|
-
function
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
sendBatch: async (_messages) => {}
|
|
2297
|
-
};
|
|
2186
|
+
function verifyCacheKey(keyHashPrefix) {
|
|
2187
|
+
const clean = (keyHashPrefix ?? "").trim();
|
|
2188
|
+
return `hx:${clean}`;
|
|
2298
2189
|
}
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2190
|
+
// src/dashboard-manifest.ts
|
|
2191
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3 } from "fs";
|
|
2192
|
+
import { join as join3 } from "path";
|
|
2193
|
+
var DASHBOARD_WORKER_PREFIX = {
|
|
2194
|
+
hoox: "global:",
|
|
2195
|
+
"trade-worker": "trade:",
|
|
2196
|
+
"agent-worker": "agent:",
|
|
2197
|
+
"telegram-worker": "bot:",
|
|
2198
|
+
"d1-worker": "database:",
|
|
2199
|
+
"email-worker": "email:",
|
|
2200
|
+
"web3-wallet-worker": "wallet:",
|
|
2201
|
+
"analytics-worker": "ai:",
|
|
2202
|
+
"report-worker": "report:",
|
|
2203
|
+
"pyne-worker": "pyne:"
|
|
2204
|
+
};
|
|
2205
|
+
var DASHBOARD_SECTION_PREFIX = {
|
|
2206
|
+
global: "global:",
|
|
2207
|
+
webhook: "webhook:",
|
|
2208
|
+
routing: "routing:",
|
|
2209
|
+
notify: "telegram:",
|
|
2210
|
+
security: "webhook:",
|
|
2211
|
+
trade: "trade:",
|
|
2212
|
+
agent: "agent:",
|
|
2213
|
+
bot: "bot:",
|
|
2214
|
+
bot_ai: "bot:",
|
|
2215
|
+
email: "email:",
|
|
2216
|
+
signal: "email:",
|
|
2217
|
+
database: "database:",
|
|
2218
|
+
retention: "retention:",
|
|
2219
|
+
cron: "cron:",
|
|
2220
|
+
behavior: "behavior:",
|
|
2221
|
+
risk: "trade:",
|
|
2222
|
+
exchanges: "trade:",
|
|
2223
|
+
fees: "trade:",
|
|
2224
|
+
providers: "agent:",
|
|
2225
|
+
models: "agent:",
|
|
2226
|
+
ai: "ai:",
|
|
2227
|
+
tracking: "ai:",
|
|
2228
|
+
query: "ai:",
|
|
2229
|
+
report: "report:",
|
|
2230
|
+
schedule: "report:",
|
|
2231
|
+
rendering: "report:",
|
|
2232
|
+
storage: "report:",
|
|
2233
|
+
delivery: "report:",
|
|
2234
|
+
endpoints: "report:",
|
|
2235
|
+
wallet: "wallet:",
|
|
2236
|
+
dex: "wallet:",
|
|
2237
|
+
wallet_security: "wallet:",
|
|
2238
|
+
pyne: "pyne:",
|
|
2239
|
+
pyne_alerts: "pyne:"
|
|
2240
|
+
};
|
|
2241
|
+
var DASHBOARD_FIELD_KV_OVERRIDES = {
|
|
2242
|
+
"webhook:tradingview_ip_check_enabled": "webhook:tradingview:ip_check_enabled",
|
|
2243
|
+
"webhook:tradingview_allowed_ips": "webhook:tradingview:allowed_ips",
|
|
2244
|
+
"webhook:queue_mode": "webhook:queue_mode",
|
|
2245
|
+
"notify:allowed_chat_ids": "telegram:allowed_chat_ids",
|
|
2246
|
+
"exchanges:binance_enabled": "exchange:binance:enabled",
|
|
2247
|
+
"exchanges:mexc_enabled": "exchange:mexc:enabled",
|
|
2248
|
+
"exchanges:bybit_enabled": "exchange:bybit:enabled",
|
|
2249
|
+
"exchanges:uniswap_ethereum_enabled": "exchange:uniswap-ethereum:enabled",
|
|
2250
|
+
"exchanges:uniswap_arbitrum_enabled": "exchange:uniswap-arbitrum:enabled",
|
|
2251
|
+
"exchanges:jupiter_solana_enabled": "exchange:jupiter-solana:enabled",
|
|
2252
|
+
"wallet:config": "wallet:config"
|
|
2253
|
+
};
|
|
2254
|
+
var DASHBOARD_SECTIONS_NOT_FLAT_KV = new Set([
|
|
2255
|
+
"providers",
|
|
2256
|
+
"models",
|
|
2257
|
+
"cron",
|
|
2258
|
+
"behavior",
|
|
2259
|
+
"endpoints"
|
|
2260
|
+
]);
|
|
2261
|
+
var DASHBOARD_SECTIONS_UI_SKIP = new Set([
|
|
2262
|
+
"cron",
|
|
2263
|
+
"behavior",
|
|
2264
|
+
"endpoints"
|
|
2265
|
+
]);
|
|
2266
|
+
function isDashboardSectionFlatKv(sectionId) {
|
|
2267
|
+
return !DASHBOARD_SECTIONS_NOT_FLAT_KV.has(sectionId);
|
|
2333
2268
|
}
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
import {
|
|
2337
|
-
existsSync as existsSync3,
|
|
2338
|
-
mkdirSync as mkdirSync2,
|
|
2339
|
-
readFileSync as readFileSync3,
|
|
2340
|
-
writeFileSync as writeFileSync2,
|
|
2341
|
-
chmodSync as chmodSync2
|
|
2342
|
-
} from "fs";
|
|
2343
|
-
import { homedir as homedir2 } from "os";
|
|
2344
|
-
import { dirname, join as join3, resolve } from "path";
|
|
2345
|
-
function createHooxPath(path) {
|
|
2346
|
-
return path;
|
|
2269
|
+
function isDashboardSectionEditable(sectionId) {
|
|
2270
|
+
return !DASHBOARD_SECTIONS_UI_SKIP.has(sectionId);
|
|
2347
2271
|
}
|
|
2348
|
-
function
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
const
|
|
2355
|
-
if (
|
|
2356
|
-
|
|
2272
|
+
function buildDashboardKvKey(worker, fieldKey) {
|
|
2273
|
+
const override = DASHBOARD_FIELD_KV_OVERRIDES[fieldKey];
|
|
2274
|
+
if (override)
|
|
2275
|
+
return override;
|
|
2276
|
+
if (fieldKey.includes(":")) {
|
|
2277
|
+
const [section, ...rest] = fieldKey.split(":");
|
|
2278
|
+
const fieldName = rest.join(":");
|
|
2279
|
+
if (section === "exchanges" && fieldName.endsWith("_enabled")) {
|
|
2280
|
+
const exchange = fieldName.slice(0, -"_enabled".length);
|
|
2281
|
+
if (exchange)
|
|
2282
|
+
return `exchange:${exchange}:enabled`;
|
|
2357
2283
|
}
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2284
|
+
const mapped = DASHBOARD_SECTION_PREFIX[section ?? ""];
|
|
2285
|
+
if (mapped)
|
|
2286
|
+
return `${mapped}${fieldName}`;
|
|
2287
|
+
return `${section}:${fieldName}`;
|
|
2361
2288
|
}
|
|
2289
|
+
const workerPrefix = DASHBOARD_WORKER_PREFIX[worker] ?? "";
|
|
2290
|
+
return `${workerPrefix}${fieldKey}`;
|
|
2362
2291
|
}
|
|
2363
|
-
function
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2292
|
+
function stripJsonc(input) {
|
|
2293
|
+
let out = "";
|
|
2294
|
+
let i = 0;
|
|
2295
|
+
let inString = false;
|
|
2296
|
+
let stringChar = "";
|
|
2297
|
+
while (i < input.length) {
|
|
2298
|
+
const c = input[i];
|
|
2299
|
+
const next = input[i + 1];
|
|
2300
|
+
if (inString) {
|
|
2301
|
+
out += c;
|
|
2302
|
+
if (c === "\\" && i + 1 < input.length) {
|
|
2303
|
+
out += next;
|
|
2304
|
+
i += 2;
|
|
2305
|
+
continue;
|
|
2306
|
+
}
|
|
2307
|
+
if (c === stringChar)
|
|
2308
|
+
inString = false;
|
|
2309
|
+
i++;
|
|
2310
|
+
continue;
|
|
2370
2311
|
}
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2312
|
+
if (c === '"' || c === "'") {
|
|
2313
|
+
inString = true;
|
|
2314
|
+
stringChar = c;
|
|
2315
|
+
out += c;
|
|
2316
|
+
i++;
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
if (c === "/" && next === "/") {
|
|
2320
|
+
while (i < input.length && input[i] !== `
|
|
2321
|
+
`)
|
|
2322
|
+
i++;
|
|
2323
|
+
continue;
|
|
2324
|
+
}
|
|
2325
|
+
if (c === "/" && next === "*") {
|
|
2326
|
+
i += 2;
|
|
2327
|
+
while (i < input.length && !(input[i] === "*" && input[i + 1] === "/"))
|
|
2328
|
+
i++;
|
|
2329
|
+
i += 2;
|
|
2330
|
+
continue;
|
|
2331
|
+
}
|
|
2332
|
+
if (c === ",") {
|
|
2333
|
+
let j = i + 1;
|
|
2334
|
+
while (j < input.length && /\s/.test(input[j]))
|
|
2335
|
+
j++;
|
|
2336
|
+
if (j < input.length && (input[j] === "}" || input[j] === "]")) {
|
|
2337
|
+
i++;
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
out += c;
|
|
2342
|
+
i++;
|
|
2374
2343
|
}
|
|
2344
|
+
return out;
|
|
2375
2345
|
}
|
|
2376
|
-
function
|
|
2377
|
-
|
|
2346
|
+
function parseFieldType(value) {
|
|
2347
|
+
if (typeof value === "boolean")
|
|
2348
|
+
return "boolean";
|
|
2349
|
+
if (typeof value === "number")
|
|
2350
|
+
return "number";
|
|
2351
|
+
if (value === "true" || value === "false")
|
|
2352
|
+
return "boolean";
|
|
2353
|
+
if (value.trim().startsWith("{") || value.trim().startsWith("["))
|
|
2354
|
+
return "json";
|
|
2355
|
+
if (!Number.isNaN(Number(value)) && value.trim() !== "")
|
|
2356
|
+
return "number";
|
|
2357
|
+
return "text";
|
|
2378
2358
|
}
|
|
2379
|
-
function
|
|
2380
|
-
|
|
2381
|
-
const path = getRememberedMonorepoPath();
|
|
2382
|
-
if (!existsSync3(path))
|
|
2383
|
-
return null;
|
|
2384
|
-
const raw = readFileSync3(path, "utf-8");
|
|
2385
|
-
const data = JSON.parse(raw);
|
|
2386
|
-
if (!data.root || typeof data.root !== "string")
|
|
2387
|
-
return null;
|
|
2388
|
-
const resolved = resolve(data.root);
|
|
2389
|
-
return isHooxSetupRoot(resolved) ? resolved : null;
|
|
2390
|
-
} catch {
|
|
2391
|
-
return null;
|
|
2392
|
-
}
|
|
2359
|
+
function labelFromKey(key) {
|
|
2360
|
+
return key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2393
2361
|
}
|
|
2394
|
-
function
|
|
2362
|
+
function parseDashboardManifest(content, workerName, onError) {
|
|
2395
2363
|
try {
|
|
2396
|
-
const
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
const
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
if (envRepo) {
|
|
2439
|
-
const resolved = resolve(envRepo);
|
|
2440
|
-
if (isHooxSetupRoot(resolved)) {
|
|
2441
|
-
return {
|
|
2442
|
-
root: resolved,
|
|
2443
|
-
source: "env",
|
|
2444
|
-
checked: {
|
|
2445
|
-
env: resolved,
|
|
2446
|
-
cwd: findHooxSetupRoot(cwd),
|
|
2447
|
-
remembered,
|
|
2448
|
-
global: globalRepo
|
|
2364
|
+
const raw = JSON.parse(stripJsonc(content));
|
|
2365
|
+
const displayName = raw.display_name || raw.displayName || workerName;
|
|
2366
|
+
const description = raw.description || "";
|
|
2367
|
+
const sections = [];
|
|
2368
|
+
if (raw.sections) {
|
|
2369
|
+
for (const [sectionId, sectionData] of Object.entries(raw.sections)) {
|
|
2370
|
+
const fields = [];
|
|
2371
|
+
const sectionFields = sectionData.fields || {};
|
|
2372
|
+
const sectionOptions = sectionData.options || {};
|
|
2373
|
+
const sectionDescriptions = sectionData.descriptions || {};
|
|
2374
|
+
const sectionValidation = sectionData.validation || {};
|
|
2375
|
+
const sectionSecrets = sectionData.secrets || {};
|
|
2376
|
+
const sectionSecretCommands = sectionData.secret_commands || {};
|
|
2377
|
+
for (const [key, value] of Object.entries(sectionFields)) {
|
|
2378
|
+
const field = {
|
|
2379
|
+
key: `${sectionId}:${key}`,
|
|
2380
|
+
label: labelFromKey(key),
|
|
2381
|
+
type: parseFieldType(value),
|
|
2382
|
+
default: value
|
|
2383
|
+
};
|
|
2384
|
+
if (sectionOptions[key]) {
|
|
2385
|
+
field.type = "select";
|
|
2386
|
+
field.options = sectionOptions[key].map((opt) => ({
|
|
2387
|
+
value: String(opt),
|
|
2388
|
+
label: String(opt)
|
|
2389
|
+
}));
|
|
2390
|
+
}
|
|
2391
|
+
if (sectionDescriptions[key]) {
|
|
2392
|
+
field.description = String(sectionDescriptions[key]);
|
|
2393
|
+
}
|
|
2394
|
+
if (sectionValidation[key]) {
|
|
2395
|
+
field.validation = sectionValidation[key];
|
|
2396
|
+
}
|
|
2397
|
+
if (sectionSecrets[key]) {
|
|
2398
|
+
field.kind = "secret";
|
|
2399
|
+
field.cliCommand = sectionSecretCommands[key];
|
|
2400
|
+
}
|
|
2401
|
+
if (key.includes("kill_switch") || key.includes("drawdown") || key === "enabled" && sectionId === "global") {
|
|
2402
|
+
if (field.kind !== "secret")
|
|
2403
|
+
field.kind = "dangerous";
|
|
2404
|
+
}
|
|
2405
|
+
fields.push(field);
|
|
2449
2406
|
}
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
remembered,
|
|
2459
|
-
global: globalRepo
|
|
2407
|
+
sections.push({
|
|
2408
|
+
id: sectionId,
|
|
2409
|
+
title: sectionData.title || labelFromKey(sectionId),
|
|
2410
|
+
description: sectionData.description || `Configure ${sectionId} settings`,
|
|
2411
|
+
icon: sectionData.icon,
|
|
2412
|
+
priority: sectionData.priority !== undefined ? sectionData.priority : sections.length * 10,
|
|
2413
|
+
fields
|
|
2414
|
+
});
|
|
2460
2415
|
}
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
2463
|
-
const local = findHooxSetupRoot(cwd);
|
|
2464
|
-
if (local) {
|
|
2465
|
-
return {
|
|
2466
|
-
root: local,
|
|
2467
|
-
source: "cwd",
|
|
2468
|
-
checked: { cwd: local, remembered, global: globalRepo }
|
|
2469
|
-
};
|
|
2470
|
-
}
|
|
2471
|
-
if (remembered) {
|
|
2416
|
+
}
|
|
2472
2417
|
return {
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2418
|
+
worker: workerName,
|
|
2419
|
+
displayName,
|
|
2420
|
+
description,
|
|
2421
|
+
sections: sections.sort((a, b) => a.priority - b.priority)
|
|
2476
2422
|
};
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2423
|
+
} catch (err) {
|
|
2424
|
+
const message = err instanceof Error ? err.message : `parse failed: ${String(err)}`;
|
|
2425
|
+
onError?.(`dashboard.jsonc for ${workerName}: ${message}`);
|
|
2479
2426
|
return {
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2427
|
+
worker: workerName,
|
|
2428
|
+
displayName: workerName,
|
|
2429
|
+
description: "Failed to load configuration",
|
|
2430
|
+
sections: []
|
|
2483
2431
|
};
|
|
2484
2432
|
}
|
|
2485
|
-
return {
|
|
2486
|
-
root: null,
|
|
2487
|
-
source: "none",
|
|
2488
|
-
checked: { cwd: null, remembered: null, global: globalRepo }
|
|
2489
|
-
};
|
|
2490
2433
|
}
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2434
|
+
var DASHBOARD_WORKER_IDS = [
|
|
2435
|
+
"hoox",
|
|
2436
|
+
"trade-worker",
|
|
2437
|
+
"agent-worker",
|
|
2438
|
+
"telegram-worker",
|
|
2439
|
+
"d1-worker",
|
|
2440
|
+
"email-worker",
|
|
2441
|
+
"web3-wallet-worker",
|
|
2442
|
+
"analytics-worker",
|
|
2443
|
+
"report-worker",
|
|
2444
|
+
"pyne-worker"
|
|
2445
|
+
];
|
|
2446
|
+
function dashboardWorkerDir(workerId) {
|
|
2447
|
+
if (workerId === "hoox")
|
|
2448
|
+
return "hoox-worker";
|
|
2449
|
+
return workerId;
|
|
2498
2450
|
}
|
|
2499
|
-
function
|
|
2500
|
-
if (
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
}
|
|
2506
|
-
const normalizedPath = relativePath.replace(/\\/g, "/");
|
|
2507
|
-
const hooxHome = getHooxHome();
|
|
2508
|
-
const fullPath = join3(hooxHome, normalizedPath);
|
|
2509
|
-
const resolvedPath = resolve(fullPath);
|
|
2510
|
-
const resolvedHome = resolve(hooxHome);
|
|
2511
|
-
if (!resolvedPath.startsWith(resolvedHome)) {
|
|
2512
|
-
throw new Error(`Resolved path "${resolvedPath}" is outside Hoox home directory`);
|
|
2513
|
-
}
|
|
2514
|
-
return createHooxPath(resolvedPath);
|
|
2451
|
+
function fieldTypeToKv(t) {
|
|
2452
|
+
if (t === "boolean")
|
|
2453
|
+
return "boolean";
|
|
2454
|
+
if (t === "number")
|
|
2455
|
+
return "number";
|
|
2456
|
+
return "string";
|
|
2515
2457
|
}
|
|
2516
|
-
function
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
const resolvedHome = resolve(hooxHome);
|
|
2521
|
-
return resolvedPath.startsWith(resolvedHome);
|
|
2522
|
-
} catch {
|
|
2523
|
-
return false;
|
|
2524
|
-
}
|
|
2458
|
+
function defaultToString(value) {
|
|
2459
|
+
if (typeof value === "string")
|
|
2460
|
+
return value;
|
|
2461
|
+
return JSON.stringify(value);
|
|
2525
2462
|
}
|
|
2526
|
-
function
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2463
|
+
function isDocumentationField(fieldKey) {
|
|
2464
|
+
const name = fieldKey.includes(":") ? fieldKey.slice(fieldKey.indexOf(":") + 1) : fieldKey;
|
|
2465
|
+
return /^(GET|POST|PUT|PATCH|DELETE)\s+\//i.test(name);
|
|
2466
|
+
}
|
|
2467
|
+
function kvManifestFromDashboardManifests(manifests) {
|
|
2468
|
+
const byKey = new Map;
|
|
2469
|
+
for (const manifest of manifests) {
|
|
2470
|
+
for (const section of manifest.sections) {
|
|
2471
|
+
if (!isDashboardSectionFlatKv(section.id))
|
|
2472
|
+
continue;
|
|
2473
|
+
for (const field of section.fields) {
|
|
2474
|
+
if (isDocumentationField(field.key))
|
|
2475
|
+
continue;
|
|
2476
|
+
const key = buildDashboardKvKey(manifest.worker, field.key);
|
|
2477
|
+
if (!key || key.endsWith(":"))
|
|
2478
|
+
continue;
|
|
2479
|
+
const next = {
|
|
2480
|
+
key,
|
|
2481
|
+
type: fieldTypeToKv(field.type),
|
|
2482
|
+
default: defaultToString(field.default),
|
|
2483
|
+
description: field.description || `${manifest.displayName}: ${field.label}`,
|
|
2484
|
+
secret: field.kind === "secret" ? true : undefined,
|
|
2485
|
+
worker: manifest.worker
|
|
2486
|
+
};
|
|
2487
|
+
const prev = byKey.get(key);
|
|
2488
|
+
if (!prev) {
|
|
2489
|
+
byKey.set(key, next);
|
|
2490
|
+
continue;
|
|
2491
|
+
}
|
|
2492
|
+
if (!prev.secret && next.secret)
|
|
2493
|
+
prev.secret = true;
|
|
2494
|
+
if ((!prev.description || prev.description.length < 8) && next.description) {
|
|
2495
|
+
prev.description = next.description;
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2530
2498
|
}
|
|
2531
|
-
const hooxHome = getHooxHome();
|
|
2532
|
-
const resolvedPath = resolve(absolutePath);
|
|
2533
|
-
const resolvedHome = resolve(hooxHome);
|
|
2534
|
-
const relativePath = resolvedPath.slice(resolvedHome.length);
|
|
2535
|
-
return relativePath.startsWith("/") || relativePath.startsWith("\\") ? relativePath.slice(1) : relativePath;
|
|
2536
|
-
} catch {
|
|
2537
|
-
return null;
|
|
2538
2499
|
}
|
|
2500
|
+
const keys = [...byKey.values()].sort((a, b) => a.key.localeCompare(b.key));
|
|
2501
|
+
return { namespace: "CONFIG_KV", keys };
|
|
2539
2502
|
}
|
|
2540
|
-
function
|
|
2541
|
-
return
|
|
2542
|
-
}
|
|
2543
|
-
function getHooxConfigDir() {
|
|
2544
|
-
return resolveHooxPath("config");
|
|
2545
|
-
}
|
|
2546
|
-
function getHooxDataDir() {
|
|
2547
|
-
return resolveHooxPath("data");
|
|
2548
|
-
}
|
|
2549
|
-
function getHooxWranglerPath() {
|
|
2550
|
-
return resolveHooxPath("config/wrangler.jsonc");
|
|
2551
|
-
}
|
|
2552
|
-
function getHooxStatePath() {
|
|
2553
|
-
return resolveHooxPath("data/state.json");
|
|
2503
|
+
function loadDashboardKvManifestFromRoot(root, onError) {
|
|
2504
|
+
return kvManifestFromDashboardManifests(loadDashboardManifestsFromRoot(root, onError));
|
|
2554
2505
|
}
|
|
2555
|
-
|
|
2556
|
-
// src/session.ts
|
|
2557
|
-
import { z as z3 } from "zod";
|
|
2558
|
-
var SessionStateFileSchema = z3.object({
|
|
2559
|
-
activeView: z3.string().optional(),
|
|
2560
|
-
sidebarExpanded: z3.boolean().optional(),
|
|
2561
|
-
windowSize: z3.object({
|
|
2562
|
-
cols: z3.number().int().positive(),
|
|
2563
|
-
rows: z3.number().int().positive()
|
|
2564
|
-
}).optional(),
|
|
2565
|
-
lastData: z3.number().nonnegative().optional(),
|
|
2566
|
-
savedAt: z3.string().optional()
|
|
2567
|
-
}).strict();
|
|
2568
|
-
var TUI_STATE_DIR = ".tui-state";
|
|
2569
|
-
function getSessionFilePath() {
|
|
2506
|
+
function tryReadFile(path) {
|
|
2570
2507
|
try {
|
|
2571
|
-
|
|
2572
|
-
|
|
2508
|
+
if (!existsSync3(path))
|
|
2509
|
+
return null;
|
|
2510
|
+
return readFileSync3(path, "utf-8");
|
|
2573
2511
|
} catch {
|
|
2574
|
-
return
|
|
2512
|
+
return null;
|
|
2575
2513
|
}
|
|
2576
2514
|
}
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2515
|
+
function loadDashboardManifestsFromRoot(root, onError) {
|
|
2516
|
+
const manifests = [];
|
|
2517
|
+
const seen = new Set;
|
|
2518
|
+
const workersDir = join3(root, "workers");
|
|
2519
|
+
if (!existsSync3(workersDir))
|
|
2520
|
+
return manifests;
|
|
2521
|
+
const warn = onError ?? ((message) => {
|
|
2522
|
+
console.warn(`[dashboard-manifest] ${message}`);
|
|
2523
|
+
});
|
|
2524
|
+
for (const workerId of DASHBOARD_WORKER_IDS) {
|
|
2525
|
+
const dir = dashboardWorkerDir(workerId);
|
|
2526
|
+
const path = join3(workersDir, dir, "dashboard.jsonc");
|
|
2527
|
+
const content = tryReadFile(path);
|
|
2528
|
+
if (!content)
|
|
2529
|
+
continue;
|
|
2530
|
+
const m = parseDashboardManifest(content, workerId, warn);
|
|
2531
|
+
if (m.sections.length > 0) {
|
|
2532
|
+
manifests.push(m);
|
|
2533
|
+
seen.add(workerId);
|
|
2534
|
+
} else if (m.description === "Failed to load configuration") {}
|
|
2535
|
+
}
|
|
2585
2536
|
try {
|
|
2586
|
-
const
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2537
|
+
for (const entry of readdirSync(workersDir)) {
|
|
2538
|
+
if (entry === "dashboard")
|
|
2539
|
+
continue;
|
|
2540
|
+
const workerId = entry === "hoox-worker" || entry === "hoox" ? "hoox" : entry;
|
|
2541
|
+
if (seen.has(workerId))
|
|
2542
|
+
continue;
|
|
2543
|
+
const path = join3(workersDir, entry, "dashboard.jsonc");
|
|
2544
|
+
const content = tryReadFile(path);
|
|
2545
|
+
if (!content)
|
|
2546
|
+
continue;
|
|
2547
|
+
const m = parseDashboardManifest(content, workerId, warn);
|
|
2548
|
+
if (m.sections.length > 0) {
|
|
2549
|
+
manifests.push(m);
|
|
2550
|
+
seen.add(workerId);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
} catch {}
|
|
2554
|
+
const publicDir = join3(workersDir, "dashboard", "public", "workers");
|
|
2555
|
+
if (existsSync3(publicDir)) {
|
|
2556
|
+
try {
|
|
2557
|
+
for (const file of readdirSync(publicDir)) {
|
|
2558
|
+
if (!file.endsWith(".jsonc") && !file.endsWith(".json"))
|
|
2559
|
+
continue;
|
|
2560
|
+
const base = file.replace(/\.jsonc?$/, "");
|
|
2561
|
+
const workerId = base === "hoox-worker" || base === "hoox" ? "hoox" : base;
|
|
2562
|
+
if (seen.has(workerId))
|
|
2563
|
+
continue;
|
|
2564
|
+
const content = tryReadFile(join3(publicDir, file));
|
|
2565
|
+
if (!content)
|
|
2566
|
+
continue;
|
|
2567
|
+
const m = parseDashboardManifest(content, workerId, warn);
|
|
2568
|
+
if (m.sections.length > 0) {
|
|
2569
|
+
manifests.push(m);
|
|
2570
|
+
seen.add(workerId);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
} catch {}
|
|
2596
2574
|
}
|
|
2575
|
+
return manifests;
|
|
2597
2576
|
}
|
|
2598
|
-
|
|
2577
|
+
// src/agent-config-fields.ts
|
|
2578
|
+
var AGENT_CONFIG_KV_KEY = KV_AGENT_CONFIG;
|
|
2579
|
+
var AGENT_CONFIG_EMBEDDED_FIELDS = {
|
|
2580
|
+
"providers:default_provider": { kind: "top", key: "defaultProvider" },
|
|
2581
|
+
"providers:fallback_chain": { kind: "top", key: "fallbackChain" },
|
|
2582
|
+
"providers:timeout_ms": { kind: "top", key: "timeoutMs" },
|
|
2583
|
+
"providers:retry_count": { kind: "top", key: "retryCount" },
|
|
2584
|
+
"models:workers_ai_model": { kind: "model", provider: "workers-ai" },
|
|
2585
|
+
"models:openai_model": { kind: "model", provider: "openai" },
|
|
2586
|
+
"models:anthropic_model": { kind: "model", provider: "anthropic" },
|
|
2587
|
+
"models:google_model": { kind: "model", provider: "google" },
|
|
2588
|
+
"risk:max_daily_drawdown_percent": {
|
|
2589
|
+
kind: "top",
|
|
2590
|
+
key: "maxDailyDrawdownPercent"
|
|
2591
|
+
},
|
|
2592
|
+
"risk:trailing_stop_percent": { kind: "top", key: "trailingStopPercent" },
|
|
2593
|
+
"risk:take_profit_percent": { kind: "top", key: "takeProfitPercent" }
|
|
2594
|
+
};
|
|
2595
|
+
var DASHBOARD_SECTIONS_AGENT_CONFIG = new Set([
|
|
2596
|
+
"providers",
|
|
2597
|
+
"models"
|
|
2598
|
+
]);
|
|
2599
|
+
function isAgentConfigEmbeddedField(fieldKey) {
|
|
2600
|
+
return Object.prototype.hasOwnProperty.call(AGENT_CONFIG_EMBEDDED_FIELDS, fieldKey);
|
|
2601
|
+
}
|
|
2602
|
+
function isAgentConfigSection(sectionId) {
|
|
2603
|
+
return DASHBOARD_SECTIONS_AGENT_CONFIG.has(sectionId);
|
|
2604
|
+
}
|
|
2605
|
+
function parseAgentConfigJson(raw) {
|
|
2606
|
+
if (raw === null || raw === undefined || raw === "")
|
|
2607
|
+
return {};
|
|
2599
2608
|
try {
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
};
|
|
2614
|
-
} catch {
|
|
2615
|
-
return { ...DEFAULT_SESSION };
|
|
2616
|
-
}
|
|
2609
|
+
let v = JSON.parse(raw);
|
|
2610
|
+
if (typeof v === "string") {
|
|
2611
|
+
try {
|
|
2612
|
+
v = JSON.parse(v);
|
|
2613
|
+
} catch {
|
|
2614
|
+
return {};
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
2618
|
+
return { ...v };
|
|
2619
|
+
}
|
|
2620
|
+
} catch {}
|
|
2621
|
+
return {};
|
|
2617
2622
|
}
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
"
|
|
2623
|
-
"
|
|
2624
|
-
"
|
|
2625
|
-
"
|
|
2626
|
-
"
|
|
2627
|
-
"settings",
|
|
2628
|
-
"queue-depth",
|
|
2629
|
-
"kv-viewer",
|
|
2630
|
-
"secrets-viewer",
|
|
2631
|
-
"db-query",
|
|
2632
|
-
"ai-chat",
|
|
2633
|
-
"edge-topology",
|
|
2634
|
-
"worker-settings"
|
|
2623
|
+
function serializeAgentConfigForKv(config) {
|
|
2624
|
+
return JSON.stringify(config);
|
|
2625
|
+
}
|
|
2626
|
+
var NUMBER_TOP_KEYS = new Set([
|
|
2627
|
+
"timeoutMs",
|
|
2628
|
+
"retryCount",
|
|
2629
|
+
"maxDailyDrawdownPercent",
|
|
2630
|
+
"trailingStopPercent",
|
|
2631
|
+
"takeProfitPercent"
|
|
2635
2632
|
]);
|
|
2636
|
-
function
|
|
2637
|
-
|
|
2633
|
+
function getAgentConfigEmbeddedValue(config, fieldKey) {
|
|
2634
|
+
const spec = AGENT_CONFIG_EMBEDDED_FIELDS[fieldKey];
|
|
2635
|
+
if (!spec)
|
|
2636
|
+
return;
|
|
2637
|
+
if (spec.kind === "model") {
|
|
2638
|
+
const map = config.modelMap;
|
|
2639
|
+
if (!map || typeof map !== "object" || Array.isArray(map))
|
|
2640
|
+
return;
|
|
2641
|
+
const v = map[spec.provider];
|
|
2642
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean")
|
|
2643
|
+
return v;
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
const v = config[spec.key];
|
|
2647
|
+
if (spec.key === "fallbackChain") {
|
|
2648
|
+
if (Array.isArray(v))
|
|
2649
|
+
return JSON.stringify(v);
|
|
2650
|
+
if (typeof v === "string")
|
|
2651
|
+
return v;
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
2655
|
+
return v;
|
|
2656
|
+
}
|
|
2657
|
+
return;
|
|
2638
2658
|
}
|
|
2639
|
-
|
|
2659
|
+
function setAgentConfigEmbeddedValue(config, fieldKey, value) {
|
|
2660
|
+
const spec = AGENT_CONFIG_EMBEDDED_FIELDS[fieldKey];
|
|
2661
|
+
if (!spec)
|
|
2662
|
+
return { ...config };
|
|
2663
|
+
const next = { ...config };
|
|
2664
|
+
if (spec.kind === "model") {
|
|
2665
|
+
const prev = next.modelMap && typeof next.modelMap === "object" && !Array.isArray(next.modelMap) ? next.modelMap : {};
|
|
2666
|
+
next.modelMap = { ...prev, [spec.provider]: String(value) };
|
|
2667
|
+
return next;
|
|
2668
|
+
}
|
|
2669
|
+
if (spec.key === "fallbackChain") {
|
|
2670
|
+
if (typeof value === "string") {
|
|
2671
|
+
try {
|
|
2672
|
+
next.fallbackChain = JSON.parse(value);
|
|
2673
|
+
} catch {
|
|
2674
|
+
next.fallbackChain = value;
|
|
2675
|
+
}
|
|
2676
|
+
} else {
|
|
2677
|
+
next.fallbackChain = value;
|
|
2678
|
+
}
|
|
2679
|
+
return next;
|
|
2680
|
+
}
|
|
2681
|
+
if (NUMBER_TOP_KEYS.has(spec.key)) {
|
|
2682
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
2683
|
+
next[spec.key] = Number.isFinite(n) ? n : value;
|
|
2684
|
+
return next;
|
|
2685
|
+
}
|
|
2686
|
+
next[spec.key] = value;
|
|
2687
|
+
return next;
|
|
2688
|
+
}
|
|
2689
|
+
function expandAgentConfigToFieldMap(config) {
|
|
2690
|
+
const out = {};
|
|
2691
|
+
for (const fieldKey of Object.keys(AGENT_CONFIG_EMBEDDED_FIELDS)) {
|
|
2692
|
+
const v = getAgentConfigEmbeddedValue(config, fieldKey);
|
|
2693
|
+
if (v !== undefined)
|
|
2694
|
+
out[fieldKey] = v;
|
|
2695
|
+
}
|
|
2696
|
+
return out;
|
|
2697
|
+
}
|
|
2698
|
+
function applyAgentConfigFieldUpdates(config, updates) {
|
|
2699
|
+
let next = { ...config };
|
|
2700
|
+
for (const [fieldKey, value] of Object.entries(updates)) {
|
|
2701
|
+
if (isAgentConfigEmbeddedField(fieldKey)) {
|
|
2702
|
+
next = setAgentConfigEmbeddedValue(next, fieldKey, value);
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
return next;
|
|
2706
|
+
}
|
|
2707
|
+
// src/wizard/types.ts
|
|
2708
|
+
var CLOUDFLARE_API_TOKEN_PLACEHOLDER = "<USE_CLOUDFLARE_API_TOKEN_ENV_OR_WRANGLER_AUTH>";
|
|
2640
2709
|
// src/wizard/presets.ts
|
|
2641
2710
|
var PRESETS = [
|
|
2642
2711
|
{
|
|
@@ -2721,6 +2790,10 @@ var INTEGRATIONS = [
|
|
|
2721
2790
|
secrets: {
|
|
2722
2791
|
WALLET_MNEMONIC_SECRET: "Wallet Mnemonic Phrase",
|
|
2723
2792
|
WALLET_PK_SECRET: "Wallet Private Key"
|
|
2793
|
+
},
|
|
2794
|
+
optionalSecrets: {
|
|
2795
|
+
SOLANA_PRIVATE_KEY: "Solana private key (base58; leave empty to auto-generate)",
|
|
2796
|
+
JUPITER_API_KEY: "Jupiter Swap API key (portal.jup.ag; Solana DEX)"
|
|
2724
2797
|
}
|
|
2725
2798
|
},
|
|
2726
2799
|
{
|
|
@@ -2789,7 +2862,7 @@ var BASE_WORKERS = {
|
|
|
2789
2862
|
"d1-worker": {
|
|
2790
2863
|
enabled: true,
|
|
2791
2864
|
path: "workers/d1-worker",
|
|
2792
|
-
vars: { database_name: "
|
|
2865
|
+
vars: { database_name: "trade-data-db" }
|
|
2793
2866
|
},
|
|
2794
2867
|
hoox: { enabled: true, path: "workers/hoox-worker", vars: {} },
|
|
2795
2868
|
"agent-worker": { enabled: true, path: "workers/agent-worker", vars: {} },
|
|
@@ -3020,7 +3093,10 @@ class WizardEngine {
|
|
|
3020
3093
|
if (integration.vars) {
|
|
3021
3094
|
Object.assign(workers[workerName].vars, integration.vars);
|
|
3022
3095
|
}
|
|
3023
|
-
for (const secretName of
|
|
3096
|
+
for (const secretName of [
|
|
3097
|
+
...Object.keys(integration.secrets),
|
|
3098
|
+
...Object.keys(integration.optionalSecrets ?? {})
|
|
3099
|
+
]) {
|
|
3024
3100
|
if (!workers[workerName].secrets.includes(secretName)) {
|
|
3025
3101
|
workers[workerName].secrets.push(secretName);
|
|
3026
3102
|
}
|
|
@@ -3038,7 +3114,7 @@ class WizardEngine {
|
|
|
3038
3114
|
}
|
|
3039
3115
|
return {
|
|
3040
3116
|
global: {
|
|
3041
|
-
cloudflare_api_token:
|
|
3117
|
+
cloudflare_api_token: CLOUDFLARE_API_TOKEN_PLACEHOLDER,
|
|
3042
3118
|
cloudflare_account_id: cf?.accountId ?? "",
|
|
3043
3119
|
cloudflare_secret_store_id: cf?.secretStoreId ?? "",
|
|
3044
3120
|
subdomain_prefix: cf?.subdomain ?? ""
|
|
@@ -3051,19 +3127,20 @@ class WizardEngine {
|
|
|
3051
3127
|
d1Databases: [],
|
|
3052
3128
|
kvNamespaces: [],
|
|
3053
3129
|
r2Buckets: [],
|
|
3054
|
-
queues: []
|
|
3130
|
+
queues: [],
|
|
3131
|
+
vectorizeIndexes: []
|
|
3055
3132
|
};
|
|
3056
3133
|
const allWorkers = [
|
|
3057
3134
|
...Object.keys(BASE_WORKERS),
|
|
3058
3135
|
...this.state.selectedWorkers,
|
|
3059
3136
|
...this.state.selectedIntegrations.map((k) => {
|
|
3060
|
-
const i = INTEGRATIONS.find((
|
|
3137
|
+
const i = INTEGRATIONS.find((i) => i.key === k);
|
|
3061
3138
|
return i?.workerName ?? "";
|
|
3062
3139
|
}).filter(Boolean)
|
|
3063
3140
|
];
|
|
3064
3141
|
const workerSet = new Set(allWorkers);
|
|
3065
3142
|
if (workerSet.has("d1-worker")) {
|
|
3066
|
-
plan.d1Databases.push("
|
|
3143
|
+
plan.d1Databases.push("trade-data-db");
|
|
3067
3144
|
}
|
|
3068
3145
|
if (workerSet.has("hoox") || workerSet.has("analytics-worker")) {
|
|
3069
3146
|
plan.kvNamespaces.push("CONFIG_KV");
|
|
@@ -3071,8 +3148,13 @@ class WizardEngine {
|
|
|
3071
3148
|
if (workerSet.has("web3-wallet-worker")) {
|
|
3072
3149
|
plan.kvNamespaces.push("WEB3_CACHE_KV");
|
|
3073
3150
|
}
|
|
3151
|
+
if (workerSet.has("hoox") || workerSet.has("telegram-worker")) {
|
|
3152
|
+
plan.vectorizeIndexes = plan.vectorizeIndexes ?? [];
|
|
3153
|
+
plan.vectorizeIndexes.push("hoox-rag-index");
|
|
3154
|
+
}
|
|
3074
3155
|
plan.d1Databases = [...new Set(plan.d1Databases)];
|
|
3075
3156
|
plan.kvNamespaces = [...new Set(plan.kvNamespaces)];
|
|
3157
|
+
plan.vectorizeIndexes = [...new Set(plan.vectorizeIndexes ?? [])];
|
|
3076
3158
|
return plan;
|
|
3077
3159
|
}
|
|
3078
3160
|
applyInput(input) {
|
|
@@ -3125,10 +3207,13 @@ class WizardEngine {
|
|
|
3125
3207
|
}
|
|
3126
3208
|
}
|
|
3127
3209
|
}
|
|
3128
|
-
|
|
3129
3210
|
// src/wizard/persistence.ts
|
|
3130
3211
|
function serializeState(state) {
|
|
3131
|
-
|
|
3212
|
+
const copy = {
|
|
3213
|
+
...state,
|
|
3214
|
+
cloudflareConfig: state.cloudflareConfig ? { ...state.cloudflareConfig, apiToken: "" } : undefined
|
|
3215
|
+
};
|
|
3216
|
+
return JSON.stringify(copy, null, 2);
|
|
3132
3217
|
}
|
|
3133
3218
|
function deserializeState(json) {
|
|
3134
3219
|
const parsed = JSON.parse(json);
|
|
@@ -3241,6 +3326,10 @@ var manifests = {
|
|
|
3241
3326
|
EXCHANGE_TESTNET_SECRET_BINDING: {
|
|
3242
3327
|
type: "secret",
|
|
3243
3328
|
description: "Optional unified testnet API secret"
|
|
3329
|
+
},
|
|
3330
|
+
WALLET_EXECUTE_KEY_BINDING: {
|
|
3331
|
+
type: "secret",
|
|
3332
|
+
description: "Mesh key for DEX swaps via web3-wallet-worker"
|
|
3244
3333
|
}
|
|
3245
3334
|
},
|
|
3246
3335
|
services: [
|
|
@@ -3253,6 +3342,11 @@ var manifests = {
|
|
|
3253
3342
|
binding: "TELEGRAM_SERVICE",
|
|
3254
3343
|
service: "telegram-worker",
|
|
3255
3344
|
description: "Notifications"
|
|
3345
|
+
},
|
|
3346
|
+
{
|
|
3347
|
+
binding: "WEB3_WALLET_SERVICE",
|
|
3348
|
+
service: "web3-wallet-worker",
|
|
3349
|
+
description: "DEX spot swaps (Uniswap / Jupiter)"
|
|
3256
3350
|
}
|
|
3257
3351
|
],
|
|
3258
3352
|
infrastructure: {
|
|
@@ -3350,6 +3444,22 @@ var manifests = {
|
|
|
3350
3444
|
WALLET_MNEMONIC_SECRET: {
|
|
3351
3445
|
type: "secret",
|
|
3352
3446
|
description: "Wallet mnemonic phrase"
|
|
3447
|
+
},
|
|
3448
|
+
WALLET_EXECUTE_KEY_BINDING: {
|
|
3449
|
+
type: "secret",
|
|
3450
|
+
description: "Mesh key for privileged wallet routes"
|
|
3451
|
+
},
|
|
3452
|
+
SOLANA_PRIVATE_KEY: {
|
|
3453
|
+
type: "secret",
|
|
3454
|
+
description: "Solana signing key (base58 seed or Phantom export)"
|
|
3455
|
+
},
|
|
3456
|
+
JUPITER_API_KEY: {
|
|
3457
|
+
type: "secret",
|
|
3458
|
+
description: "Jupiter Swap V2 API key"
|
|
3459
|
+
},
|
|
3460
|
+
RPC_URL_SOLANA: {
|
|
3461
|
+
type: "plaintext",
|
|
3462
|
+
description: "Solana JSON-RPC URL"
|
|
3353
3463
|
}
|
|
3354
3464
|
},
|
|
3355
3465
|
services: [
|
|
@@ -3587,7 +3697,6 @@ var WORKER_CATALOG = WORKER_NAMES.map((name) => {
|
|
|
3587
3697
|
function getWorkerDefaultEnabled(name) {
|
|
3588
3698
|
return manifests[name]?.defaultEnabled;
|
|
3589
3699
|
}
|
|
3590
|
-
|
|
3591
3700
|
// src/schemas/validators.ts
|
|
3592
3701
|
import { parse } from "jsonc-parser";
|
|
3593
3702
|
function validateWranglerJsonc(workerName, manifest, jsoncContent) {
|
|
@@ -3904,7 +4013,6 @@ var useUIStore = create()(immer((set) => ({
|
|
|
3904
4013
|
state.statusErrorExpanded = expanded;
|
|
3905
4014
|
})
|
|
3906
4015
|
})));
|
|
3907
|
-
|
|
3908
4016
|
// src/stores/service-store.ts
|
|
3909
4017
|
import { create as create2 } from "zustand";
|
|
3910
4018
|
import { immer as immer2 } from "zustand/middleware/immer";
|
|
@@ -3970,9 +4078,9 @@ var useServiceStore = create2()(immer2((set, get) => ({
|
|
|
3970
4078
|
...initialState2,
|
|
3971
4079
|
fetchWorkers: async () => {
|
|
3972
4080
|
const gen = ++fetchWorkersGeneration;
|
|
3973
|
-
|
|
4081
|
+
await Promise.resolve().then(() => init_api_client());
|
|
3974
4082
|
try {
|
|
3975
|
-
const data = await
|
|
4083
|
+
const data = await hooxFetch("/v1/workers");
|
|
3976
4084
|
if (gen !== fetchWorkersGeneration)
|
|
3977
4085
|
return;
|
|
3978
4086
|
clearReconnectTimer();
|
|
@@ -4033,11 +4141,11 @@ var useServiceStore = create2()(immer2((set, get) => ({
|
|
|
4033
4141
|
}
|
|
4034
4142
|
},
|
|
4035
4143
|
streamTrades: async () => {
|
|
4036
|
-
|
|
4144
|
+
await Promise.resolve().then(() => init_sse());
|
|
4037
4145
|
tradeStreamAbort?.();
|
|
4038
4146
|
tradeStreamAbort = null;
|
|
4039
4147
|
try {
|
|
4040
|
-
const sub =
|
|
4148
|
+
const sub = subscribeSSE("/v1/trades/stream", (trade) => {
|
|
4041
4149
|
get().pushTrade(trade);
|
|
4042
4150
|
});
|
|
4043
4151
|
const resolved = await Promise.resolve(sub);
|
|
@@ -4056,11 +4164,11 @@ var useServiceStore = create2()(immer2((set, get) => ({
|
|
|
4056
4164
|
}
|
|
4057
4165
|
},
|
|
4058
4166
|
streamLogs: async () => {
|
|
4059
|
-
|
|
4167
|
+
await Promise.resolve().then(() => init_sse());
|
|
4060
4168
|
logStreamAbort?.();
|
|
4061
4169
|
logStreamAbort = null;
|
|
4062
4170
|
try {
|
|
4063
|
-
const sub =
|
|
4171
|
+
const sub = subscribeSSE("/v1/logs/stream", (log) => {
|
|
4064
4172
|
get().pushLog(log);
|
|
4065
4173
|
});
|
|
4066
4174
|
const resolved = await Promise.resolve(sub);
|
|
@@ -4223,7 +4331,6 @@ scheduledReconnectTick = async () => {
|
|
|
4223
4331
|
await useServiceStore.getState().fetchWorkers();
|
|
4224
4332
|
}
|
|
4225
4333
|
};
|
|
4226
|
-
|
|
4227
4334
|
// src/stores/config-store.ts
|
|
4228
4335
|
import { create as create3 } from "zustand";
|
|
4229
4336
|
import { immer as immer3 } from "zustand/middleware/immer";
|
|
@@ -4372,177 +4479,183 @@ var useConfigStore = create3()(persist(immer3((set) => ({
|
|
|
4372
4479
|
...persisted
|
|
4373
4480
|
})
|
|
4374
4481
|
}));
|
|
4375
|
-
|
|
4376
|
-
// src/index.ts
|
|
4377
|
-
init_api_client();
|
|
4378
|
-
init_sse();
|
|
4379
|
-
init_operator_transport();
|
|
4380
4482
|
export {
|
|
4381
|
-
|
|
4382
|
-
writeConfigSync,
|
|
4383
|
-
writeConfig,
|
|
4384
|
-
validateWranglerJsonc,
|
|
4385
|
-
validateRootSecrets,
|
|
4386
|
-
validateDevVars,
|
|
4387
|
-
validateConfig,
|
|
4388
|
-
validateAll,
|
|
4389
|
-
useUIStore,
|
|
4390
|
-
useServiceStore,
|
|
4391
|
-
useConfigStore,
|
|
4392
|
-
trackAnalytics,
|
|
4393
|
-
toError,
|
|
4394
|
-
subscribeSSE,
|
|
4395
|
-
stripJsonc,
|
|
4396
|
-
setAgentConfigEmbeddedValue,
|
|
4397
|
-
serviceFetch,
|
|
4398
|
-
serializeState,
|
|
4399
|
-
serializeAgentConfigForKv,
|
|
4400
|
-
saveSession,
|
|
4401
|
-
restoreSession,
|
|
4402
|
-
resolveOperatorTransportProfile,
|
|
4403
|
-
resolveInternalAuthKey,
|
|
4404
|
-
resolveHooxRuntimeRoot,
|
|
4405
|
-
resolveHooxPath,
|
|
4406
|
-
resolveDependencies,
|
|
4407
|
-
rememberMonorepoRoot,
|
|
4408
|
-
readConfigSync,
|
|
4409
|
-
readConfig,
|
|
4410
|
-
parseDashboardManifest,
|
|
4411
|
-
parseAgentConfigJson,
|
|
4412
|
-
operatorUrl,
|
|
4413
|
-
logKvTimestamp,
|
|
4414
|
-
loadDashboardManifestsFromRoot,
|
|
4415
|
-
loadDashboardKvManifestFromRoot,
|
|
4416
|
-
kvPutMany,
|
|
4417
|
-
kvManifestFromDashboardManifests,
|
|
4418
|
-
kvGetManyAsRecord,
|
|
4419
|
-
kvGetMany,
|
|
4420
|
-
isWithinHooxHome,
|
|
4421
|
-
isUnixModeGroupOrWorldAccessible,
|
|
4422
|
-
isTruthyKillSwitchFlag,
|
|
4423
|
-
isTradingPaused,
|
|
4424
|
-
isHooxSetupRoot,
|
|
4425
|
-
isDashboardSectionFlatKv,
|
|
4426
|
-
isDashboardSectionEditable,
|
|
4427
|
-
isConfigWorldOrGroupReadable,
|
|
4428
|
-
isAgentConfigSection,
|
|
4429
|
-
isAgentConfigEmbeddedField,
|
|
4430
|
-
hooxFetch,
|
|
4431
|
-
healthCheck,
|
|
4432
|
-
headersToObject,
|
|
4433
|
-
hasOperatorClientCredentials,
|
|
4434
|
-
getWorkerDefaultEnabled,
|
|
4435
|
-
getTuiEntryCandidates,
|
|
4436
|
-
getRememberedMonorepoRoot,
|
|
4437
|
-
getRememberedMonorepoPath,
|
|
4438
|
-
getRelativeHooxPath,
|
|
4439
|
-
getHooxWranglerPath,
|
|
4440
|
-
getHooxStatePath,
|
|
4441
|
-
getHooxRepoPath,
|
|
4442
|
-
getHooxHome,
|
|
4443
|
-
getHooxDataDir,
|
|
4444
|
-
getHooxConfigDir,
|
|
4445
|
-
getAgentConfigEmbeddedValue,
|
|
4446
|
-
generateWranglerJsonc,
|
|
4447
|
-
generateDevVars,
|
|
4448
|
-
formatUptime,
|
|
4449
|
-
formatTimestamp,
|
|
4450
|
-
formatRequests,
|
|
4451
|
-
formatRelativeTime as formatRelativeTimeFromTime,
|
|
4452
|
-
formatRelativeTime2 as formatRelativeTime,
|
|
4453
|
-
formatPercent,
|
|
4454
|
-
formatNumber,
|
|
4455
|
-
formatMemory,
|
|
4456
|
-
formatLatency,
|
|
4457
|
-
formatDurationCompact,
|
|
4458
|
-
formatDuration2 as formatDuration,
|
|
4459
|
-
formatCurrency,
|
|
4460
|
-
formatCpu,
|
|
4461
|
-
formatConfigPermissionWarning,
|
|
4462
|
-
formatCompactCurrency,
|
|
4463
|
-
findHooxSetupRoot,
|
|
4464
|
-
expandAgentConfigToFieldMap,
|
|
4465
|
-
ensureHooxDirSecure,
|
|
4466
|
-
deserializeState,
|
|
4467
|
-
dashboardWorkerDir,
|
|
4468
|
-
createSuccessResponse,
|
|
4469
|
-
createQueueHandler,
|
|
4470
|
-
createMockVectorizeIndex,
|
|
4471
|
-
createMockSecretsStore,
|
|
4472
|
-
createMockR2,
|
|
4473
|
-
createMockQueue,
|
|
4474
|
-
createMockKV,
|
|
4475
|
-
createMockFetcher,
|
|
4476
|
-
createMockExecutionContext,
|
|
4477
|
-
createMockEnv,
|
|
4478
|
-
createMockD1,
|
|
4479
|
-
createMockAnalyticsEngineDataset,
|
|
4480
|
-
createMockAnalyticsEngine,
|
|
4481
|
-
createMockAi,
|
|
4482
|
-
createJsonResponse,
|
|
4483
|
-
createErrorResponse,
|
|
4484
|
-
createCronHandler,
|
|
4485
|
-
checkKillSwitch,
|
|
4486
|
-
buildOperatorAuthHeaders,
|
|
4487
|
-
buildDashboardKvKey,
|
|
4488
|
-
authenticatedServiceFetch,
|
|
4489
|
-
applyAgentConfigFieldUpdates,
|
|
4490
|
-
WorkerStatusColor,
|
|
4491
|
-
WorkerAPIError,
|
|
4492
|
-
WizardEngine,
|
|
4493
|
-
WebhookPayloadSchema,
|
|
4494
|
-
WORKER_NAMES,
|
|
4495
|
-
WORKER_MANIFESTS,
|
|
4496
|
-
WORKER_DEPENDENCIES,
|
|
4497
|
-
WORKER_CATALOG,
|
|
4498
|
-
WIZARD_STATE_PATH,
|
|
4499
|
-
WALLET_EXECUTE_AUTH_KEY_FIELDS,
|
|
4500
|
-
TradeSignalSchema,
|
|
4501
|
-
TradeQueueMessageSchema,
|
|
4502
|
-
TradeActionSchema,
|
|
4503
|
-
TRADE_READ_AUTH_KEY_FIELDS,
|
|
4504
|
-
TRADE_EXECUTE_AUTH_KEY_FIELDS,
|
|
4505
|
-
TRADEMARK_NOTICE,
|
|
4506
|
-
TRADEMARKS,
|
|
4507
|
-
TELEGRAM_ALERT_AUTH_KEY_FIELDS,
|
|
4508
|
-
ServiceAuthError,
|
|
4509
|
-
PositionSchema,
|
|
4510
|
-
PRESETS,
|
|
4511
|
-
LogLevelColor,
|
|
4512
|
-
KV_BULK_GET_MAX_KEYS,
|
|
4513
|
-
KVKeys,
|
|
4514
|
-
KILL_SWITCH_KEYS,
|
|
4515
|
-
KILL_SWITCH_ACTIVE_PREFIX,
|
|
4516
|
-
InternalAuthKeyFields,
|
|
4517
|
-
INTEGRATIONS,
|
|
4518
|
-
HOOX_DIR_MODE,
|
|
4519
|
-
HOOX_CONFIG_FILE_MODE,
|
|
4520
|
-
FULL_LEGAL_NOTICE,
|
|
4521
|
-
ExchangeRouter,
|
|
4522
|
-
Errors,
|
|
4523
|
-
DISCLAIMER_HEADER,
|
|
4524
|
-
DISCLAIMER,
|
|
4525
|
-
DASHBOARD_WORKER_PREFIX,
|
|
4526
|
-
DASHBOARD_WORKER_IDS,
|
|
4527
|
-
DASHBOARD_TRADE_EXECUTE_AUTH_KEY_FIELDS,
|
|
4528
|
-
DASHBOARD_TELEGRAM_ALERT_AUTH_KEY_FIELDS,
|
|
4529
|
-
DASHBOARD_SECTION_PREFIX,
|
|
4530
|
-
DASHBOARD_SECTIONS_UI_SKIP,
|
|
4531
|
-
DASHBOARD_SECTIONS_NOT_FLAT_KV,
|
|
4532
|
-
DASHBOARD_SECTIONS_AGENT_CONFIG,
|
|
4533
|
-
DASHBOARD_FIELD_KV_OVERRIDES,
|
|
4534
|
-
DASHBOARD_D1_READ_AUTH_KEY_FIELDS,
|
|
4535
|
-
D1_WRITE_AUTH_KEY_FIELDS,
|
|
4536
|
-
D1_READ_AUTH_KEY_FIELDS,
|
|
4537
|
-
D1Repository,
|
|
4538
|
-
CoolBracketPalette,
|
|
4539
|
-
ConnectionStatusColor,
|
|
4540
|
-
Colors,
|
|
4541
|
-
COPYRIGHT,
|
|
4542
|
-
CALLED_BY,
|
|
4543
|
-
BaseExchangeClient2 as BaseExchangeClient,
|
|
4544
|
-
BalanceSchema,
|
|
4545
|
-
AlertSeverityColor,
|
|
4483
|
+
AGENT_CONFIG_EMBEDDED_FIELDS,
|
|
4546
4484
|
AGENT_CONFIG_KV_KEY,
|
|
4547
|
-
|
|
4485
|
+
AlertSeverityColor,
|
|
4486
|
+
BalanceSchema,
|
|
4487
|
+
BaseExchangeClient2 as BaseExchangeClient,
|
|
4488
|
+
CALLED_BY,
|
|
4489
|
+
CLOUDFLARE_API_TOKEN_PLACEHOLDER,
|
|
4490
|
+
COPYRIGHT,
|
|
4491
|
+
Colors,
|
|
4492
|
+
ConnectionStatusColor,
|
|
4493
|
+
CoolBracketPalette,
|
|
4494
|
+
D1Repository,
|
|
4495
|
+
D1_READ_AUTH_KEY_FIELDS,
|
|
4496
|
+
D1_WRITE_AUTH_KEY_FIELDS,
|
|
4497
|
+
DASHBOARD_D1_READ_AUTH_KEY_FIELDS,
|
|
4498
|
+
DASHBOARD_FIELD_KV_OVERRIDES,
|
|
4499
|
+
DASHBOARD_SECTIONS_AGENT_CONFIG,
|
|
4500
|
+
DASHBOARD_SECTIONS_NOT_FLAT_KV,
|
|
4501
|
+
DASHBOARD_SECTIONS_UI_SKIP,
|
|
4502
|
+
DASHBOARD_SECTION_PREFIX,
|
|
4503
|
+
DASHBOARD_TELEGRAM_ALERT_AUTH_KEY_FIELDS,
|
|
4504
|
+
DASHBOARD_TRADE_EXECUTE_AUTH_KEY_FIELDS,
|
|
4505
|
+
DASHBOARD_WORKER_IDS,
|
|
4506
|
+
DASHBOARD_WORKER_PREFIX,
|
|
4507
|
+
DISCLAIMER,
|
|
4508
|
+
DISCLAIMER_HEADER,
|
|
4509
|
+
Errors,
|
|
4510
|
+
ExchangeRouter,
|
|
4511
|
+
FULL_LEGAL_NOTICE,
|
|
4512
|
+
HOOX_CONFIG_FILE_MODE,
|
|
4513
|
+
HOOX_DIR_MODE,
|
|
4514
|
+
INTEGRATIONS,
|
|
4515
|
+
InternalAuthKeyFields,
|
|
4516
|
+
KILL_SWITCH_ACTIVE_PREFIX,
|
|
4517
|
+
KILL_SWITCH_KEYS,
|
|
4518
|
+
KVKeys,
|
|
4519
|
+
KV_BULK_GET_MAX_KEYS,
|
|
4520
|
+
LogLevelColor,
|
|
4521
|
+
PRESETS,
|
|
4522
|
+
PositionSchema,
|
|
4523
|
+
ServiceAuthError,
|
|
4524
|
+
TELEGRAM_ALERT_AUTH_KEY_FIELDS,
|
|
4525
|
+
TRADEMARKS,
|
|
4526
|
+
TRADEMARK_NOTICE,
|
|
4527
|
+
TRADE_EXECUTE_AUTH_KEY_FIELDS,
|
|
4528
|
+
TRADE_READ_AUTH_KEY_FIELDS,
|
|
4529
|
+
TradeActionSchema,
|
|
4530
|
+
TradeQueueMessageSchema,
|
|
4531
|
+
TradeSignalSchema,
|
|
4532
|
+
WALLET_EXECUTE_AUTH_KEY_FIELDS,
|
|
4533
|
+
WIZARD_STATE_PATH,
|
|
4534
|
+
WORKER_CATALOG,
|
|
4535
|
+
WORKER_DEPENDENCIES,
|
|
4536
|
+
WORKER_MANIFESTS,
|
|
4537
|
+
WORKER_NAMES,
|
|
4538
|
+
WebhookPayloadSchema,
|
|
4539
|
+
WizardEngine,
|
|
4540
|
+
WorkerAPIError,
|
|
4541
|
+
WorkerStatusColor,
|
|
4542
|
+
applyAgentConfigFieldUpdates,
|
|
4543
|
+
authenticatedServiceFetch,
|
|
4544
|
+
buildDashboardKvKey,
|
|
4545
|
+
buildOperatorAuthHeaders,
|
|
4546
|
+
checkKillSwitch,
|
|
4547
|
+
createCronHandler,
|
|
4548
|
+
createErrorResponse,
|
|
4549
|
+
createJsonResponse,
|
|
4550
|
+
createMockAi,
|
|
4551
|
+
createMockAnalyticsEngine,
|
|
4552
|
+
createMockAnalyticsEngineDataset,
|
|
4553
|
+
createMockD1,
|
|
4554
|
+
createMockEnv,
|
|
4555
|
+
createMockExecutionContext,
|
|
4556
|
+
createMockFetcher,
|
|
4557
|
+
createMockKV,
|
|
4558
|
+
createMockQueue,
|
|
4559
|
+
createMockR2,
|
|
4560
|
+
createMockSecretsStore,
|
|
4561
|
+
createMockVectorizeIndex,
|
|
4562
|
+
createQueueHandler,
|
|
4563
|
+
createSuccessResponse,
|
|
4564
|
+
dashboardWorkerDir,
|
|
4565
|
+
deserializeState,
|
|
4566
|
+
effectiveTenantId,
|
|
4567
|
+
ensureHooxDirSecure,
|
|
4568
|
+
expandAgentConfigToFieldMap,
|
|
4569
|
+
findHooxSetupRoot,
|
|
4570
|
+
formatCompactCurrency,
|
|
4571
|
+
formatConfigPermissionWarning,
|
|
4572
|
+
formatCpu,
|
|
4573
|
+
formatCurrency,
|
|
4574
|
+
formatDuration,
|
|
4575
|
+
formatDurationCompact,
|
|
4576
|
+
formatLatency,
|
|
4577
|
+
formatMemory,
|
|
4578
|
+
formatNumber,
|
|
4579
|
+
formatPercent,
|
|
4580
|
+
formatRelativeTime,
|
|
4581
|
+
formatRelativeTime2 as formatRelativeTimeFromTime,
|
|
4582
|
+
formatRequests,
|
|
4583
|
+
formatTimestamp,
|
|
4584
|
+
formatUptime,
|
|
4585
|
+
generateDevVars,
|
|
4586
|
+
generateWranglerJsonc,
|
|
4587
|
+
getAgentConfigEmbeddedValue,
|
|
4588
|
+
getHooxConfigDir,
|
|
4589
|
+
getHooxDataDir,
|
|
4590
|
+
getHooxHome,
|
|
4591
|
+
getHooxRepoPath,
|
|
4592
|
+
getHooxStatePath,
|
|
4593
|
+
getHooxWranglerPath,
|
|
4594
|
+
getRelativeHooxPath,
|
|
4595
|
+
getRememberedMonorepoPath,
|
|
4596
|
+
getRememberedMonorepoRoot,
|
|
4597
|
+
getTuiEntryCandidates,
|
|
4598
|
+
getWorkerDefaultEnabled,
|
|
4599
|
+
hasOperatorClientCredentials,
|
|
4600
|
+
hasScope,
|
|
4601
|
+
headersToObject,
|
|
4602
|
+
healthCheck,
|
|
4603
|
+
hooxFetch,
|
|
4604
|
+
isAgentConfigEmbeddedField,
|
|
4605
|
+
isAgentConfigSection,
|
|
4606
|
+
isConfigWorldOrGroupReadable,
|
|
4607
|
+
isDashboardSectionEditable,
|
|
4608
|
+
isDashboardSectionFlatKv,
|
|
4609
|
+
isHooxSetupRoot,
|
|
4610
|
+
isTenantFlagOn,
|
|
4611
|
+
isTenantMode,
|
|
4612
|
+
isTradingPaused,
|
|
4613
|
+
isTruthyKillSwitchFlag,
|
|
4614
|
+
isUnixModeGroupOrWorldAccessible,
|
|
4615
|
+
isWithinHooxHome,
|
|
4616
|
+
kvGetMany,
|
|
4617
|
+
kvGetManyAsRecord,
|
|
4618
|
+
kvManifestFromDashboardManifests,
|
|
4619
|
+
kvPutMany,
|
|
4620
|
+
loadDashboardKvManifestFromRoot,
|
|
4621
|
+
loadDashboardManifestsFromRoot,
|
|
4622
|
+
logKvTimestamp,
|
|
4623
|
+
operatorUrl,
|
|
4624
|
+
parseAgentConfigJson,
|
|
4625
|
+
parseDashboardManifest,
|
|
4626
|
+
readConfig,
|
|
4627
|
+
readConfigSync,
|
|
4628
|
+
rememberMonorepoRoot,
|
|
4629
|
+
resolveDependencies,
|
|
4630
|
+
resolveHooxPath,
|
|
4631
|
+
resolveHooxRuntimeRoot,
|
|
4632
|
+
resolveInternalAuthKey,
|
|
4633
|
+
resolveOperatorTransportProfile,
|
|
4634
|
+
restoreSession,
|
|
4635
|
+
saveSession,
|
|
4636
|
+
scopeKey,
|
|
4637
|
+
scopeR2Key,
|
|
4638
|
+
serializeAgentConfigForKv,
|
|
4639
|
+
serializeState,
|
|
4640
|
+
serviceFetch,
|
|
4641
|
+
setAgentConfigEmbeddedValue,
|
|
4642
|
+
stripJsonc,
|
|
4643
|
+
subscribeSSE,
|
|
4644
|
+
tenantContextOf,
|
|
4645
|
+
tenantScope,
|
|
4646
|
+
toError,
|
|
4647
|
+
trackAnalytics,
|
|
4648
|
+
useConfigStore,
|
|
4649
|
+
useServiceStore,
|
|
4650
|
+
useUIStore,
|
|
4651
|
+
validateAll,
|
|
4652
|
+
validateConfig,
|
|
4653
|
+
validateDevVars,
|
|
4654
|
+
validateRootSecrets,
|
|
4655
|
+
validateWranglerJsonc,
|
|
4656
|
+
verifyCacheKey,
|
|
4657
|
+
withTenantId,
|
|
4658
|
+
writeConfig,
|
|
4659
|
+
writeConfigSync,
|
|
4660
|
+
writeSecureFileSync
|
|
4548
4661
|
};
|