@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.
Files changed (77) hide show
  1. package/README.md +6 -0
  2. package/dist/agent-config-fields.js +21 -29
  3. package/dist/analytics.d.ts +3 -4
  4. package/dist/analytics.d.ts.map +1 -1
  5. package/dist/analytics.js +56 -16
  6. package/dist/api-client.js +12 -21
  7. package/dist/colors.js +5 -21
  8. package/dist/config.js +11 -27
  9. package/dist/cron-handler.js +0 -16
  10. package/dist/d1/index.js +8 -25
  11. package/dist/d1/repository.js +3 -19
  12. package/dist/d1/schemas.js +4 -20
  13. package/dist/dashboard-manifest.d.ts +2 -1
  14. package/dist/dashboard-manifest.d.ts.map +1 -1
  15. package/dist/dashboard-manifest.js +37 -33
  16. package/dist/entitlements.d.ts +46 -0
  17. package/dist/entitlements.d.ts.map +1 -0
  18. package/dist/entitlements.js +15 -0
  19. package/dist/errors.js +7 -23
  20. package/dist/exchange-client.js +2 -18
  21. package/dist/exchanges/base-exchange-client.js +0 -16
  22. package/dist/exchanges/index.js +0 -16
  23. package/dist/format-time.js +2 -18
  24. package/dist/formatters.js +12 -28
  25. package/dist/health.js +3 -19
  26. package/dist/index.d.ts +5 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +2211 -2098
  29. package/dist/kill-switch.js +12 -20
  30. package/dist/kvKeys.d.ts +8 -0
  31. package/dist/kvKeys.d.ts.map +1 -1
  32. package/dist/kvKeys.js +42 -46
  33. package/dist/kvUtils.js +8 -24
  34. package/dist/legal.js +5 -21
  35. package/dist/middleware/auth.js +9 -25
  36. package/dist/middleware/cors.js +4 -20
  37. package/dist/middleware/index.js +141 -163
  38. package/dist/middleware/logger.js +9 -25
  39. package/dist/middleware/rate-limit.js +2 -18
  40. package/dist/middleware/security-headers.js +2 -18
  41. package/dist/middleware/validate.js +5 -21
  42. package/dist/middleware/wait-until.js +2 -18
  43. package/dist/operator-transport.js +13 -17
  44. package/dist/path-utils.js +15 -31
  45. package/dist/queue-handler.js +0 -16
  46. package/dist/router.js +0 -16
  47. package/dist/schemas/index.js +35 -27
  48. package/dist/schemas/registry.d.ts.map +1 -1
  49. package/dist/schemas/registry.js +29 -20
  50. package/dist/schemas/validators.js +5 -21
  51. package/dist/service-bindings.js +13 -29
  52. package/dist/session.js +2 -18
  53. package/dist/sse.js +10 -18
  54. package/dist/stores/config-store.js +8 -22
  55. package/dist/stores/service-store.js +16 -29
  56. package/dist/stores/ui-store.js +0 -16
  57. package/dist/tenant.d.ts +61 -0
  58. package/dist/tenant.d.ts.map +1 -0
  59. package/dist/tenant.js +56 -0
  60. package/dist/test-utils.js +11 -27
  61. package/dist/types.d.ts +4 -2
  62. package/dist/types.d.ts.map +1 -1
  63. package/dist/types.js +10 -24
  64. package/dist/wizard/engine.d.ts.map +1 -1
  65. package/dist/wizard/engine.js +21 -21
  66. package/dist/wizard/index.d.ts +1 -0
  67. package/dist/wizard/index.d.ts.map +1 -1
  68. package/dist/wizard/index.js +36 -33
  69. package/dist/wizard/persistence.d.ts +4 -0
  70. package/dist/wizard/persistence.d.ts.map +1 -1
  71. package/dist/wizard/persistence.js +7 -19
  72. package/dist/wizard/presets.d.ts.map +1 -1
  73. package/dist/wizard/presets.js +10 -22
  74. package/dist/wizard/types.d.ts +6 -0
  75. package/dist/wizard/types.d.ts.map +1 -1
  76. package/dist/wizard/types.js +5 -0
  77. package/package.json +7 -2
package/dist/index.js CHANGED
@@ -1,19 +1,15 @@
1
1
  // @bun
