@jango-blockchained/hoox-shared 1.0.8 → 1.1.0

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