2
- var __defProp = Object.defineProperty;
3
- var __returnValue = (v) => v;
4
- function __exportSetter(name, newValue) {
5
- this[name] = __returnValue.bind(null, newValue);
6
- }
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, {
10
- get: all[name],
11
- enumerable: true,
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 (VALID_TRANSPORTS.has(v)) {
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", VALID_TRANSPORTS;
71
+ var DEFAULT_API_BASE = "http://localhost:8787", VALID_TRANSPORTS2;
76
72
  var init_operator_transport = __esm(() => {
77
- VALID_TRANSPORTS = new Set([
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
- // src/agent-config-fields.ts
326
- var AGENT_CONFIG_KV_KEY = KV_AGENT_CONFIG;
327
- var AGENT_CONFIG_EMBEDDED_FIELDS = {
328
- "providers:default_provider": { kind: "top", key: "defaultProvider" },
329
- "providers:fallback_chain": { kind: "top", key: "fallbackChain" },
330
- "providers:timeout_ms": { kind: "top", key: "timeoutMs" },
331
- "providers:retry_count": { kind: "top", key: "retryCount" },
332
- "models:workers_ai_model": { kind: "model", provider: "workers-ai" },
333
- "models:openai_model": { kind: "model", provider: "openai" },
334
- "models:anthropic_model": { kind: "model", provider: "anthropic" },
335
- "models:google_model": { kind: "model", provider: "google" },
336
- "risk:max_daily_drawdown_percent": {
337
- kind: "top",
338
- key: "maxDailyDrawdownPercent"
339
- },
340
- "risk:trailing_stop_percent": { kind: "top", key: "trailingStopPercent" },
341
- "risk:take_profit_percent": { kind: "top", key: "takeProfitPercent" }
342
- };
343
- var DASHBOARD_SECTIONS_AGENT_CONFIG = new Set([
344
- "providers",
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 isAgentConfigSection(sectionId) {
351
- return DASHBOARD_SECTIONS_AGENT_CONFIG.has(sectionId);
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 parseAgentConfigJson(raw) {
354
- if (raw === null || raw === undefined || raw === "")
355
- return {};
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
- let v = JSON.parse(raw);
358
- if (typeof v === "string") {
359
- try {
360
- v = JSON.parse(v);
361
- } catch {
362
- return {};
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
- if (v && typeof v === "object" && !Array.isArray(v)) {
366
- return { ...v };
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
- } catch {}
369
- return {};
479
+ return { enabled: false, error: errorStr };
480
+ }
370
481
  }
371
- function serializeAgentConfigForKv(config) {
372
- return JSON.stringify(config);
482
+ async function isTradingPaused(kv, options) {
483
+ const result = await checkKillSwitch(kv, options);
484
+ return result.enabled;
373
485
  }
374
- var NUMBER_TOP_KEYS = new Set([
375
- "timeoutMs",
376
- "retryCount",
377
- "maxDailyDrawdownPercent",
378
- "trailingStopPercent",
379
- "takeProfitPercent"
380
- ]);
381
- function getAgentConfigEmbeddedValue(config, fieldKey) {
382
- const spec = AGENT_CONFIG_EMBEDDED_FIELDS[fieldKey];
383
- if (!spec)
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
- const v = config[spec.key];
395
- if (spec.key === "fallbackChain") {
396
- if (Array.isArray(v))
397
- return JSON.stringify(v);
398
- if (typeof v === "string")
399
- return v;
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
- if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
403
- return v;
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 setAgentConfigEmbeddedValue(config, fieldKey, value) {
408
- const spec = AGENT_CONFIG_EMBEDDED_FIELDS[fieldKey];
409
- if (!spec)
410
- return { ...config };
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
- if (spec.key === "fallbackChain") {
418
- if (typeof value === "string") {
419
- try {
420
- next.fallbackChain = JSON.parse(value);
421
- } catch {
422
- next.fallbackChain = value;
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
- } else {
425
- next.fallbackChain = value;
550
+ return sanitized;
426
551
  }
427
- return next;
428
- }
429
- if (NUMBER_TOP_KEYS.has(spec.key)) {
430
- const n = typeof value === "number" ? value : Number(value);
431
- next[spec.key] = Number.isFinite(n) ? n : value;
432
- return next;
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
- next[spec.key] = value;
435
- return next;
564
+ return data;
436
565
  }
437
- function expandAgentConfigToFieldMap(config) {
438
- const out = {};
439
- for (const fieldKey of Object.keys(AGENT_CONFIG_EMBEDDED_FIELDS)) {
440
- const v = getAgentConfigEmbeddedValue(config, fieldKey);
441
- if (v !== undefined)
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 applyAgentConfigFieldUpdates(config, updates) {
447
- let next = { ...config };
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
- // src/analytics.ts
457
- async function trackAnalytics(env, endpoint, body, options) {
458
- if (!env.ANALYTICS_SERVICE)
459
- return;
460
- try {
461
- const payload = {
462
- ...body,
463
- ...options?.indexes ? { indexes: [...options.indexes] } : {}
464
- };
465
- const headers = {
466
- "Content-Type": "application/json"
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
- // src/colors.ts
483
- var Colors = {
484
- background: "#050508",
485
- foreground: "#E8E8F0",
486
- card: "#0A0A0F",
487
- border: "#232330",
488
- muted: "#8B8B9E",
489
- "muted-foreground": "#6E6E84",
490
- dim: "#3C3C50",
491
- accent: "#818CF8",
492
- "accent-dim": "#6366F1",
493
- success: "#34D399",
494
- warning: "#FBBF24",
495
- error: "#FB7185",
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
- soundEnabled: true,
569
- defaultView: "dashboard"
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
- function readConfigSync() {
572
- const envConfig = {
573
- apiUrl: process.env.HOOX_API_URL,
574
- apiToken: process.env.HOOX_API_TOKEN
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
- try {
596
- chmodSync(dir, HOOX_DIR_MODE);
597
- } catch {}
598
- }
599
- function writeSecureFileSync(filePath, contents, mode = HOOX_CONFIG_FILE_MODE) {
600
- writeFileSync(filePath, contents, { encoding: "utf-8", mode });
601
- try {
602
- chmodSync(filePath, mode);
603
- } catch {}
604
- }
605
- function isUnixModeGroupOrWorldAccessible(mode) {
606
- return (mode & 63) !== 0;
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
- function formatConfigPermissionWarning(filePath = CONFIG_PATH) {
618
- if (!isConfigWorldOrGroupReadable(filePath))
619
- return null;
620
- 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.`;
621
- }
622
- function writeConfigSync(config) {
623
- try {
624
- ensureHooxDirSecure(HOX_DIR);
625
- writeSecureFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), HOOX_CONFIG_FILE_MODE);
626
- } catch (err) {
627
- console.error("Failed to write config:", err);
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
- async function writeConfig(config) {
631
- writeConfigSync(config);
632
- }
633
- var VALID_TRANSPORTS2 = new Set([
634
- "public",
635
- "access",
636
- "mtls",
637
- "tunnel"
638
- ]);
639
- function validateConfig(config) {
640
- const errors = [];
641
- if (config.apiUrl && !config.apiUrl.startsWith("http")) {
642
- errors.push("apiUrl must start with http:// or https://");
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
- if (config.refreshIntervalMs !== undefined && config.refreshIntervalMs < 100) {
645
- errors.push("refreshIntervalMs must be >= 100ms");
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
- if (config.theme && !["dark", "light"].includes(config.theme)) {
648
- errors.push('theme must be "dark" or "light"');
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
- if (config.transport !== undefined && !VALID_TRANSPORTS2.has(config.transport)) {
651
- errors.push('transport must be "public" | "access" | "mtls" | "tunnel"');
682
+ async closeLong(symbol, quantity) {
683
+ return this.executeTrade({
684
+ symbol,
685
+ side: "long",
686
+ quantity,
687
+ reduceOnly: true
688
+ });
652
689
  }
653
- return errors;
654
- }
655
-
656
- // src/cron-handler.ts
657
- function createCronHandler(options) {
658
- const { name, handler, logger } = options;
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
- try {
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
- // src/errors.ts
686
- function toError(err, fallback = "Unknown error") {
687
- if (err instanceof Error)
688
- return err.message;
689
- if (typeof err === "string")
690
- return err;
691
- if (err && typeof err === "object" && "message" in err) {
692
- const message = err.message;
693
- if (typeof message === "string")
694
- return message;
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
- if (err === null || err === undefined)
697
- return fallback;
698
- try {
699
- return JSON.stringify(err) || fallback;
700
- } catch {
701
- return fallback;
716
+ get testnet() {
717
+ return this.isTestnet;
702
718
  }
703
- }
704
- var SENSITIVE_FIELDS = new Set([
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
- return false;
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
- return false;
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
- if (Array.isArray(data)) {
736
- return data.map(sanitizeOutput);
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
- if (t === "object") {
739
- const obj = data;
740
- const keys = Object.keys(obj);
741
- if (keys.length < 10) {
742
- const sanitized2 = {};
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
- return data;
764
- }
765
- function createJsonResponse(data, status = 200) {
766
- return new Response(JSON.stringify(sanitizeOutput(data)), {
767
- status,
768
- headers: { "Content-Type": "application/json" }
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 = z.object({
968
- id: z.string(),
969
- signal_id: z.string().nullable().optional(),
970
- timestamp: z.number().int().positive(),
971
- exchange: z.string().min(1),
972
- symbol: z.string().min(1),
973
- action: z.string().min(1),
974
- quantity: z.number().nullable(),
975
- price: z.number().nullable(),
976
- leverage: z.number().nullable(),
977
- status: z.string().min(1),
978
- error_message: z.string().nullable().optional(),
979
- raw_response: z.string().nullable().optional(),
980
- created_at: z.number().int().optional()
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 = z.object({
983
- id: z.string(),
984
- exchange: z.string().min(1),
985
- symbol: z.string().min(1),
986
- side: z.enum(["LONG", "SHORT"]),
987
- entry_price: z.number().nullable().optional(),
988
- mark_price: z.number().nullable().optional(),
989
- liquidation_price: z.number().nullable().optional(),
990
- leverage: z.number().nullable().optional(),
991
- size: z.number().nullable(),
992
- unrealized_pnl: z.number().nullable().optional(),
993
- status: z.enum(["OPEN", "CLOSED"]),
994
- updated_at: z.number().int()
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 = z.object({
997
- id: z.string(),
998
- exchange: z.string().min(1),
999
- asset: z.string().min(1),
1000
- free: z.number().nullable(),
1001
- used: z.number().nullable(),
1002
- total: z.number().nullable(),
1003
- timestamp: z.number().int().positive()
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 = z.object({
1006
- id: z.string(),
1007
- timestamp: z.number().int().optional(),
1008
- level: z.string().min(1),
1009
- service: z.string().min(1),
1010
- message: z.string().min(1),
1011
- details: z.string().nullable().optional()
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 = z.object({
1014
- signal_id: z.string(),
1015
- timestamp: z.number().int().positive(),
1016
- symbol: z.string().min(1),
1017
- signal_type: z.string().min(1),
1018
- source: z.string().nullable().optional(),
1019
- raw_data: z.string().nullable().optional(),
1020
- processed_at: z.number().int().optional()
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/dashboard-manifest.ts
1023
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
1024
- import { join as join2 } from "path";
1025
- var DASHBOARD_WORKER_PREFIX = {
1026
- hoox: "global:",
1027
- "trade-worker": "trade:",
1028
- "agent-worker": "agent:",
1029
- "telegram-worker": "bot:",
1030
- "d1-worker": "database:",
1031
- "email-worker": "email:",
1032
- "web3-wallet-worker": "wallet:",
1033
- "analytics-worker": "ai:",
1034
- "report-worker": "report:",
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 isDashboardSectionEditable(sectionId) {
1082
- return !DASHBOARD_SECTIONS_UI_SKIP.has(sectionId);
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 buildDashboardKvKey(worker, fieldKey) {
1085
- const override = DASHBOARD_FIELD_KV_OVERRIDES[fieldKey];
1086
- if (override)
1087
- return override;
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 workerPrefix = DASHBOARD_WORKER_PREFIX[worker] ?? "";
1102
- return `${workerPrefix}${fieldKey}`;
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 stripJsonc(input) {
1105
- let out = "";
1106
- let i = 0;
1107
- let inString = false;
1108
- let stringChar = "";
1109
- while (i < input.length) {
1110
- const c = input[i];
1111
- const next = input[i + 1];
1112
- if (inString) {
1113
- out += c;
1114
- if (c === "\\" && i + 1 < input.length) {
1115
- out += next;
1116
- i += 2;
1117
- continue;
1118
- }
1119
- if (c === stringChar)
1120
- inString = false;
1121
- i++;
1122
- continue;
1123
- }
1124
- if (c === '"' || c === "'") {
1125
- inString = true;
1126
- stringChar = c;
1127
- out += c;
1128
- i++;
1129
- continue;
1130
- }
1131
- if (c === "/" && next === "/") {
1132
- while (i < input.length && input[i] !== `
1133
- `)
1134
- i++;
1135
- continue;
1136
- }
1137
- if (c === "/" && next === "*") {
1138
- i += 2;
1139
- while (i < input.length && !(input[i] === "*" && input[i + 1] === "/"))
1140
- i++;
1141
- i += 2;
1142
- continue;
1143
- }
1144
- if (c === ",") {
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 out;
1294
+ return keys;
1157
1295
  }
1158
- function parseFieldType(value) {
1159
- if (typeof value === "boolean")
1160
- return "boolean";
1161
- if (typeof value === "number")
1162
- return "number";
1163
- if (value === "true" || value === "false")
1164
- return "boolean";
1165
- if (value.trim().startsWith("{") || value.trim().startsWith("["))
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 labelFromKey(key) {
1172
- return key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
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 parseDashboardManifest(content, workerName, onError) {
1175
- try {
1176
- const raw = JSON.parse(stripJsonc(content));
1177
- const displayName = raw.display_name || raw.displayName || workerName;
1178
- const description = raw.description || "";
1179
- const sections = [];
1180
- if (raw.sections) {
1181
- for (const [sectionId, sectionData] of Object.entries(raw.sections)) {
1182
- const fields = [];
1183
- const sectionFields = sectionData.fields || {};
1184
- const sectionOptions = sectionData.options || {};
1185
- const sectionDescriptions = sectionData.descriptions || {};
1186
- const sectionValidation = sectionData.validation || {};
1187
- const sectionSecrets = sectionData.secrets || {};
1188
- const sectionSecretCommands = sectionData.secret_commands || {};
1189
- for (const [key, value] of Object.entries(sectionFields)) {
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
- worker: workerName,
1231
- displayName,
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
- var DASHBOARD_WORKER_IDS = [
1247
- "hoox",
1248
- "trade-worker",
1249
- "agent-worker",
1250
- "telegram-worker",
1251
- "d1-worker",
1252
- "email-worker",
1253
- "web3-wallet-worker",
1254
- "analytics-worker",
1255
- "report-worker",
1256
- "pyne-worker"
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
- function dashboardWorkerDir(workerId) {
1259
- if (workerId === "hoox")
1260
- return "hoox-worker";
1261
- return workerId;
1262
- }
1263
- function fieldTypeToKv(t) {
1264
- if (t === "boolean")
1265
- return "boolean";
1266
- if (t === "number")
1267
- return "number";
1268
- return "string";
1269
- }
1270
- function defaultToString(value) {
1271
- if (typeof value === "string")
1272
- return value;
1273
- return JSON.stringify(value);
1274
- }
1275
- function isDocumentationField(fieldKey) {
1276
- const name = fieldKey.includes(":") ? fieldKey.slice(fieldKey.indexOf(":") + 1) : fieldKey;
1277
- return /^(GET|POST|PUT|PATCH|DELETE)\s+\//i.test(name);
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
- function kvManifestFromDashboardManifests(manifests) {
1280
- const byKey = new Map;
1281
- for (const manifest of manifests) {
1282
- for (const section of manifest.sections) {
1283
- if (!isDashboardSectionFlatKv(section.id))
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 tryReadFile(path) {
1319
- try {
1320
- if (!existsSync2(path))
1321
- return null;
1322
- return readFileSync2(path, "utf-8");
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
- function loadDashboardManifestsFromRoot(root, onError) {
1328
- const manifests = [];
1329
- const seen = new Set;
1330
- const workersDir = join2(root, "workers");
1331
- if (!existsSync2(workersDir))
1332
- return manifests;
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
- class BaseExchangeClient {
1413
- apiKey;
1414
- apiSecret;
1415
- baseUrl;
1416
- isTestnet;
1417
- constructor(config) {
1418
- if (!config.apiKey || !config.apiSecret) {
1419
- throw new Error("API key and secret are required.");
1420
- }
1421
- this.apiKey = config.apiKey;
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
- // src/exchanges/base-exchange-client.ts
1478
- class BaseExchangeClient2 {
1479
- apiKey;
1480
- apiSecret;
1481
- baseUrl;
1482
- isTestnet;
1483
- importedKeyPromise;
1484
- constructor(apiKey, apiSecret, options) {
1485
- if (!apiKey || !apiSecret) {
1486
- throw new Error(this.getErrorMessagePrefix() + "API key and secret are required.");
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
- // src/format-time.ts
1581
- function formatRelativeTime(timestampMs, nowMs) {
1582
- const now = nowMs ?? Date.now();
1583
- const diffMs = now - timestampMs;
1584
- if (diffMs < 0)
1585
- return "just now";
1586
- if (timestampMs === 0)
1587
- return "\u2014";
1588
- const seconds = Math.floor(diffMs / 1000);
1589
- const minutes = Math.floor(seconds / 60);
1590
- const hours = Math.floor(minutes / 60);
1591
- const days = Math.floor(hours / 24);
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 formatDuration(durationMs) {
1603
- const seconds = Math.floor(durationMs / 1000);
1604
- const minutes = Math.floor(seconds / 60);
1605
- const hours = Math.floor(minutes / 60);
1606
- if (hours > 0) {
1607
- const remainMin = minutes % 60;
1608
- return remainMin > 0 ? `${hours}h ${remainMin}m` : `${hours}h`;
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 formatDuration2(seconds) {
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 formatRelativeTime2(timestamp) {
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 formatDuration2(seconds);
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
- // src/health.ts
1742
- function healthCheck(options) {
1743
- const result = {
1744
- status: "ok",
1745
- timestamp: new Date().toISOString()
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
- // src/legal.ts
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
- async function checkKillSwitch(kv, options) {
1847
- const onMissingKv = options?.onMissingKv ?? "open";
1848
- const onReadError = options?.onReadError ?? "closed";
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
- const values = await Promise.all(KILL_SWITCH_KEYS.map(async (key) => ({
1857
- key,
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
- // src/kvUtils.ts
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
- await env.REPORT_KV.put(key, timestamp);
1885
- } catch (error) {
1886
- console.error(`Failed to log timestamp to KV: ${toError(error, "Unknown error")}`);
1887
- }
1781
+ chmodSync(filePath, mode);
1782
+ } catch {}
1888
1783
  }
1889
- var KV_BULK_GET_MAX_KEYS = 100;
1890
- async function tryBulkGetChunk(kv, chunk) {
1784
+ function isUnixModeGroupOrWorldAccessible(mode) {
1785
+ return (mode & 63) !== 0;
1786
+ }
1787
+ function isConfigWorldOrGroupReadable(filePath = CONFIG_PATH) {
1891
1788
  try {
1892
- const result = await kv.get([...chunk]);
1893
- if (!(result instanceof Map)) {
1894
- return null;
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 null;
1793
+ return false;
1899
1794
  }
1900
1795
  }
1901
- async function kvGetMany(kv, keys) {
1902
- if (keys.length === 0)
1903
- return [];
1904
- const out = new Array(keys.length);
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
- async function kvGetManyAsRecord(kv, keys) {
1924
- const values = await kvGetMany(kv, keys);
1925
- const out = {};
1926
- for (let i = 0;i < keys.length; i++) {
1927
- out[keys[i]] = values[i] ?? null;
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 kvPutMany(kv, entries) {
1932
- if (entries.length === 0)
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
- function headersToObject(headers) {
1937
- const result = {};
1938
- if (!headers)
1939
- return result;
1940
- try {
1941
- headers.forEach((value, key) => {
1942
- result[key] = value;
1943
- });
1944
- } catch (error) {
1945
- console.error(`Error converting headers to object: ${toError(error, "Unknown error")}`);
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
- return result;
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/queue-handler.ts
1951
- async function mapPool(items, concurrency, fn) {
1952
- if (items.length === 0)
1953
- return;
1954
- const limit = Math.min(Math.max(1, concurrency), items.length);
1955
- if (limit === 1) {
1956
- for (const item of items) {
1957
- await fn(item);
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
- return;
1960
- }
1961
- let nextIndex = 0;
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 createQueueHandler(options) {
1973
- const {
1974
- maxRetries,
1975
- backoffDelays,
1976
- onMessage,
1977
- onRetry,
1978
- onDLQ,
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
- // src/middleware/auth.ts
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
- async function timingSafeEqualAsync(a, b) {
2023
- const subtle = globalThis.crypto?.subtle;
2024
- if (!subtle || typeof subtle.digest !== "function" || typeof subtle.timingSafeEqual !== "function") {
2025
- return timingSafeEqual(a, b);
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 resolveOperatorApiKey(env) {
2035
- const preferred = env.OPERATOR_API_KEY;
2036
- if (typeof preferred === "string" && preferred.length > 0) {
2037
- return preferred;
2038
- }
2039
- const legacy = env.INTERNAL_API_KEY;
2040
- if (typeof legacy === "string" && legacy.length > 0) {
2041
- return legacy;
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
- async function requireAuth(request, env) {
2046
- return requireOperatorAuth(request, env);
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
- async function requireOperatorAuth(request, env) {
2049
- const apiKey = resolveOperatorApiKey(env);
2050
- if (!apiKey) {
2051
- return new Response(JSON.stringify({
2052
- error: "Operator API key not configured",
2053
- hint: "Set OPERATOR_API_KEY (preferred) or INTERNAL_API_KEY on the Worker"
2054
- }), { status: 401, headers: { "Content-Type": "application/json" } });
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 authHeader = request.headers.get("Authorization");
2057
- const expectedHeader = `Bearer ${apiKey}`;
2058
- if (!authHeader || !await timingSafeEqualAsync(authHeader, expectedHeader)) {
2059
- return new Response(JSON.stringify({ error: "Unauthorized" }), {
2060
- status: 401,
2061
- headers: { "Content-Type": "application/json" }
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
- return null;
2065
- }
2066
- function createOperatorAuthMiddleware() {
2067
- return async (request, env, _ctx) => {
2068
- const denied = await requireOperatorAuth(request, env);
2069
- if (denied)
2070
- return denied;
2071
- return;
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 normalizeKeyNames(keyName) {
2075
- if (typeof keyName === "string") {
2076
- return [keyName];
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
- return [...keyName];
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 collectInternalAuthKeys(env, keyNames) {
2081
- const keys = [];
2082
- for (const name of normalizeKeyNames(keyNames)) {
2083
- const value = env[name];
2084
- if (typeof value === "string" && value.length > 0) {
2085
- keys.push(value);
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 matchesAnyInternalAuthKey(providedKey, expectedKeys) {
2091
- let matched = false;
2092
- for (const expected of expectedKeys) {
2093
- if (timingSafeEqual(providedKey, expected)) {
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 internalAuthNotConfiguredResponse(keyNames) {
2100
- const label = normalizeKeyNames(keyNames).join(" | ");
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 requireInternalAuth(request, env, keyName = "INTERNAL_KEY_BINDING") {
2107
- const expectedKeys = collectInternalAuthKeys(env, keyName);
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 createInternalAuthMiddleware(keyName = "INTERNAL_KEY_BINDING") {
2118
- return async (request, env, _ctx) => {
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 checkInternalAuth(request, env, keyName = "INTERNAL_KEY_BINDING") {
2131
- const expectedKeys = collectInternalAuthKeys(env, keyName);
2132
- if (expectedKeys.length === 0) {
2133
- return {
2134
- authorized: false,
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/service-bindings.ts
2146
- var InternalAuthKeyFields = {
2147
- DEFAULT: "INTERNAL_KEY_BINDING",
2148
- D1_READ: "D1_READ_KEY_BINDING",
2149
- D1_WRITE: "D1_WRITE_KEY_BINDING",
2150
- TRADE_EXECUTE: "TRADE_EXECUTE_KEY_BINDING",
2151
- TRADE_READ: "TRADE_READ_KEY_BINDING",
2152
- TELEGRAM_ALERT: "TELEGRAM_INTERNAL_KEY_BINDING",
2153
- WALLET_EXECUTE: "WALLET_EXECUTE_KEY_BINDING",
2154
- AGENT: "AGENT_INTERNAL_KEY",
2155
- D1_INTERNAL_ALIAS: "D1_INTERNAL_KEY",
2156
- TRADE_INTERNAL_ALIAS: "TRADE_INTERNAL_KEY",
2157
- TELEGRAM_INTERNAL_ALIAS: "TELEGRAM_INTERNAL_KEY"
2158
- };
2159
- var D1_READ_AUTH_KEY_FIELDS = [
2160
- InternalAuthKeyFields.D1_READ,
2161
- InternalAuthKeyFields.DEFAULT
2162
- ];
2163
- var D1_WRITE_AUTH_KEY_FIELDS = [
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
- function authenticatedServiceFetch(binding, env, path, body, options) {
2210
- const key = options?.internalKey ?? (options?.internalKeyFields ? resolveInternalAuthKey(env, options.internalKeyFields) : env.INTERNAL_KEY_BINDING);
2211
- if (!key) {
2212
- const label = options?.internalKeyFields ? Array.isArray(options.internalKeyFields) ? options.internalKeyFields.join(" | ") : options.internalKeyFields : "INTERNAL_KEY_BINDING";
2213
- return Promise.reject(new ServiceAuthError(`${label} not configured`));
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 createMockPreparedStatement() {
2263
- return {
2264
- bind: (..._params) => createMockPreparedStatement(),
2265
- run: async () => ({ success: true }),
2266
- all: async () => ({ success: true, results: [] }),
2267
- first: async () => null
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
- function createMockD1(overrides) {
2271
- return {
2272
- prepare: (_sql) => createMockPreparedStatement(),
2273
- dump: async () => new ArrayBuffer(0),
2274
- batch: async (_statements) => [],
2275
- exec: async (_sql) => ({ success: true }),
2276
- ...overrides
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
- function createMockFetcher(response) {
2280
- const defaultResponse = response ?? new Response("OK", { status: 200 });
2281
- return {
2282
- fetch: async (_request, _init) => defaultResponse
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 createMockAnalyticsEngine() {
2286
- return {
2287
- writeDataPoint: (_data) => {}
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
- function createMockAnalyticsEngineDataset() {
2291
- return createMockAnalyticsEngine();
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 createMockQueue() {
2294
- return {
2295
- send: async (_body, _options) => {},
2296
- sendBatch: async (_messages) => {}
2297
- };
2186
+ function verifyCacheKey(keyHashPrefix) {
2187
+ const clean = (keyHashPrefix ?? "").trim();
2188
+ return `hx:${clean}`;
2298
2189
  }
2299
- function createMockVectorizeIndex(overrides) {
2300
- return {
2301
- query: async (_vector, _options) => ({
2302
- matches: []
2303
- }),
2304
- upsert: async (_vectors) => {},
2305
- deleteByIds: async (_ids) => {},
2306
- describe: async () => ({ dimension: 384, totalVectors: 0 }),
2307
- ...overrides
2308
- };
2309
- }
2310
- function createMockAi(overrides) {
2311
- return {
2312
- run: async (_model, _input) => ({}),
2313
- ...overrides
2314
- };
2315
- }
2316
- function createMockExecutionContext(overrides) {
2317
- return {
2318
- waitUntil: (_promise) => {},
2319
- passThroughOnException: () => {},
2320
- ...overrides
2321
- };
2322
- }
2323
- function createMockSecretsStore(overrides) {
2324
- return {
2325
- get: async (_key) => null,
2326
- put: async (_key, _value) => {},
2327
- delete: async (_key) => {},
2328
- ...overrides
2329
- };
2330
- }
2331
- function createMockEnv(overrides) {
2332
- return overrides;
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
- // src/path-utils.ts
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 getHooxHome() {
2349
- try {
2350
- const override = process.env.HOOX_HOME?.trim();
2351
- if (override) {
2352
- return createHooxPath(resolve(override));
2353
- }
2354
- const home = homedir2();
2355
- if (!home || home.length === 0) {
2356
- return createHooxPath(resolve(process.cwd(), ".hoox"));
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
- return createHooxPath(join3(home, ".hoox"));
2359
- } catch {
2360
- return createHooxPath(resolve(process.cwd(), ".hoox"));
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 isHooxSetupRoot(dir) {
2364
- if (!dir)
2365
- return false;
2366
- try {
2367
- const root = resolve(dir);
2368
- if (!existsSync3(join3(root, "packages", "cli", "package.json"))) {
2369
- return false;
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
- return existsSync3(join3(root, "wrangler.jsonc")) || existsSync3(join3(root, "wrangler.jsonc.example")) || existsSync3(join3(root, "workers")) || existsSync3(join3(root, ".gitmodules"));
2372
- } catch {
2373
- return false;
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 getRememberedMonorepoPath() {
2377
- return resolveHooxPath("config/monorepo.json");
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 getRememberedMonorepoRoot() {
2380
- try {
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 rememberMonorepoRoot(root) {
2362
+ function parseDashboardManifest(content, workerName, onError) {
2395
2363
  try {
2396
- const resolved = resolve(root);
2397
- if (!isHooxSetupRoot(resolved))
2398
- return false;
2399
- const configDir = getHooxConfigDir();
2400
- mkdirSync2(configDir, { recursive: true, mode: 448 });
2401
- try {
2402
- chmodSync2(configDir, 448);
2403
- } catch {}
2404
- const payload = {
2405
- root: resolved,
2406
- updatedAt: new Date().toISOString()
2407
- };
2408
- const path = getRememberedMonorepoPath();
2409
- writeFileSync2(path, JSON.stringify(payload, null, 2) + `
2410
- `, {
2411
- mode: 384
2412
- });
2413
- try {
2414
- chmodSync2(path, 384);
2415
- } catch {}
2416
- return true;
2417
- } catch {
2418
- return false;
2419
- }
2420
- }
2421
- function findHooxSetupRoot(startDir = process.cwd()) {
2422
- let dir = resolve(startDir);
2423
- for (;; ) {
2424
- if (isHooxSetupRoot(dir))
2425
- return dir;
2426
- const parent = dirname(dir);
2427
- if (parent === dir)
2428
- return null;
2429
- dir = parent;
2430
- }
2431
- }
2432
- function resolveHooxRuntimeRoot(options) {
2433
- const env = options?.env ?? process.env;
2434
- const cwd = resolve(options?.cwd ?? process.cwd());
2435
- const globalRepo = getHooxRepoPath();
2436
- const remembered = options?.skipRemembered ? null : getRememberedMonorepoRoot();
2437
- const envRepo = env.HOOX_REPO?.trim();
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
- return {
2453
- root: null,
2454
- source: "env",
2455
- checked: {
2456
- env: resolved,
2457
- cwd: findHooxSetupRoot(cwd),
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
- root: remembered,
2474
- source: "remembered",
2475
- checked: { cwd: null, remembered, global: globalRepo }
2418
+ worker: workerName,
2419
+ displayName,
2420
+ description,
2421
+ sections: sections.sort((a, b) => a.priority - b.priority)
2476
2422
  };
2477
- }
2478
- if (isHooxSetupRoot(globalRepo)) {
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
- root: globalRepo,
2481
- source: "global",
2482
- checked: { cwd: null, remembered: null, global: globalRepo }
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
- function getTuiEntryCandidates(runtimeRoot) {
2492
- const root = resolve(runtimeRoot);
2493
- return [
2494
- join3(root, "packages", "tui", "src", "main.tsx"),
2495
- join3(root, "packages", "tui", "dist", "main.js"),
2496
- join3(root, "packages", "tui", "src", "main.ts")
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 resolveHooxPath(relativePath) {
2500
- if (!relativePath || typeof relativePath !== "string") {
2501
- throw new Error("relativePath must be a non-empty string");
2502
- }
2503
- if (relativePath.includes("..")) {
2504
- throw new Error(`Path traversal detected in relativePath: "${relativePath}"`);
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 isWithinHooxHome(path) {
2517
- try {
2518
- const hooxHome = getHooxHome();
2519
- const resolvedPath = resolve(path);
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 getRelativeHooxPath(absolutePath) {
2527
- try {
2528
- if (!isWithinHooxHome(absolutePath)) {
2529
- return null;
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 getHooxRepoPath() {
2541
- return resolveHooxPath("repo");
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
- const hooxHome = getHooxHome();
2572
- return `${hooxHome}/${TUI_STATE_DIR}/session.json`;
2508
+ if (!existsSync3(path))
2509
+ return null;
2510
+ return readFileSync3(path, "utf-8");
2573
2511
  } catch {
2574
- return `${process.cwd()}/${TUI_STATE_DIR}/session.json`;
2512
+ return null;
2575
2513
  }
2576
2514
  }
2577
- var DEFAULT_SESSION = {
2578
- activeView: "dashboard",
2579
- sidebarExpanded: true,
2580
- windowSize: { cols: 80, rows: 24 },
2581
- lastData: 0,
2582
- savedAt: new Date().toISOString()
2583
- };
2584
- async function saveSession(activeView, sidebarExpanded, windowSize, lastData) {
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 session = {
2587
- activeView,
2588
- sidebarExpanded,
2589
- windowSize,
2590
- lastData,
2591
- savedAt: new Date().toISOString()
2592
- };
2593
- await Bun.write(getSessionFilePath(), JSON.stringify(session, null, 2));
2594
- } catch (error) {
2595
- console.error("[session] Failed to save session:", error.message);
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
- async function restoreSession() {
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
- const file = Bun.file(getSessionFilePath());
2601
- const exists = await file.exists();
2602
- if (!exists)
2603
- return { ...DEFAULT_SESSION };
2604
- const raw = await file.text();
2605
- const result = SessionStateFileSchema.safeParse(JSON.parse(raw));
2606
- const parsed = result.success ? result.data : {};
2607
- return {
2608
- activeView: validateViewId(parsed.activeView) ? parsed.activeView : DEFAULT_SESSION.activeView,
2609
- sidebarExpanded: typeof parsed.sidebarExpanded === "boolean" ? parsed.sidebarExpanded : DEFAULT_SESSION.sidebarExpanded,
2610
- windowSize: parsed.windowSize ?? DEFAULT_SESSION.windowSize,
2611
- lastData: typeof parsed.lastData === "number" ? parsed.lastData : DEFAULT_SESSION.lastData,
2612
- savedAt: parsed.savedAt ?? DEFAULT_SESSION.savedAt
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
- var VALID_VIEW_IDS = new Set([
2619
- "dashboard",
2620
- "workers",
2621
- "worker-detail",
2622
- "trade-monitor",
2623
- "logs-viewer",
2624
- "service-manager",
2625
- "config-editor",
2626
- "setup-wizard",
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 validateViewId(id) {
2637
- return typeof id === "string" && VALID_VIEW_IDS.has(id);
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: "hoox-db" }
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 Object.keys(integration.secrets)) {
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: cf?.apiToken ?? "",
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((i2) => i2.key === k);
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("hoox-db");
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
- return JSON.stringify(state, null, 2);
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
- const { hooxFetch: hooxFetch2 } = await Promise.resolve().then(() => (init_api_client(), exports_api_client));
4081
+ await Promise.resolve().then(() => init_api_client());
3974
4082
  try {
3975
- const data = await hooxFetch2("/v1/workers");
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
- const { subscribeSSE: subscribeSSE2 } = await Promise.resolve().then(() => (init_sse(), exports_sse));
4144
+ await Promise.resolve().then(() => init_sse());
4037
4145
  tradeStreamAbort?.();
4038
4146
  tradeStreamAbort = null;
4039
4147
  try {
4040
- const sub = subscribeSSE2("/v1/trades/stream", (trade) => {
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
- const { subscribeSSE: subscribeSSE2 } = await Promise.resolve().then(() => (init_sse(), exports_sse));
4167
+ await Promise.resolve().then(() => init_sse());
4060
4168
  logStreamAbort?.();
4061
4169
  logStreamAbort = null;
4062
4170
  try {
4063
- const sub = subscribeSSE2("/v1/logs/stream", (log) => {
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
- writeSecureFileSync,
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
- AGENT_CONFIG_EMBEDDED_FIELDS
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
  };