@hoox-sh/hoox-shared 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist/agent-config-fields.d.ts +32 -0
- package/dist/agent-config-fields.d.ts.map +1 -0
- package/dist/agent-config-fields.js +220 -0
- package/dist/analytics.d.ts +47 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +45 -0
- package/dist/api-client.d.ts +48 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +191 -0
- package/dist/colors.d.ts +76 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/colors.js +82 -0
- package/dist/config.d.ts +61 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +146 -0
- package/dist/cron-handler.d.ts +38 -0
- package/dist/cron-handler.d.ts.map +1 -0
- package/dist/cron-handler.js +48 -0
- package/dist/d1/index.d.ts +9 -0
- package/dist/d1/index.d.ts.map +1 -0
- package/dist/d1/index.js +360 -0
- package/dist/d1/repository.d.ts +86 -0
- package/dist/d1/repository.d.ts.map +1 -0
- package/dist/d1/repository.js +297 -0
- package/dist/d1/schemas.d.ts +89 -0
- package/dist/d1/schemas.d.ts.map +1 -0
- package/dist/d1/schemas.js +81 -0
- package/dist/dashboard-manifest.d.ts +135 -0
- package/dist/dashboard-manifest.d.ts.map +1 -0
- package/dist/dashboard-manifest.js +401 -0
- package/dist/errors.d.ts +44 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +149 -0
- package/dist/exchange-client.d.ts +98 -0
- package/dist/exchange-client.d.ts.map +1 -0
- package/dist/exchange-client.js +107 -0
- package/dist/exchanges/base-exchange-client.d.ts +81 -0
- package/dist/exchanges/base-exchange-client.d.ts.map +1 -0
- package/dist/exchanges/base-exchange-client.js +123 -0
- package/dist/exchanges/index.d.ts +7 -0
- package/dist/exchanges/index.d.ts.map +1 -0
- package/dist/exchanges/index.js +123 -0
- package/dist/exchanges/types.d.ts +23 -0
- package/dist/exchanges/types.d.ts.map +1 -0
- package/dist/exchanges/types.js +1 -0
- package/dist/format-time.d.ts +28 -0
- package/dist/format-time.d.ts.map +1 -0
- package/dist/format-time.js +57 -0
- package/dist/formatters.d.ts +36 -0
- package/dist/formatters.d.ts.map +1 -0
- package/dist/formatters.js +155 -0
- package/dist/health.d.ts +19 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +160 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4198 -0
- package/dist/kvKeys.d.ts +76 -0
- package/dist/kvKeys.d.ts.map +1 -0
- package/dist/kvKeys.js +107 -0
- package/dist/kvUtils.d.ts +30 -0
- package/dist/kvUtils.d.ts.map +1 -0
- package/dist/kvUtils.js +170 -0
- package/dist/legal.d.ts +15 -0
- package/dist/legal.d.ts.map +1 -0
- package/dist/legal.js +40 -0
- package/dist/middleware/auth.d.ts +102 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +151 -0
- package/dist/middleware/cors.d.ts +58 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +77 -0
- package/dist/middleware/index.d.ts +14 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +635 -0
- package/dist/middleware/logger.d.ts +17 -0
- package/dist/middleware/logger.d.ts.map +1 -0
- package/dist/middleware/logger.js +202 -0
- package/dist/middleware/rate-limit.d.ts +30 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +181 -0
- package/dist/middleware/security-headers.d.ts +43 -0
- package/dist/middleware/security-headers.d.ts.map +1 -0
- package/dist/middleware/security-headers.js +58 -0
- package/dist/middleware/validate.d.ts +25 -0
- package/dist/middleware/validate.d.ts.map +1 -0
- package/dist/middleware/validate.js +56 -0
- package/dist/operator-transport.d.ts +65 -0
- package/dist/operator-transport.d.ts.map +1 -0
- package/dist/operator-transport.js +87 -0
- package/dist/path-utils.d.ts +190 -0
- package/dist/path-utils.d.ts.map +1 -0
- package/dist/path-utils.js +188 -0
- package/dist/queue-handler.d.ts +47 -0
- package/dist/queue-handler.d.ts.map +1 -0
- package/dist/queue-handler.js +46 -0
- package/dist/router.d.ts +15 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +130 -0
- package/dist/schemas/index.d.ts +8 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +705 -0
- package/dist/schemas/registry.d.ts +12 -0
- package/dist/schemas/registry.d.ts.map +1 -0
- package/dist/schemas/registry.js +433 -0
- package/dist/schemas/types.d.ts +83 -0
- package/dist/schemas/types.d.ts.map +1 -0
- package/dist/schemas/types.js +1 -0
- package/dist/schemas/validators.d.ts +39 -0
- package/dist/schemas/validators.d.ts.map +1 -0
- package/dist/schemas/validators.js +290 -0
- package/dist/service-bindings.d.ts +78 -0
- package/dist/service-bindings.d.ts.map +1 -0
- package/dist/service-bindings.js +248 -0
- package/dist/session.d.ts +39 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +261 -0
- package/dist/sse.d.ts +39 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +154 -0
- package/dist/stores/config-store.d.ts +91 -0
- package/dist/stores/config-store.d.ts.map +1 -0
- package/dist/stores/config-store.js +255 -0
- package/dist/stores/service-store.d.ts +266 -0
- package/dist/stores/service-store.d.ts.map +1 -0
- package/dist/stores/service-store.js +516 -0
- package/dist/stores/ui-store.d.ts +67 -0
- package/dist/stores/ui-store.d.ts.map +1 -0
- package/dist/stores/ui-store.js +62 -0
- package/dist/test-utils.d.ts +136 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +128 -0
- package/dist/types/errors.d.ts +20 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +1 -0
- package/dist/types/router.d.ts +36 -0
- package/dist/types/router.d.ts.map +1 -0
- package/dist/types/router.js +1 -0
- package/dist/types.d.ts +442 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +83 -0
- package/dist/wizard/engine.d.ts +46 -0
- package/dist/wizard/engine.d.ts.map +1 -0
- package/dist/wizard/engine.js +525 -0
- package/dist/wizard/index.d.ts +11 -0
- package/dist/wizard/index.d.ts.map +1 -0
- package/dist/wizard/index.js +544 -0
- package/dist/wizard/persistence.d.ts +18 -0
- package/dist/wizard/persistence.d.ts.map +1 -0
- package/dist/wizard/persistence.js +31 -0
- package/dist/wizard/presets.d.ts +38 -0
- package/dist/wizard/presets.d.ts.map +1 -0
- package/dist/wizard/presets.js +223 -0
- package/dist/wizard/provisioner.d.ts +15 -0
- package/dist/wizard/provisioner.d.ts.map +1 -0
- package/dist/wizard/provisioner.js +1 -0
- package/dist/wizard/types.d.ts +76 -0
- package/dist/wizard/types.d.ts.map +1 -0
- package/dist/wizard/types.js +1 -0
- package/package.json +117 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4198 @@
|
|
|
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
|
+
});
|
|
15
|
+
};
|
|
16
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
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
|
+
|
|
81
|
+
// src/api-client.ts
|
|
82
|
+
var exports_api_client = {};
|
|
83
|
+
__export(exports_api_client, {
|
|
84
|
+
hooxFetch: () => hooxFetch,
|
|
85
|
+
WorkerAPIError: () => WorkerAPIError
|
|
86
|
+
});
|
|
87
|
+
function isRetryableStatus(status) {
|
|
88
|
+
if (status === 401 || status === 403 || status === 429)
|
|
89
|
+
return false;
|
|
90
|
+
if (status >= 500)
|
|
91
|
+
return true;
|
|
92
|
+
if (status === 408)
|
|
93
|
+
return true;
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
function isNetworkError(error) {
|
|
97
|
+
if (error instanceof Error) {
|
|
98
|
+
const msg = error.message.toLowerCase();
|
|
99
|
+
return error.name === "AbortError" || error.name === "TimeoutError" || msg.includes("fetch") || msg.includes("network") || msg.includes("econnrefused") || msg.includes("enotfound") || msg.includes("dns") || msg.includes("timeout");
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
function sleep(ms) {
|
|
104
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
105
|
+
}
|
|
106
|
+
function activeProfile(override) {
|
|
107
|
+
return override ?? resolveOperatorTransportProfile();
|
|
108
|
+
}
|
|
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 ?? {};
|
|
112
|
+
let lastError;
|
|
113
|
+
for (let attempt = 0;attempt <= MAX_RETRIES; attempt++) {
|
|
114
|
+
const controller = new AbortController;
|
|
115
|
+
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
116
|
+
try {
|
|
117
|
+
const url = operatorUrl(profile, path);
|
|
118
|
+
const headers = {
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
...buildOperatorAuthHeaders(profile),
|
|
121
|
+
...fetchInit.headers
|
|
122
|
+
};
|
|
123
|
+
const response = await fetch(url, {
|
|
124
|
+
...fetchInit,
|
|
125
|
+
headers,
|
|
126
|
+
signal: controller.signal
|
|
127
|
+
});
|
|
128
|
+
if (response.status === 429) {
|
|
129
|
+
throw new WorkerAPIError("API rate limited \u2014 backing off", {
|
|
130
|
+
status: 429,
|
|
131
|
+
retryable: false
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (response.status === 401 || response.status === 403) {
|
|
135
|
+
throw new WorkerAPIError(`Authentication failed (HTTP ${response.status})`, { status: response.status, retryable: false });
|
|
136
|
+
}
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
if (attempt < MAX_RETRIES && isRetryableStatus(response.status)) {
|
|
139
|
+
const delay = Math.min(BASE_RETRY_DELAY_MS * Math.pow(2, attempt), MAX_RETRY_DELAY_MS);
|
|
140
|
+
await sleep(delay);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
throw new WorkerAPIError(`HTTP ${response.status}: ${response.statusText}`, {
|
|
144
|
+
status: response.status,
|
|
145
|
+
retryable: isRetryableStatus(response.status)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return await response.json();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
lastError = error;
|
|
151
|
+
if (error instanceof WorkerAPIError && !error.retryable) {
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
if (attempt < MAX_RETRIES && isNetworkError(error)) {
|
|
155
|
+
const delay = Math.min(BASE_RETRY_DELAY_MS * Math.pow(2, attempt), MAX_RETRY_DELAY_MS);
|
|
156
|
+
await sleep(delay);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (!(error instanceof WorkerAPIError)) {
|
|
160
|
+
throw new WorkerAPIError(`Network request failed: ${error.message}`, { cause: error, retryable: false, status: 0 });
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
} finally {
|
|
164
|
+
clearTimeout(timeoutId);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
throw new WorkerAPIError("Max retries exhausted", {
|
|
168
|
+
cause: lastError,
|
|
169
|
+
retryable: false
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
var FETCH_TIMEOUT_MS = 5000, MAX_RETRIES = 3, BASE_RETRY_DELAY_MS = 1000, MAX_RETRY_DELAY_MS = 16000, WorkerAPIError;
|
|
173
|
+
var init_api_client = __esm(() => {
|
|
174
|
+
init_operator_transport();
|
|
175
|
+
WorkerAPIError = class WorkerAPIError extends Error {
|
|
176
|
+
status;
|
|
177
|
+
retryable;
|
|
178
|
+
constructor(message, options) {
|
|
179
|
+
super(message, { cause: options?.cause });
|
|
180
|
+
this.name = "WorkerAPIError";
|
|
181
|
+
this.status = options?.status ?? 0;
|
|
182
|
+
this.retryable = options?.retryable ?? false;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// src/sse.ts
|
|
188
|
+
var exports_sse = {};
|
|
189
|
+
__export(exports_sse, {
|
|
190
|
+
subscribeSSE: () => subscribeSSE
|
|
191
|
+
});
|
|
192
|
+
function subscribeSSE(path, callback, onStatus, options) {
|
|
193
|
+
let aborted = false;
|
|
194
|
+
const profile = options?.transport ?? (options?.transportEnv ? resolveOperatorTransportProfile(options.transportEnv) : resolveOperatorTransportProfile());
|
|
195
|
+
const run = async () => {
|
|
196
|
+
let attempt = 0;
|
|
197
|
+
while (!aborted && attempt < MAX_RECONNECT_ATTEMPTS) {
|
|
198
|
+
try {
|
|
199
|
+
const url = operatorUrl(profile, path);
|
|
200
|
+
const response = await fetch(url, {
|
|
201
|
+
headers: {
|
|
202
|
+
Accept: "text/event-stream",
|
|
203
|
+
...buildOperatorAuthHeaders(profile)
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
if (!response.ok || !response.body) {
|
|
207
|
+
throw new Error(`SSE connection failed: HTTP ${response.status}`);
|
|
208
|
+
}
|
|
209
|
+
attempt = 0;
|
|
210
|
+
onStatus?.("connected");
|
|
211
|
+
const reader = response.body.getReader();
|
|
212
|
+
const decoder = new TextDecoder;
|
|
213
|
+
let buffer = "";
|
|
214
|
+
while (!aborted) {
|
|
215
|
+
const { done, value } = await reader.read();
|
|
216
|
+
if (done)
|
|
217
|
+
break;
|
|
218
|
+
buffer += decoder.decode(value, { stream: true });
|
|
219
|
+
const lines = buffer.split(`
|
|
220
|
+
`);
|
|
221
|
+
buffer = lines.pop() || "";
|
|
222
|
+
for (const line of lines) {
|
|
223
|
+
const trimmed = line.trim();
|
|
224
|
+
if (trimmed.startsWith("data: ")) {
|
|
225
|
+
try {
|
|
226
|
+
const parsed = JSON.parse(trimmed.slice(6));
|
|
227
|
+
callback(parsed);
|
|
228
|
+
} catch {}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
if (aborted)
|
|
234
|
+
break;
|
|
235
|
+
onStatus?.("reconnecting");
|
|
236
|
+
const delay = Math.min(RECONNECT_BASE_MS * Math.pow(2, attempt), RECONNECT_MAX_MS);
|
|
237
|
+
attempt++;
|
|
238
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (!aborted) {
|
|
242
|
+
onStatus?.("disconnected");
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
run();
|
|
246
|
+
return {
|
|
247
|
+
abort: () => {
|
|
248
|
+
aborted = true;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
var RECONNECT_BASE_MS = 1000, RECONNECT_MAX_MS = 16000, MAX_RECONNECT_ATTEMPTS = 10;
|
|
253
|
+
var init_sse = __esm(() => {
|
|
254
|
+
init_operator_transport();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// src/kvKeys.ts
|
|
258
|
+
var KV_TRADE_DEFAULT_LEVERAGE = "trade:default_leverage";
|
|
259
|
+
var KV_TRADE_MAX_POSITION_SIZE = "trade:max_position_size";
|
|
260
|
+
var KV_TRADE_MAX_DAILY_DRAWDOWN_PERCENT = "trade:max_daily_drawdown_percent";
|
|
261
|
+
var KV_TRADE_TRAILING_STOP_PERCENT = "trade:trailing_stop_percent";
|
|
262
|
+
var KV_TRADE_KILL_SWITCH = "trade:kill_switch";
|
|
263
|
+
var KV_TRADE_ROUTING = "trade:routing";
|
|
264
|
+
var KV_BOT_ENABLED = "bot:enabled";
|
|
265
|
+
var KV_BOT_DEFAULT_CHAT_ID = "bot:default_chat_id";
|
|
266
|
+
var KV_BOT_NOTIFY_ON_EXECUTION = "bot:notify_on_execution";
|
|
267
|
+
var KV_BOT_NOTIFY_ON_ERROR = "bot:notify_on_error";
|
|
268
|
+
var KV_WEBHOOK_IP_CHECK_ENABLED = "webhook:tradingview:ip_check_enabled";
|
|
269
|
+
var KV_WEBHOOK_ALLOWED_IPS = "webhook:tradingview:allowed_ips";
|
|
270
|
+
var KV_WEBHOOK_QUEUE_MODE = "webhook:queue_mode";
|
|
271
|
+
var KV_EMAIL_SCAN_SUBJECT = "email:scan_subject";
|
|
272
|
+
var KV_EMAIL_USE_IMAP = "email:use_imap";
|
|
273
|
+
var KV_EMAIL_COIN_PATTERN = "email:coin_pattern";
|
|
274
|
+
var KV_EMAIL_ACTION_PATTERN = "email:action_pattern";
|
|
275
|
+
var KV_EMAIL_QUANTITY_MULTIPLIER = "email:quantity_multiplier";
|
|
276
|
+
var KV_AGENT_CONFIG = "agent:config";
|
|
277
|
+
var KV_AGENT_OPENAI_KEY = "agent:openai_key";
|
|
278
|
+
var KV_AGENT_ANTHROPIC_KEY = "agent:anthropic_key";
|
|
279
|
+
var KV_AGENT_GOOGLE_KEY = "agent:google_key";
|
|
280
|
+
var KV_DASHBOARD_AI_HEALTH_SUMMARY = "dashboard:ai_health_summary";
|
|
281
|
+
var KV_HEALTH_CHECK = "health_check";
|
|
282
|
+
var KV_HOUSEKEEPING_LAST_CHECK = "housekeeping:last_check";
|
|
283
|
+
var QUEUE_MODE_FAILOVER = "queue_failover";
|
|
284
|
+
var QUEUE_MODE_EVERYWHERE = "queue_everywhere";
|
|
285
|
+
var QUEUE_MODE_DISABLED = "queue_disabled";
|
|
286
|
+
var KVKeys = {
|
|
287
|
+
KV_TRADE_DEFAULT_LEVERAGE,
|
|
288
|
+
KV_TRADE_MAX_POSITION_SIZE,
|
|
289
|
+
KV_TRADE_MAX_DAILY_DRAWDOWN_PERCENT,
|
|
290
|
+
KV_TRADE_TRAILING_STOP_PERCENT,
|
|
291
|
+
KV_TRADE_KILL_SWITCH,
|
|
292
|
+
KV_TRADE_ROUTING,
|
|
293
|
+
KV_BOT_ENABLED,
|
|
294
|
+
KV_BOT_DEFAULT_CHAT_ID,
|
|
295
|
+
KV_BOT_NOTIFY_ON_EXECUTION,
|
|
296
|
+
KV_BOT_NOTIFY_ON_ERROR,
|
|
297
|
+
KV_WEBHOOK_IP_CHECK_ENABLED,
|
|
298
|
+
KV_WEBHOOK_ALLOWED_IPS,
|
|
299
|
+
KV_WEBHOOK_QUEUE_MODE,
|
|
300
|
+
KV_EMAIL_SCAN_SUBJECT,
|
|
301
|
+
KV_EMAIL_USE_IMAP,
|
|
302
|
+
KV_EMAIL_COIN_PATTERN,
|
|
303
|
+
KV_EMAIL_ACTION_PATTERN,
|
|
304
|
+
KV_EMAIL_QUANTITY_MULTIPLIER,
|
|
305
|
+
KV_AGENT_CONFIG,
|
|
306
|
+
KV_AGENT_OPENAI_KEY,
|
|
307
|
+
KV_AGENT_ANTHROPIC_KEY,
|
|
308
|
+
KV_AGENT_GOOGLE_KEY,
|
|
309
|
+
KV_DASHBOARD_AI_HEALTH_SUMMARY,
|
|
310
|
+
KV_HEALTH_CHECK,
|
|
311
|
+
KV_HOUSEKEEPING_LAST_CHECK,
|
|
312
|
+
QUEUE_MODE_FAILOVER,
|
|
313
|
+
QUEUE_MODE_EVERYWHERE,
|
|
314
|
+
QUEUE_MODE_DISABLED
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// src/agent-config-fields.ts
|
|
318
|
+
var AGENT_CONFIG_KV_KEY = KV_AGENT_CONFIG;
|
|
319
|
+
var AGENT_CONFIG_EMBEDDED_FIELDS = {
|
|
320
|
+
"providers:default_provider": { kind: "top", key: "defaultProvider" },
|
|
321
|
+
"providers:fallback_chain": { kind: "top", key: "fallbackChain" },
|
|
322
|
+
"providers:timeout_ms": { kind: "top", key: "timeoutMs" },
|
|
323
|
+
"providers:retry_count": { kind: "top", key: "retryCount" },
|
|
324
|
+
"models:workers_ai_model": { kind: "model", provider: "workers-ai" },
|
|
325
|
+
"models:openai_model": { kind: "model", provider: "openai" },
|
|
326
|
+
"models:anthropic_model": { kind: "model", provider: "anthropic" },
|
|
327
|
+
"models:google_model": { kind: "model", provider: "google" },
|
|
328
|
+
"risk:max_daily_drawdown_percent": {
|
|
329
|
+
kind: "top",
|
|
330
|
+
key: "maxDailyDrawdownPercent"
|
|
331
|
+
},
|
|
332
|
+
"risk:trailing_stop_percent": { kind: "top", key: "trailingStopPercent" },
|
|
333
|
+
"risk:take_profit_percent": { kind: "top", key: "takeProfitPercent" }
|
|
334
|
+
};
|
|
335
|
+
var DASHBOARD_SECTIONS_AGENT_CONFIG = new Set([
|
|
336
|
+
"providers",
|
|
337
|
+
"models"
|
|
338
|
+
]);
|
|
339
|
+
function isAgentConfigEmbeddedField(fieldKey) {
|
|
340
|
+
return Object.prototype.hasOwnProperty.call(AGENT_CONFIG_EMBEDDED_FIELDS, fieldKey);
|
|
341
|
+
}
|
|
342
|
+
function isAgentConfigSection(sectionId) {
|
|
343
|
+
return DASHBOARD_SECTIONS_AGENT_CONFIG.has(sectionId);
|
|
344
|
+
}
|
|
345
|
+
function parseAgentConfigJson(raw) {
|
|
346
|
+
if (raw === null || raw === undefined || raw === "")
|
|
347
|
+
return {};
|
|
348
|
+
try {
|
|
349
|
+
let v = JSON.parse(raw);
|
|
350
|
+
if (typeof v === "string") {
|
|
351
|
+
try {
|
|
352
|
+
v = JSON.parse(v);
|
|
353
|
+
} catch {
|
|
354
|
+
return {};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
358
|
+
return { ...v };
|
|
359
|
+
}
|
|
360
|
+
} catch {}
|
|
361
|
+
return {};
|
|
362
|
+
}
|
|
363
|
+
function serializeAgentConfigForKv(config) {
|
|
364
|
+
return JSON.stringify(config);
|
|
365
|
+
}
|
|
366
|
+
var NUMBER_TOP_KEYS = new Set([
|
|
367
|
+
"timeoutMs",
|
|
368
|
+
"retryCount",
|
|
369
|
+
"maxDailyDrawdownPercent",
|
|
370
|
+
"trailingStopPercent",
|
|
371
|
+
"takeProfitPercent"
|
|
372
|
+
]);
|
|
373
|
+
function getAgentConfigEmbeddedValue(config, fieldKey) {
|
|
374
|
+
const spec = AGENT_CONFIG_EMBEDDED_FIELDS[fieldKey];
|
|
375
|
+
if (!spec)
|
|
376
|
+
return;
|
|
377
|
+
if (spec.kind === "model") {
|
|
378
|
+
const map = config.modelMap;
|
|
379
|
+
if (!map || typeof map !== "object" || Array.isArray(map))
|
|
380
|
+
return;
|
|
381
|
+
const v2 = map[spec.provider];
|
|
382
|
+
if (typeof v2 === "string" || typeof v2 === "number" || typeof v2 === "boolean")
|
|
383
|
+
return v2;
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const v = config[spec.key];
|
|
387
|
+
if (spec.key === "fallbackChain") {
|
|
388
|
+
if (Array.isArray(v))
|
|
389
|
+
return JSON.stringify(v);
|
|
390
|
+
if (typeof v === "string")
|
|
391
|
+
return v;
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
395
|
+
return v;
|
|
396
|
+
}
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
function setAgentConfigEmbeddedValue(config, fieldKey, value) {
|
|
400
|
+
const spec = AGENT_CONFIG_EMBEDDED_FIELDS[fieldKey];
|
|
401
|
+
if (!spec)
|
|
402
|
+
return { ...config };
|
|
403
|
+
const next = { ...config };
|
|
404
|
+
if (spec.kind === "model") {
|
|
405
|
+
const prev = next.modelMap && typeof next.modelMap === "object" && !Array.isArray(next.modelMap) ? next.modelMap : {};
|
|
406
|
+
next.modelMap = { ...prev, [spec.provider]: String(value) };
|
|
407
|
+
return next;
|
|
408
|
+
}
|
|
409
|
+
if (spec.key === "fallbackChain") {
|
|
410
|
+
if (typeof value === "string") {
|
|
411
|
+
try {
|
|
412
|
+
next.fallbackChain = JSON.parse(value);
|
|
413
|
+
} catch {
|
|
414
|
+
next.fallbackChain = value;
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
next.fallbackChain = value;
|
|
418
|
+
}
|
|
419
|
+
return next;
|
|
420
|
+
}
|
|
421
|
+
if (NUMBER_TOP_KEYS.has(spec.key)) {
|
|
422
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
423
|
+
next[spec.key] = Number.isFinite(n) ? n : value;
|
|
424
|
+
return next;
|
|
425
|
+
}
|
|
426
|
+
next[spec.key] = value;
|
|
427
|
+
return next;
|
|
428
|
+
}
|
|
429
|
+
function expandAgentConfigToFieldMap(config) {
|
|
430
|
+
const out = {};
|
|
431
|
+
for (const fieldKey of Object.keys(AGENT_CONFIG_EMBEDDED_FIELDS)) {
|
|
432
|
+
const v = getAgentConfigEmbeddedValue(config, fieldKey);
|
|
433
|
+
if (v !== undefined)
|
|
434
|
+
out[fieldKey] = v;
|
|
435
|
+
}
|
|
436
|
+
return out;
|
|
437
|
+
}
|
|
438
|
+
function applyAgentConfigFieldUpdates(config, updates) {
|
|
439
|
+
let next = { ...config };
|
|
440
|
+
for (const [fieldKey, value] of Object.entries(updates)) {
|
|
441
|
+
if (isAgentConfigEmbeddedField(fieldKey)) {
|
|
442
|
+
next = setAgentConfigEmbeddedValue(next, fieldKey, value);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return next;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/analytics.ts
|
|
449
|
+
async function trackAnalytics(env, endpoint, body, options) {
|
|
450
|
+
if (!env.ANALYTICS_SERVICE)
|
|
451
|
+
return;
|
|
452
|
+
try {
|
|
453
|
+
const payload = {
|
|
454
|
+
...body,
|
|
455
|
+
...options?.indexes ? { indexes: [...options.indexes] } : {}
|
|
456
|
+
};
|
|
457
|
+
const headers = {
|
|
458
|
+
"Content-Type": "application/json"
|
|
459
|
+
};
|
|
460
|
+
const internalKey = env.INTERNAL_KEY_BINDING;
|
|
461
|
+
if (internalKey) {
|
|
462
|
+
headers["X-Internal-Auth-Key"] = internalKey;
|
|
463
|
+
}
|
|
464
|
+
await env.ANALYTICS_SERVICE.fetch(new Request(`http://localhost${endpoint}`, {
|
|
465
|
+
method: "POST",
|
|
466
|
+
headers,
|
|
467
|
+
body: JSON.stringify(payload)
|
|
468
|
+
}));
|
|
469
|
+
} catch (e) {
|
|
470
|
+
console.error("Analytics tracking failed:", e);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/colors.ts
|
|
475
|
+
var Colors = {
|
|
476
|
+
background: "#050508",
|
|
477
|
+
foreground: "#E8E8F0",
|
|
478
|
+
card: "#0A0A0F",
|
|
479
|
+
border: "#232330",
|
|
480
|
+
muted: "#8B8B9E",
|
|
481
|
+
"muted-foreground": "#6E6E84",
|
|
482
|
+
dim: "#3C3C50",
|
|
483
|
+
accent: "#818CF8",
|
|
484
|
+
"accent-dim": "#6366F1",
|
|
485
|
+
success: "#34D399",
|
|
486
|
+
warning: "#FBBF24",
|
|
487
|
+
error: "#FB7185",
|
|
488
|
+
info: "#38BDF8",
|
|
489
|
+
text: "#E8E8F0",
|
|
490
|
+
"text-muted": "#8B8B9E",
|
|
491
|
+
panel: "#0A0A0F",
|
|
492
|
+
divider: "#232330",
|
|
493
|
+
highlight: "#22D3EE",
|
|
494
|
+
backdrop: "#000000"
|
|
495
|
+
};
|
|
496
|
+
var CoolBracketPalette = [
|
|
497
|
+
"#22D3EE",
|
|
498
|
+
"#38BDF8",
|
|
499
|
+
"#60A5FA",
|
|
500
|
+
"#818CF8",
|
|
501
|
+
"#A78BFA",
|
|
502
|
+
"#C084FC",
|
|
503
|
+
"#E879F9",
|
|
504
|
+
"#A78BFA",
|
|
505
|
+
"#818CF8",
|
|
506
|
+
"#38BDF8"
|
|
507
|
+
];
|
|
508
|
+
var ConnectionStatusColor = {
|
|
509
|
+
connected: Colors.success,
|
|
510
|
+
polling: Colors.highlight,
|
|
511
|
+
reconnecting: Colors.warning,
|
|
512
|
+
offline: Colors.error
|
|
513
|
+
};
|
|
514
|
+
var WorkerStatusColor = {
|
|
515
|
+
operational: Colors.success,
|
|
516
|
+
degraded: Colors.warning,
|
|
517
|
+
down: Colors.error
|
|
518
|
+
};
|
|
519
|
+
var LogLevelColor = {
|
|
520
|
+
error: Colors.error,
|
|
521
|
+
warn: Colors.warning,
|
|
522
|
+
info: Colors.foreground,
|
|
523
|
+
debug: Colors.muted
|
|
524
|
+
};
|
|
525
|
+
var AlertSeverityColor = {
|
|
526
|
+
info: Colors.info,
|
|
527
|
+
warning: Colors.warning,
|
|
528
|
+
error: Colors.error,
|
|
529
|
+
critical: Colors.error
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
// src/config.ts
|
|
533
|
+
import {
|
|
534
|
+
readFileSync,
|
|
535
|
+
writeFileSync,
|
|
536
|
+
existsSync,
|
|
537
|
+
mkdirSync,
|
|
538
|
+
chmodSync,
|
|
539
|
+
statSync
|
|
540
|
+
} from "fs";
|
|
541
|
+
import { join } from "path";
|
|
542
|
+
import { homedir } from "os";
|
|
543
|
+
var HOX_DIR = join(homedir(), ".hoox");
|
|
544
|
+
var CONFIG_PATH = join(HOX_DIR, "config.json");
|
|
545
|
+
var HOOX_DIR_MODE = 448;
|
|
546
|
+
var HOOX_CONFIG_FILE_MODE = 384;
|
|
547
|
+
var DEFAULT_CONFIG = {
|
|
548
|
+
apiUrl: "http://localhost:8787",
|
|
549
|
+
apiToken: "",
|
|
550
|
+
transport: "public",
|
|
551
|
+
refreshIntervalMs: 500,
|
|
552
|
+
theme: "dark",
|
|
553
|
+
activeExchanges: ["binance", "bybit", "mexc"],
|
|
554
|
+
notifications: {
|
|
555
|
+
alerts: true,
|
|
556
|
+
trades: true,
|
|
557
|
+
debug: false,
|
|
558
|
+
system: true
|
|
559
|
+
},
|
|
560
|
+
soundEnabled: true,
|
|
561
|
+
defaultView: "dashboard"
|
|
562
|
+
};
|
|
563
|
+
function readConfigSync() {
|
|
564
|
+
const envConfig = {
|
|
565
|
+
apiUrl: process.env.HOOX_API_URL,
|
|
566
|
+
apiToken: process.env.HOOX_API_TOKEN
|
|
567
|
+
};
|
|
568
|
+
let fileConfig = {};
|
|
569
|
+
try {
|
|
570
|
+
if (existsSync(CONFIG_PATH)) {
|
|
571
|
+
fileConfig = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
572
|
+
}
|
|
573
|
+
} catch {}
|
|
574
|
+
return {
|
|
575
|
+
...DEFAULT_CONFIG,
|
|
576
|
+
...fileConfig,
|
|
577
|
+
...Object.fromEntries(Object.entries(envConfig).filter(([_, v]) => v !== undefined))
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
async function readConfig() {
|
|
581
|
+
return readConfigSync();
|
|
582
|
+
}
|
|
583
|
+
function ensureHooxDirSecure(dir = HOX_DIR) {
|
|
584
|
+
if (!existsSync(dir)) {
|
|
585
|
+
mkdirSync(dir, { recursive: true, mode: HOOX_DIR_MODE });
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
chmodSync(dir, HOOX_DIR_MODE);
|
|
589
|
+
} catch {}
|
|
590
|
+
}
|
|
591
|
+
function writeSecureFileSync(filePath, contents, mode = HOOX_CONFIG_FILE_MODE) {
|
|
592
|
+
writeFileSync(filePath, contents, { encoding: "utf-8", mode });
|
|
593
|
+
try {
|
|
594
|
+
chmodSync(filePath, mode);
|
|
595
|
+
} catch {}
|
|
596
|
+
}
|
|
597
|
+
function isUnixModeGroupOrWorldAccessible(mode) {
|
|
598
|
+
return (mode & 63) !== 0;
|
|
599
|
+
}
|
|
600
|
+
function isConfigWorldOrGroupReadable(filePath = CONFIG_PATH) {
|
|
601
|
+
try {
|
|
602
|
+
if (!existsSync(filePath))
|
|
603
|
+
return false;
|
|
604
|
+
return isUnixModeGroupOrWorldAccessible(statSync(filePath).mode & 511);
|
|
605
|
+
} catch {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function formatConfigPermissionWarning(filePath = CONFIG_PATH) {
|
|
610
|
+
if (!isConfigWorldOrGroupReadable(filePath))
|
|
611
|
+
return null;
|
|
612
|
+
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.`;
|
|
613
|
+
}
|
|
614
|
+
function writeConfigSync(config) {
|
|
615
|
+
try {
|
|
616
|
+
ensureHooxDirSecure(HOX_DIR);
|
|
617
|
+
writeSecureFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), HOOX_CONFIG_FILE_MODE);
|
|
618
|
+
} catch (err) {
|
|
619
|
+
console.error("Failed to write config:", err);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async function writeConfig(config) {
|
|
623
|
+
writeConfigSync(config);
|
|
624
|
+
}
|
|
625
|
+
var VALID_TRANSPORTS2 = new Set([
|
|
626
|
+
"public",
|
|
627
|
+
"access",
|
|
628
|
+
"mtls",
|
|
629
|
+
"tunnel"
|
|
630
|
+
]);
|
|
631
|
+
function validateConfig(config) {
|
|
632
|
+
const errors = [];
|
|
633
|
+
if (config.apiUrl && !config.apiUrl.startsWith("http")) {
|
|
634
|
+
errors.push("apiUrl must start with http:// or https://");
|
|
635
|
+
}
|
|
636
|
+
if (config.refreshIntervalMs !== undefined && config.refreshIntervalMs < 100) {
|
|
637
|
+
errors.push("refreshIntervalMs must be >= 100ms");
|
|
638
|
+
}
|
|
639
|
+
if (config.theme && !["dark", "light"].includes(config.theme)) {
|
|
640
|
+
errors.push('theme must be "dark" or "light"');
|
|
641
|
+
}
|
|
642
|
+
if (config.transport !== undefined && !VALID_TRANSPORTS2.has(config.transport)) {
|
|
643
|
+
errors.push('transport must be "public" | "access" | "mtls" | "tunnel"');
|
|
644
|
+
}
|
|
645
|
+
return errors;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// src/cron-handler.ts
|
|
649
|
+
function createCronHandler(options) {
|
|
650
|
+
const { name, handler, logger } = options;
|
|
651
|
+
return async (event, env, ctx) => {
|
|
652
|
+
const startTime = Date.now();
|
|
653
|
+
logger?.info(`${name} cron triggered`, {
|
|
654
|
+
cron: event.cron,
|
|
655
|
+
scheduledTime: event.scheduledTime
|
|
656
|
+
});
|
|
657
|
+
try {
|
|
658
|
+
await handler(event, env, ctx);
|
|
659
|
+
const durationMs = Date.now() - startTime;
|
|
660
|
+
logger?.info(`${name} cron handler completed successfully`, {
|
|
661
|
+
cron: event.cron,
|
|
662
|
+
durationMs
|
|
663
|
+
});
|
|
664
|
+
} catch (error) {
|
|
665
|
+
const durationMs = Date.now() - startTime;
|
|
666
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
667
|
+
logger?.error(`${name} cron handler failed`, {
|
|
668
|
+
cron: event.cron,
|
|
669
|
+
durationMs,
|
|
670
|
+
error: errorMsg
|
|
671
|
+
});
|
|
672
|
+
throw error;
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/errors.ts
|
|
678
|
+
function toError(err, fallback = "Unknown error") {
|
|
679
|
+
if (err instanceof Error)
|
|
680
|
+
return err.message;
|
|
681
|
+
if (typeof err === "string")
|
|
682
|
+
return err;
|
|
683
|
+
if (err && typeof err === "object" && "message" in err && typeof err.message === "string") {
|
|
684
|
+
return err.message;
|
|
685
|
+
}
|
|
686
|
+
if (err === null || err === undefined)
|
|
687
|
+
return fallback;
|
|
688
|
+
try {
|
|
689
|
+
return JSON.stringify(err) || fallback;
|
|
690
|
+
} catch {
|
|
691
|
+
return fallback;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
var SENSITIVE_FIELDS = new Set([
|
|
695
|
+
"password",
|
|
696
|
+
"token",
|
|
697
|
+
"secret",
|
|
698
|
+
"api_key",
|
|
699
|
+
"apiKey",
|
|
700
|
+
"authorization"
|
|
701
|
+
]);
|
|
702
|
+
function hasSensitiveFields(obj) {
|
|
703
|
+
for (const key of Object.keys(obj)) {
|
|
704
|
+
if (SENSITIVE_FIELDS.has(key))
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
function hasErrorValues(obj) {
|
|
710
|
+
for (const v of Object.values(obj)) {
|
|
711
|
+
if (v instanceof Error)
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
function sanitizeOutput(data) {
|
|
717
|
+
if (data === null || data === undefined)
|
|
718
|
+
return data;
|
|
719
|
+
const t = typeof data;
|
|
720
|
+
if (t === "string" || t === "number" || t === "boolean")
|
|
721
|
+
return data;
|
|
722
|
+
if (data instanceof Error) {
|
|
723
|
+
return { name: data.name, message: data.message };
|
|
724
|
+
}
|
|
725
|
+
if (Array.isArray(data)) {
|
|
726
|
+
return data.map(sanitizeOutput);
|
|
727
|
+
}
|
|
728
|
+
if (t === "object") {
|
|
729
|
+
const obj = data;
|
|
730
|
+
const keys = Object.keys(obj);
|
|
731
|
+
if (keys.length < 10) {
|
|
732
|
+
const sanitized2 = {};
|
|
733
|
+
for (const k of keys) {
|
|
734
|
+
if (k === "stack" || k === "cause")
|
|
735
|
+
continue;
|
|
736
|
+
const v = obj[k];
|
|
737
|
+
sanitized2[k] = v instanceof Error ? { name: v.name, message: v.message } : sanitizeOutput(v);
|
|
738
|
+
}
|
|
739
|
+
return sanitized2;
|
|
740
|
+
}
|
|
741
|
+
if (!hasSensitiveFields(obj) && !hasErrorValues(obj)) {
|
|
742
|
+
return obj;
|
|
743
|
+
}
|
|
744
|
+
const sanitized = {};
|
|
745
|
+
for (const k of keys) {
|
|
746
|
+
if (k === "stack" || k === "cause")
|
|
747
|
+
continue;
|
|
748
|
+
const v = obj[k];
|
|
749
|
+
sanitized[k] = v instanceof Error ? { name: v.name, message: v.message } : sanitizeOutput(v);
|
|
750
|
+
}
|
|
751
|
+
return sanitized;
|
|
752
|
+
}
|
|
753
|
+
return data;
|
|
754
|
+
}
|
|
755
|
+
function createJsonResponse(data, status = 200) {
|
|
756
|
+
return new Response(JSON.stringify(sanitizeOutput(data)), {
|
|
757
|
+
status,
|
|
758
|
+
headers: { "Content-Type": "application/json" }
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
function createSuccessResponse(result) {
|
|
762
|
+
return createJsonResponse({ success: true, result }, 200);
|
|
763
|
+
}
|
|
764
|
+
function createErrorResponse(error, status) {
|
|
765
|
+
const message = typeof error === "string" ? error : error.message;
|
|
766
|
+
const statusCode = typeof error === "string" ? status ?? 500 : error.status ?? 500;
|
|
767
|
+
const body = { success: false, error: message };
|
|
768
|
+
if (typeof error !== "string" && error.code)
|
|
769
|
+
body.code = error.code;
|
|
770
|
+
if (typeof error !== "string" && error.details !== undefined)
|
|
771
|
+
body.details = error.details;
|
|
772
|
+
return new Response(JSON.stringify(body), {
|
|
773
|
+
status: statusCode,
|
|
774
|
+
headers: { "Content-Type": "application/json" }
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
var Errors = {
|
|
778
|
+
badRequest: (message) => createErrorResponse({ message, status: 400, code: "BAD_REQUEST" }),
|
|
779
|
+
unauthorized: (message = "Unauthorized") => createErrorResponse({ message, status: 401, code: "UNAUTHORIZED" }),
|
|
780
|
+
forbidden: (message = "Forbidden") => createErrorResponse({ message, status: 403, code: "FORBIDDEN" }),
|
|
781
|
+
notFound: (message = "Not found") => createErrorResponse({ message, status: 404, code: "NOT_FOUND" }),
|
|
782
|
+
methodNotAllowed: (message = "Method not allowed") => createErrorResponse({ message, status: 405, code: "METHOD_NOT_ALLOWED" }),
|
|
783
|
+
rateLimited: (retryAfter) => {
|
|
784
|
+
const res = createErrorResponse({
|
|
785
|
+
message: "Rate limit exceeded",
|
|
786
|
+
status: 429,
|
|
787
|
+
code: "RATE_LIMITED"
|
|
788
|
+
});
|
|
789
|
+
if (retryAfter)
|
|
790
|
+
res.headers.set("Retry-After", String(retryAfter));
|
|
791
|
+
return res;
|
|
792
|
+
},
|
|
793
|
+
internal: (err) => {
|
|
794
|
+
const message = err ? toError(err, "Internal server error") : "Internal server error";
|
|
795
|
+
return createErrorResponse({
|
|
796
|
+
message,
|
|
797
|
+
status: 500,
|
|
798
|
+
code: "INTERNAL_ERROR"
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
// src/d1/repository.ts
|
|
804
|
+
class D1Repository {
|
|
805
|
+
db;
|
|
806
|
+
constructor(db) {
|
|
807
|
+
this.db = db;
|
|
808
|
+
}
|
|
809
|
+
async query(sql, params = []) {
|
|
810
|
+
try {
|
|
811
|
+
const stmt = this.db.prepare(sql).bind(...params);
|
|
812
|
+
const result = await stmt.all();
|
|
813
|
+
return {
|
|
814
|
+
success: result.success,
|
|
815
|
+
results: result.results,
|
|
816
|
+
error: result.error
|
|
817
|
+
};
|
|
818
|
+
} catch (error) {
|
|
819
|
+
return {
|
|
820
|
+
success: false,
|
|
821
|
+
error: toError(error)
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
async execute(sql, params = []) {
|
|
826
|
+
try {
|
|
827
|
+
const stmt = this.db.prepare(sql).bind(...params);
|
|
828
|
+
const result = await stmt.run();
|
|
829
|
+
return {
|
|
830
|
+
success: result.success,
|
|
831
|
+
meta: {
|
|
832
|
+
last_row_id: result.meta?.last_row_id,
|
|
833
|
+
changes: result.meta?.changes,
|
|
834
|
+
duration: result.meta?.duration
|
|
835
|
+
},
|
|
836
|
+
error: result.error
|
|
837
|
+
};
|
|
838
|
+
} catch (error) {
|
|
839
|
+
return {
|
|
840
|
+
success: false,
|
|
841
|
+
error: toError(error)
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
async batch(statements) {
|
|
846
|
+
try {
|
|
847
|
+
const prepared = statements.map((s) => s.params && s.params.length > 0 ? this.db.prepare(s.query).bind(...s.params) : this.db.prepare(s.query));
|
|
848
|
+
const results = await this.db.batch(prepared);
|
|
849
|
+
return results.map((r) => ({
|
|
850
|
+
success: r.success,
|
|
851
|
+
meta: r.meta ? { last_row_id: r.meta.last_row_id, changes: r.meta.changes } : undefined,
|
|
852
|
+
error: r.error
|
|
853
|
+
}));
|
|
854
|
+
} catch (error) {
|
|
855
|
+
return statements.map(() => ({
|
|
856
|
+
success: false,
|
|
857
|
+
error: toError(error)
|
|
858
|
+
}));
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
prepare(sql) {
|
|
862
|
+
return this.db.prepare(sql);
|
|
863
|
+
}
|
|
864
|
+
async getTrades(limit = 50, offset = 0) {
|
|
865
|
+
return this.query("SELECT * FROM trades ORDER BY timestamp DESC LIMIT ? OFFSET ?", [limit, offset]);
|
|
866
|
+
}
|
|
867
|
+
async insertTrade(trade) {
|
|
868
|
+
return this.execute(`INSERT INTO trades (id, timestamp, exchange, symbol, action, quantity, price, leverage, status, raw_response)
|
|
869
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
870
|
+
trade.id || crypto.randomUUID(),
|
|
871
|
+
trade.timestamp,
|
|
872
|
+
trade.exchange,
|
|
873
|
+
trade.symbol,
|
|
874
|
+
trade.action,
|
|
875
|
+
trade.quantity ?? null,
|
|
876
|
+
trade.price ?? null,
|
|
877
|
+
trade.leverage ?? null,
|
|
878
|
+
trade.status,
|
|
879
|
+
trade.raw_response ?? null
|
|
880
|
+
]);
|
|
881
|
+
}
|
|
882
|
+
async getPositions(status) {
|
|
883
|
+
if (status) {
|
|
884
|
+
return this.query("SELECT * FROM positions WHERE status = ? ORDER BY updated_at DESC", [status]);
|
|
885
|
+
}
|
|
886
|
+
return this.query("SELECT * FROM positions ORDER BY updated_at DESC");
|
|
887
|
+
}
|
|
888
|
+
async upsertPosition(position) {
|
|
889
|
+
return this.execute(`REPLACE INTO positions (id, exchange, symbol, side, entry_price, mark_price, liquidation_price, leverage, size, unrealized_pnl, status, updated_at)
|
|
890
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
891
|
+
position.id,
|
|
892
|
+
position.exchange,
|
|
893
|
+
position.symbol,
|
|
894
|
+
position.side,
|
|
895
|
+
position.entry_price ?? null,
|
|
896
|
+
position.mark_price ?? null,
|
|
897
|
+
position.liquidation_price ?? null,
|
|
898
|
+
position.leverage ?? null,
|
|
899
|
+
position.size,
|
|
900
|
+
position.unrealized_pnl ?? null,
|
|
901
|
+
position.status,
|
|
902
|
+
position.updated_at
|
|
903
|
+
]);
|
|
904
|
+
}
|
|
905
|
+
async getTradeSignals(limit = 10) {
|
|
906
|
+
return this.query(`SELECT signal_id, timestamp, symbol, signal_type, source, processed_at
|
|
907
|
+
FROM trade_signals
|
|
908
|
+
ORDER BY processed_at DESC
|
|
909
|
+
LIMIT ?`, [limit]);
|
|
910
|
+
}
|
|
911
|
+
async insertTradeSignal(signal) {
|
|
912
|
+
return this.execute(`INSERT INTO trade_signals (signal_id, timestamp, symbol, signal_type, source, raw_data)
|
|
913
|
+
VALUES (?, ?, ?, ?, ?, ?)`, [
|
|
914
|
+
signal.signal_id,
|
|
915
|
+
signal.timestamp,
|
|
916
|
+
signal.symbol,
|
|
917
|
+
signal.signal_type,
|
|
918
|
+
signal.source ?? null,
|
|
919
|
+
signal.raw_data ?? null
|
|
920
|
+
]);
|
|
921
|
+
}
|
|
922
|
+
async getSystemLogs(limit = 50) {
|
|
923
|
+
return this.query("SELECT * FROM system_logs ORDER BY timestamp DESC LIMIT ?", [limit]);
|
|
924
|
+
}
|
|
925
|
+
async insertSystemLog(log) {
|
|
926
|
+
return this.execute(`INSERT INTO system_logs (id, level, service, message, details) VALUES (?, ?, ?, ?, ?)`, [
|
|
927
|
+
log.id || crypto.randomUUID(),
|
|
928
|
+
log.level,
|
|
929
|
+
log.service,
|
|
930
|
+
log.message,
|
|
931
|
+
log.details ?? null
|
|
932
|
+
]);
|
|
933
|
+
}
|
|
934
|
+
async getLatestBalances() {
|
|
935
|
+
return this.query(`
|
|
936
|
+
SELECT b.id, b.exchange, b.asset, b.total, b.free, b.used, b.timestamp
|
|
937
|
+
FROM balances b
|
|
938
|
+
INNER JOIN (
|
|
939
|
+
SELECT exchange, asset, MAX(timestamp) as max_time
|
|
940
|
+
FROM balances
|
|
941
|
+
GROUP BY exchange, asset
|
|
942
|
+
) latest ON b.exchange = latest.exchange AND b.asset = latest.asset AND b.timestamp = latest.max_time
|
|
943
|
+
`);
|
|
944
|
+
}
|
|
945
|
+
async healthCheck() {
|
|
946
|
+
try {
|
|
947
|
+
await this.db.prepare("SELECT 1").first();
|
|
948
|
+
return true;
|
|
949
|
+
} catch {
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// src/d1/schemas.ts
|
|
956
|
+
import { z } from "zod";
|
|
957
|
+
var TradeRecordSchema = z.object({
|
|
958
|
+
id: z.string(),
|
|
959
|
+
signal_id: z.string().nullable().optional(),
|
|
960
|
+
timestamp: z.number().int().positive(),
|
|
961
|
+
exchange: z.string().min(1),
|
|
962
|
+
symbol: z.string().min(1),
|
|
963
|
+
action: z.string().min(1),
|
|
964
|
+
quantity: z.number().nullable(),
|
|
965
|
+
price: z.number().nullable(),
|
|
966
|
+
leverage: z.number().nullable(),
|
|
967
|
+
status: z.string().min(1),
|
|
968
|
+
error_message: z.string().nullable().optional(),
|
|
969
|
+
raw_response: z.string().nullable().optional(),
|
|
970
|
+
created_at: z.number().int().optional()
|
|
971
|
+
}).strict();
|
|
972
|
+
var PositionRecordSchema = z.object({
|
|
973
|
+
id: z.string(),
|
|
974
|
+
exchange: z.string().min(1),
|
|
975
|
+
symbol: z.string().min(1),
|
|
976
|
+
side: z.enum(["LONG", "SHORT"]),
|
|
977
|
+
entry_price: z.number().nullable().optional(),
|
|
978
|
+
mark_price: z.number().nullable().optional(),
|
|
979
|
+
liquidation_price: z.number().nullable().optional(),
|
|
980
|
+
leverage: z.number().nullable().optional(),
|
|
981
|
+
size: z.number().nullable(),
|
|
982
|
+
unrealized_pnl: z.number().nullable().optional(),
|
|
983
|
+
status: z.enum(["OPEN", "CLOSED"]),
|
|
984
|
+
updated_at: z.number().int()
|
|
985
|
+
}).strict();
|
|
986
|
+
var BalanceRecordSchema = z.object({
|
|
987
|
+
id: z.string(),
|
|
988
|
+
exchange: z.string().min(1),
|
|
989
|
+
asset: z.string().min(1),
|
|
990
|
+
free: z.number().nullable(),
|
|
991
|
+
used: z.number().nullable(),
|
|
992
|
+
total: z.number().nullable(),
|
|
993
|
+
timestamp: z.number().int().positive()
|
|
994
|
+
}).strict();
|
|
995
|
+
var SystemLogRecordSchema = z.object({
|
|
996
|
+
id: z.string(),
|
|
997
|
+
timestamp: z.number().int().optional(),
|
|
998
|
+
level: z.string().min(1),
|
|
999
|
+
service: z.string().min(1),
|
|
1000
|
+
message: z.string().min(1),
|
|
1001
|
+
details: z.string().nullable().optional()
|
|
1002
|
+
}).strict();
|
|
1003
|
+
var TradeSignalRecordSchema = z.object({
|
|
1004
|
+
signal_id: z.string(),
|
|
1005
|
+
timestamp: z.number().int().positive(),
|
|
1006
|
+
symbol: z.string().min(1),
|
|
1007
|
+
signal_type: z.string().min(1),
|
|
1008
|
+
source: z.string().nullable().optional(),
|
|
1009
|
+
raw_data: z.string().nullable().optional(),
|
|
1010
|
+
processed_at: z.number().int().optional()
|
|
1011
|
+
}).strict();
|
|
1012
|
+
// src/dashboard-manifest.ts
|
|
1013
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
1014
|
+
import { join as join2 } from "path";
|
|
1015
|
+
var DASHBOARD_WORKER_PREFIX = {
|
|
1016
|
+
hoox: "global:",
|
|
1017
|
+
"trade-worker": "trade:",
|
|
1018
|
+
"agent-worker": "agent:",
|
|
1019
|
+
"telegram-worker": "bot:",
|
|
1020
|
+
"d1-worker": "database:",
|
|
1021
|
+
"email-worker": "email:",
|
|
1022
|
+
"web3-wallet-worker": "wallet:",
|
|
1023
|
+
"analytics-worker": "ai:",
|
|
1024
|
+
"report-worker": "report:",
|
|
1025
|
+
"pyne-worker": "pyne:"
|
|
1026
|
+
};
|
|
1027
|
+
var DASHBOARD_SECTION_PREFIX = {
|
|
1028
|
+
global: "global:",
|
|
1029
|
+
webhook: "webhook:",
|
|
1030
|
+
routing: "routing:",
|
|
1031
|
+
security: "webhook:",
|
|
1032
|
+
trade: "trade:",
|
|
1033
|
+
agent: "agent:",
|
|
1034
|
+
bot: "bot:",
|
|
1035
|
+
email: "email:",
|
|
1036
|
+
signal: "email:",
|
|
1037
|
+
database: "database:",
|
|
1038
|
+
retention: "retention:",
|
|
1039
|
+
cron: "cron:",
|
|
1040
|
+
behavior: "behavior:",
|
|
1041
|
+
risk: "trade:",
|
|
1042
|
+
exchanges: "trade:",
|
|
1043
|
+
fees: "trade:",
|
|
1044
|
+
providers: "agent:",
|
|
1045
|
+
models: "agent:",
|
|
1046
|
+
ai: "ai:",
|
|
1047
|
+
report: "report:",
|
|
1048
|
+
pyne: "pyne:",
|
|
1049
|
+
pyne_alerts: "pyne:"
|
|
1050
|
+
};
|
|
1051
|
+
var DASHBOARD_FIELD_KV_OVERRIDES = {
|
|
1052
|
+
"webhook:tradingview_ip_check_enabled": "webhook:tradingview:ip_check_enabled",
|
|
1053
|
+
"webhook:tradingview_allowed_ips": "webhook:tradingview:allowed_ips",
|
|
1054
|
+
"exchanges:binance_enabled": "exchange:binance:enabled",
|
|
1055
|
+
"exchanges:mexc_enabled": "exchange:mexc:enabled",
|
|
1056
|
+
"exchanges:bybit_enabled": "exchange:bybit:enabled"
|
|
1057
|
+
};
|
|
1058
|
+
var DASHBOARD_SECTIONS_NOT_FLAT_KV = new Set([
|
|
1059
|
+
"providers",
|
|
1060
|
+
"models",
|
|
1061
|
+
"cron",
|
|
1062
|
+
"behavior"
|
|
1063
|
+
]);
|
|
1064
|
+
var DASHBOARD_SECTIONS_UI_SKIP = new Set([
|
|
1065
|
+
"cron",
|
|
1066
|
+
"behavior"
|
|
1067
|
+
]);
|
|
1068
|
+
function isDashboardSectionFlatKv(sectionId) {
|
|
1069
|
+
return !DASHBOARD_SECTIONS_NOT_FLAT_KV.has(sectionId);
|
|
1070
|
+
}
|
|
1071
|
+
function isDashboardSectionEditable(sectionId) {
|
|
1072
|
+
return !DASHBOARD_SECTIONS_UI_SKIP.has(sectionId);
|
|
1073
|
+
}
|
|
1074
|
+
function buildDashboardKvKey(worker, fieldKey) {
|
|
1075
|
+
const override = DASHBOARD_FIELD_KV_OVERRIDES[fieldKey];
|
|
1076
|
+
if (override)
|
|
1077
|
+
return override;
|
|
1078
|
+
if (fieldKey.includes(":")) {
|
|
1079
|
+
const [section, ...rest] = fieldKey.split(":");
|
|
1080
|
+
const fieldName = rest.join(":");
|
|
1081
|
+
if (section === "exchanges" && fieldName.endsWith("_enabled")) {
|
|
1082
|
+
const exchange = fieldName.slice(0, -"_enabled".length);
|
|
1083
|
+
if (exchange)
|
|
1084
|
+
return `exchange:${exchange}:enabled`;
|
|
1085
|
+
}
|
|
1086
|
+
const mapped = DASHBOARD_SECTION_PREFIX[section ?? ""];
|
|
1087
|
+
if (mapped)
|
|
1088
|
+
return `${mapped}${fieldName}`;
|
|
1089
|
+
return `${section}:${fieldName}`;
|
|
1090
|
+
}
|
|
1091
|
+
const workerPrefix = DASHBOARD_WORKER_PREFIX[worker] ?? "";
|
|
1092
|
+
return `${workerPrefix}${fieldKey}`;
|
|
1093
|
+
}
|
|
1094
|
+
function stripJsonc(input) {
|
|
1095
|
+
let out = "";
|
|
1096
|
+
let i = 0;
|
|
1097
|
+
let inString = false;
|
|
1098
|
+
let stringChar = "";
|
|
1099
|
+
while (i < input.length) {
|
|
1100
|
+
const c = input[i];
|
|
1101
|
+
const next = input[i + 1];
|
|
1102
|
+
if (inString) {
|
|
1103
|
+
out += c;
|
|
1104
|
+
if (c === "\\" && i + 1 < input.length) {
|
|
1105
|
+
out += next;
|
|
1106
|
+
i += 2;
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
if (c === stringChar)
|
|
1110
|
+
inString = false;
|
|
1111
|
+
i++;
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
if (c === '"' || c === "'") {
|
|
1115
|
+
inString = true;
|
|
1116
|
+
stringChar = c;
|
|
1117
|
+
out += c;
|
|
1118
|
+
i++;
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
if (c === "/" && next === "/") {
|
|
1122
|
+
while (i < input.length && input[i] !== `
|
|
1123
|
+
`)
|
|
1124
|
+
i++;
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
if (c === "/" && next === "*") {
|
|
1128
|
+
i += 2;
|
|
1129
|
+
while (i < input.length && !(input[i] === "*" && input[i + 1] === "/"))
|
|
1130
|
+
i++;
|
|
1131
|
+
i += 2;
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
if (c === ",") {
|
|
1135
|
+
let j = i + 1;
|
|
1136
|
+
while (j < input.length && /\s/.test(input[j]))
|
|
1137
|
+
j++;
|
|
1138
|
+
if (j < input.length && (input[j] === "}" || input[j] === "]")) {
|
|
1139
|
+
i++;
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
out += c;
|
|
1144
|
+
i++;
|
|
1145
|
+
}
|
|
1146
|
+
return out;
|
|
1147
|
+
}
|
|
1148
|
+
function parseFieldType(value) {
|
|
1149
|
+
if (typeof value === "boolean")
|
|
1150
|
+
return "boolean";
|
|
1151
|
+
if (typeof value === "number")
|
|
1152
|
+
return "number";
|
|
1153
|
+
if (value === "true" || value === "false")
|
|
1154
|
+
return "boolean";
|
|
1155
|
+
if (value.trim().startsWith("{") || value.trim().startsWith("["))
|
|
1156
|
+
return "json";
|
|
1157
|
+
if (!Number.isNaN(Number(value)) && value.trim() !== "")
|
|
1158
|
+
return "number";
|
|
1159
|
+
return "text";
|
|
1160
|
+
}
|
|
1161
|
+
function labelFromKey(key) {
|
|
1162
|
+
return key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1163
|
+
}
|
|
1164
|
+
function parseDashboardManifest(content, workerName, onError) {
|
|
1165
|
+
try {
|
|
1166
|
+
const raw = JSON.parse(stripJsonc(content));
|
|
1167
|
+
const displayName = raw.display_name || raw.displayName || workerName;
|
|
1168
|
+
const description = raw.description || "";
|
|
1169
|
+
const sections = [];
|
|
1170
|
+
if (raw.sections) {
|
|
1171
|
+
for (const [sectionId, sectionData] of Object.entries(raw.sections)) {
|
|
1172
|
+
const fields = [];
|
|
1173
|
+
const sectionFields = sectionData.fields || {};
|
|
1174
|
+
const sectionOptions = sectionData.options || {};
|
|
1175
|
+
const sectionDescriptions = sectionData.descriptions || {};
|
|
1176
|
+
const sectionValidation = sectionData.validation || {};
|
|
1177
|
+
const sectionSecrets = sectionData.secrets || {};
|
|
1178
|
+
const sectionSecretCommands = sectionData.secret_commands || {};
|
|
1179
|
+
for (const [key, value] of Object.entries(sectionFields)) {
|
|
1180
|
+
const field = {
|
|
1181
|
+
key: `${sectionId}:${key}`,
|
|
1182
|
+
label: labelFromKey(key),
|
|
1183
|
+
type: parseFieldType(value),
|
|
1184
|
+
default: value
|
|
1185
|
+
};
|
|
1186
|
+
if (sectionOptions[key]) {
|
|
1187
|
+
field.type = "select";
|
|
1188
|
+
field.options = sectionOptions[key].map((opt) => ({
|
|
1189
|
+
value: String(opt),
|
|
1190
|
+
label: String(opt)
|
|
1191
|
+
}));
|
|
1192
|
+
}
|
|
1193
|
+
if (sectionDescriptions[key]) {
|
|
1194
|
+
field.description = String(sectionDescriptions[key]);
|
|
1195
|
+
}
|
|
1196
|
+
if (sectionValidation[key]) {
|
|
1197
|
+
field.validation = sectionValidation[key];
|
|
1198
|
+
}
|
|
1199
|
+
if (sectionSecrets[key]) {
|
|
1200
|
+
field.kind = "secret";
|
|
1201
|
+
field.cliCommand = sectionSecretCommands[key];
|
|
1202
|
+
}
|
|
1203
|
+
if (key.includes("kill_switch") || key.includes("drawdown") || key === "enabled" && sectionId === "global") {
|
|
1204
|
+
if (field.kind !== "secret")
|
|
1205
|
+
field.kind = "dangerous";
|
|
1206
|
+
}
|
|
1207
|
+
fields.push(field);
|
|
1208
|
+
}
|
|
1209
|
+
sections.push({
|
|
1210
|
+
id: sectionId,
|
|
1211
|
+
title: sectionData.title || labelFromKey(sectionId),
|
|
1212
|
+
description: sectionData.description || `Configure ${sectionId} settings`,
|
|
1213
|
+
icon: sectionData.icon,
|
|
1214
|
+
priority: sectionData.priority !== undefined ? sectionData.priority : sections.length * 10,
|
|
1215
|
+
fields
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
return {
|
|
1220
|
+
worker: workerName,
|
|
1221
|
+
displayName,
|
|
1222
|
+
description,
|
|
1223
|
+
sections: sections.sort((a, b) => a.priority - b.priority)
|
|
1224
|
+
};
|
|
1225
|
+
} catch (err) {
|
|
1226
|
+
const message = err instanceof Error ? err.message : `parse failed: ${String(err)}`;
|
|
1227
|
+
onError?.(`dashboard.jsonc for ${workerName}: ${message}`);
|
|
1228
|
+
return {
|
|
1229
|
+
worker: workerName,
|
|
1230
|
+
displayName: workerName,
|
|
1231
|
+
description: "Failed to load configuration",
|
|
1232
|
+
sections: []
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
var DASHBOARD_WORKER_IDS = [
|
|
1237
|
+
"hoox",
|
|
1238
|
+
"trade-worker",
|
|
1239
|
+
"agent-worker",
|
|
1240
|
+
"telegram-worker",
|
|
1241
|
+
"d1-worker",
|
|
1242
|
+
"email-worker",
|
|
1243
|
+
"web3-wallet-worker",
|
|
1244
|
+
"analytics-worker",
|
|
1245
|
+
"report-worker",
|
|
1246
|
+
"pyne-worker"
|
|
1247
|
+
];
|
|
1248
|
+
function dashboardWorkerDir(workerId) {
|
|
1249
|
+
if (workerId === "hoox")
|
|
1250
|
+
return "hoox-worker";
|
|
1251
|
+
return workerId;
|
|
1252
|
+
}
|
|
1253
|
+
function fieldTypeToKv(t) {
|
|
1254
|
+
if (t === "boolean")
|
|
1255
|
+
return "boolean";
|
|
1256
|
+
if (t === "number")
|
|
1257
|
+
return "number";
|
|
1258
|
+
return "string";
|
|
1259
|
+
}
|
|
1260
|
+
function defaultToString(value) {
|
|
1261
|
+
if (typeof value === "string")
|
|
1262
|
+
return value;
|
|
1263
|
+
return JSON.stringify(value);
|
|
1264
|
+
}
|
|
1265
|
+
function isDocumentationField(fieldKey) {
|
|
1266
|
+
const name = fieldKey.includes(":") ? fieldKey.slice(fieldKey.indexOf(":") + 1) : fieldKey;
|
|
1267
|
+
return /^(GET|POST|PUT|PATCH|DELETE)\s+\//i.test(name);
|
|
1268
|
+
}
|
|
1269
|
+
function kvManifestFromDashboardManifests(manifests) {
|
|
1270
|
+
const byKey = new Map;
|
|
1271
|
+
for (const manifest of manifests) {
|
|
1272
|
+
for (const section of manifest.sections) {
|
|
1273
|
+
if (!isDashboardSectionFlatKv(section.id))
|
|
1274
|
+
continue;
|
|
1275
|
+
for (const field of section.fields) {
|
|
1276
|
+
if (isDocumentationField(field.key))
|
|
1277
|
+
continue;
|
|
1278
|
+
const key = buildDashboardKvKey(manifest.worker, field.key);
|
|
1279
|
+
if (!key || key.endsWith(":"))
|
|
1280
|
+
continue;
|
|
1281
|
+
const next = {
|
|
1282
|
+
key,
|
|
1283
|
+
type: fieldTypeToKv(field.type),
|
|
1284
|
+
default: defaultToString(field.default),
|
|
1285
|
+
description: field.description || `${manifest.displayName}: ${field.label}`,
|
|
1286
|
+
secret: field.kind === "secret" ? true : undefined,
|
|
1287
|
+
worker: manifest.worker
|
|
1288
|
+
};
|
|
1289
|
+
const prev = byKey.get(key);
|
|
1290
|
+
if (!prev) {
|
|
1291
|
+
byKey.set(key, next);
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
if (!prev.secret && next.secret)
|
|
1295
|
+
prev.secret = true;
|
|
1296
|
+
if ((!prev.description || prev.description.length < 8) && next.description) {
|
|
1297
|
+
prev.description = next.description;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
const keys = [...byKey.values()].sort((a, b) => a.key.localeCompare(b.key));
|
|
1303
|
+
return { namespace: "CONFIG_KV", keys };
|
|
1304
|
+
}
|
|
1305
|
+
function loadDashboardKvManifestFromRoot(root, onError) {
|
|
1306
|
+
return kvManifestFromDashboardManifests(loadDashboardManifestsFromRoot(root, onError));
|
|
1307
|
+
}
|
|
1308
|
+
function tryReadFile(path) {
|
|
1309
|
+
try {
|
|
1310
|
+
if (!existsSync2(path))
|
|
1311
|
+
return null;
|
|
1312
|
+
return readFileSync2(path, "utf-8");
|
|
1313
|
+
} catch {
|
|
1314
|
+
return null;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
function loadDashboardManifestsFromRoot(root, onError) {
|
|
1318
|
+
const manifests = [];
|
|
1319
|
+
const seen = new Set;
|
|
1320
|
+
const workersDir = join2(root, "workers");
|
|
1321
|
+
if (!existsSync2(workersDir))
|
|
1322
|
+
return manifests;
|
|
1323
|
+
const warn = onError ?? ((message) => {
|
|
1324
|
+
console.warn(`[dashboard-manifest] ${message}`);
|
|
1325
|
+
});
|
|
1326
|
+
for (const workerId of DASHBOARD_WORKER_IDS) {
|
|
1327
|
+
const dir = dashboardWorkerDir(workerId);
|
|
1328
|
+
const path = join2(workersDir, dir, "dashboard.jsonc");
|
|
1329
|
+
const content = tryReadFile(path);
|
|
1330
|
+
if (!content)
|
|
1331
|
+
continue;
|
|
1332
|
+
const m = parseDashboardManifest(content, workerId, warn);
|
|
1333
|
+
if (m.sections.length > 0) {
|
|
1334
|
+
manifests.push(m);
|
|
1335
|
+
seen.add(workerId);
|
|
1336
|
+
} else if (m.description === "Failed to load configuration") {}
|
|
1337
|
+
}
|
|
1338
|
+
try {
|
|
1339
|
+
for (const entry of readdirSync(workersDir)) {
|
|
1340
|
+
if (entry === "dashboard")
|
|
1341
|
+
continue;
|
|
1342
|
+
const workerId = entry === "hoox-worker" || entry === "hoox" ? "hoox" : entry;
|
|
1343
|
+
if (seen.has(workerId))
|
|
1344
|
+
continue;
|
|
1345
|
+
const path = join2(workersDir, entry, "dashboard.jsonc");
|
|
1346
|
+
const content = tryReadFile(path);
|
|
1347
|
+
if (!content)
|
|
1348
|
+
continue;
|
|
1349
|
+
const m = parseDashboardManifest(content, workerId, warn);
|
|
1350
|
+
if (m.sections.length > 0) {
|
|
1351
|
+
manifests.push(m);
|
|
1352
|
+
seen.add(workerId);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
} catch {}
|
|
1356
|
+
const publicDir = join2(workersDir, "dashboard", "public", "workers");
|
|
1357
|
+
if (existsSync2(publicDir)) {
|
|
1358
|
+
try {
|
|
1359
|
+
for (const file of readdirSync(publicDir)) {
|
|
1360
|
+
if (!file.endsWith(".jsonc") && !file.endsWith(".json"))
|
|
1361
|
+
continue;
|
|
1362
|
+
const base = file.replace(/\.jsonc?$/, "");
|
|
1363
|
+
const workerId = base === "hoox-worker" || base === "hoox" ? "hoox" : base;
|
|
1364
|
+
if (seen.has(workerId))
|
|
1365
|
+
continue;
|
|
1366
|
+
const content = tryReadFile(join2(publicDir, file));
|
|
1367
|
+
if (!content)
|
|
1368
|
+
continue;
|
|
1369
|
+
const m = parseDashboardManifest(content, workerId, warn);
|
|
1370
|
+
if (m.sections.length > 0) {
|
|
1371
|
+
manifests.push(m);
|
|
1372
|
+
seen.add(workerId);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
} catch {}
|
|
1376
|
+
}
|
|
1377
|
+
return manifests;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// src/exchange-client.ts
|
|
1381
|
+
class ExchangeRouter {
|
|
1382
|
+
providers = new Map;
|
|
1383
|
+
registerProvider(provider) {
|
|
1384
|
+
this.providers.set(provider.name.toLowerCase(), provider);
|
|
1385
|
+
}
|
|
1386
|
+
route(exchange, env, options) {
|
|
1387
|
+
const key = exchange.toLowerCase();
|
|
1388
|
+
const provider = this.providers.get(key);
|
|
1389
|
+
if (!provider) {
|
|
1390
|
+
throw new Error(`Unsupported exchange: ${exchange}`);
|
|
1391
|
+
}
|
|
1392
|
+
if (options?.testnet && provider.supportsTestTrading === false) {
|
|
1393
|
+
throw new Error(`TEST_TRADING_UNSUPPORTED: ${exchange} does not support test/sandbox trading via API`);
|
|
1394
|
+
}
|
|
1395
|
+
if (!provider.hasCredentials(env)) {
|
|
1396
|
+
throw new Error(`API secret bindings not configured or accessible for ${exchange}`);
|
|
1397
|
+
}
|
|
1398
|
+
return { exchange: key, client: provider.createClient(env, options) };
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
class BaseExchangeClient {
|
|
1403
|
+
apiKey;
|
|
1404
|
+
apiSecret;
|
|
1405
|
+
baseUrl;
|
|
1406
|
+
isTestnet;
|
|
1407
|
+
constructor(config) {
|
|
1408
|
+
if (!config.apiKey || !config.apiSecret) {
|
|
1409
|
+
throw new Error("API key and secret are required.");
|
|
1410
|
+
}
|
|
1411
|
+
this.apiKey = config.apiKey;
|
|
1412
|
+
this.apiSecret = config.apiSecret;
|
|
1413
|
+
this.baseUrl = config.baseUrl || this.getDefaultBaseUrl();
|
|
1414
|
+
this.isTestnet = config.testnet || false;
|
|
1415
|
+
}
|
|
1416
|
+
async fetch(method, path, params) {
|
|
1417
|
+
const url = `${this.baseUrl}${path}`;
|
|
1418
|
+
const headers = this.buildHeaders(method, path, params);
|
|
1419
|
+
const body = params ? new URLSearchParams(params).toString() : undefined;
|
|
1420
|
+
const response = await fetch(url, {
|
|
1421
|
+
method,
|
|
1422
|
+
headers,
|
|
1423
|
+
body: method !== "GET" ? body : undefined
|
|
1424
|
+
});
|
|
1425
|
+
if (!response.ok) {
|
|
1426
|
+
const error = await response.text();
|
|
1427
|
+
throw new Error(`Exchange API error: ${response.status} - ${error}`);
|
|
1428
|
+
}
|
|
1429
|
+
return response.json();
|
|
1430
|
+
}
|
|
1431
|
+
async openLong(symbol, quantity, price, orderType = "MARKET") {
|
|
1432
|
+
return this.executeTrade({
|
|
1433
|
+
symbol,
|
|
1434
|
+
side: "long",
|
|
1435
|
+
orderType,
|
|
1436
|
+
quantity,
|
|
1437
|
+
price
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
async openShort(symbol, quantity, price, orderType = "MARKET") {
|
|
1441
|
+
return this.executeTrade({
|
|
1442
|
+
symbol,
|
|
1443
|
+
side: "short",
|
|
1444
|
+
orderType,
|
|
1445
|
+
quantity,
|
|
1446
|
+
price
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
async closeLong(symbol, quantity) {
|
|
1450
|
+
return this.executeTrade({
|
|
1451
|
+
symbol,
|
|
1452
|
+
side: "long",
|
|
1453
|
+
quantity,
|
|
1454
|
+
reduceOnly: true
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
async closeShort(symbol, quantity) {
|
|
1458
|
+
return this.executeTrade({
|
|
1459
|
+
symbol,
|
|
1460
|
+
side: "short",
|
|
1461
|
+
quantity,
|
|
1462
|
+
reduceOnly: true
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// src/exchanges/base-exchange-client.ts
|
|
1468
|
+
class BaseExchangeClient2 {
|
|
1469
|
+
apiKey;
|
|
1470
|
+
apiSecret;
|
|
1471
|
+
baseUrl;
|
|
1472
|
+
isTestnet;
|
|
1473
|
+
importedKeyPromise;
|
|
1474
|
+
constructor(apiKey, apiSecret, options) {
|
|
1475
|
+
if (!apiKey || !apiSecret) {
|
|
1476
|
+
throw new Error(this.getErrorMessagePrefix() + "API key and secret are required.");
|
|
1477
|
+
}
|
|
1478
|
+
this.apiKey = apiKey;
|
|
1479
|
+
this.apiSecret = apiSecret;
|
|
1480
|
+
this.isTestnet = options?.testnet ?? false;
|
|
1481
|
+
this.baseUrl = options?.baseUrl ?? this.getDefaultBaseUrl();
|
|
1482
|
+
this.importedKeyPromise = this.importKey();
|
|
1483
|
+
}
|
|
1484
|
+
get testnet() {
|
|
1485
|
+
return this.isTestnet;
|
|
1486
|
+
}
|
|
1487
|
+
getErrorMessagePrefix() {
|
|
1488
|
+
return "";
|
|
1489
|
+
}
|
|
1490
|
+
generateSignature(_params) {
|
|
1491
|
+
throw new Error("generateSignature must be implemented by subclass");
|
|
1492
|
+
}
|
|
1493
|
+
buildHeaders(_method, _path, _params) {
|
|
1494
|
+
return new Headers;
|
|
1495
|
+
}
|
|
1496
|
+
async importKey() {
|
|
1497
|
+
const encoder = new TextEncoder;
|
|
1498
|
+
return crypto.subtle.importKey("raw", encoder.encode(this.apiSecret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
1499
|
+
}
|
|
1500
|
+
async cryptoSign(data) {
|
|
1501
|
+
const encoder = new TextEncoder;
|
|
1502
|
+
const key = await this.importedKeyPromise;
|
|
1503
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
|
|
1504
|
+
return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1505
|
+
}
|
|
1506
|
+
async fetch(method, path, params) {
|
|
1507
|
+
const url = `${this.baseUrl}${path}`;
|
|
1508
|
+
const headers = this.buildHeaders(method, path, params);
|
|
1509
|
+
const body = params ? new URLSearchParams(params).toString() : undefined;
|
|
1510
|
+
const response = await fetch(url, {
|
|
1511
|
+
method,
|
|
1512
|
+
headers,
|
|
1513
|
+
body: method !== "GET" ? body : undefined
|
|
1514
|
+
});
|
|
1515
|
+
if (!response.ok) {
|
|
1516
|
+
const error = await response.text();
|
|
1517
|
+
throw new Error(`Exchange API error: ${response.status} - ${error}`);
|
|
1518
|
+
}
|
|
1519
|
+
return response.json();
|
|
1520
|
+
}
|
|
1521
|
+
setLeverage(_symbol, _leverage) {
|
|
1522
|
+
throw new Error("setLeverage must be implemented by subclass");
|
|
1523
|
+
}
|
|
1524
|
+
executeTrade(_params) {
|
|
1525
|
+
throw new Error("executeTrade must be implemented by subclass");
|
|
1526
|
+
}
|
|
1527
|
+
getAccountInfo() {
|
|
1528
|
+
throw new Error("getAccountInfo must be implemented by subclass");
|
|
1529
|
+
}
|
|
1530
|
+
getPositions(_symbol) {
|
|
1531
|
+
throw new Error("getPositions must be implemented by subclass");
|
|
1532
|
+
}
|
|
1533
|
+
async openLong(symbol, quantity, price, orderType = "MARKET") {
|
|
1534
|
+
return this.executeTrade({
|
|
1535
|
+
symbol,
|
|
1536
|
+
side: "BUY",
|
|
1537
|
+
orderType,
|
|
1538
|
+
quantity,
|
|
1539
|
+
price
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
async openShort(symbol, quantity, price, orderType = "MARKET") {
|
|
1543
|
+
return this.executeTrade({
|
|
1544
|
+
symbol,
|
|
1545
|
+
side: "SELL",
|
|
1546
|
+
orderType,
|
|
1547
|
+
quantity,
|
|
1548
|
+
price
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
async closeLong(symbol, quantity) {
|
|
1552
|
+
return this.executeTrade({
|
|
1553
|
+
symbol,
|
|
1554
|
+
side: "SELL",
|
|
1555
|
+
orderType: "MARKET",
|
|
1556
|
+
quantity,
|
|
1557
|
+
reduceOnly: true
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
async closeShort(symbol, quantity) {
|
|
1561
|
+
return this.executeTrade({
|
|
1562
|
+
symbol,
|
|
1563
|
+
side: "BUY",
|
|
1564
|
+
orderType: "MARKET",
|
|
1565
|
+
quantity,
|
|
1566
|
+
reduceOnly: true
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
// src/format-time.ts
|
|
1571
|
+
function formatRelativeTime(timestampMs, nowMs) {
|
|
1572
|
+
const now = nowMs ?? Date.now();
|
|
1573
|
+
const diffMs = now - timestampMs;
|
|
1574
|
+
if (diffMs < 0)
|
|
1575
|
+
return "just now";
|
|
1576
|
+
if (timestampMs === 0)
|
|
1577
|
+
return "\u2014";
|
|
1578
|
+
const seconds = Math.floor(diffMs / 1000);
|
|
1579
|
+
const minutes = Math.floor(seconds / 60);
|
|
1580
|
+
const hours = Math.floor(minutes / 60);
|
|
1581
|
+
const days = Math.floor(hours / 24);
|
|
1582
|
+
if (seconds < 60)
|
|
1583
|
+
return "< 1m ago";
|
|
1584
|
+
if (minutes < 60)
|
|
1585
|
+
return `${minutes}m ago`;
|
|
1586
|
+
if (hours < 24)
|
|
1587
|
+
return `${hours}h ago`;
|
|
1588
|
+
if (days < 30)
|
|
1589
|
+
return `${days}d ago`;
|
|
1590
|
+
return `> 30d ago`;
|
|
1591
|
+
}
|
|
1592
|
+
function formatDuration(durationMs) {
|
|
1593
|
+
const seconds = Math.floor(durationMs / 1000);
|
|
1594
|
+
const minutes = Math.floor(seconds / 60);
|
|
1595
|
+
const hours = Math.floor(minutes / 60);
|
|
1596
|
+
if (hours > 0) {
|
|
1597
|
+
const remainMin = minutes % 60;
|
|
1598
|
+
return remainMin > 0 ? `${hours}h ${remainMin}m` : `${hours}h`;
|
|
1599
|
+
}
|
|
1600
|
+
if (minutes > 0) {
|
|
1601
|
+
const remainSec = seconds % 60;
|
|
1602
|
+
return remainSec > 0 ? `${minutes}m ${remainSec}s` : `${minutes}m`;
|
|
1603
|
+
}
|
|
1604
|
+
return `${seconds}s`;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// src/formatters.ts
|
|
1608
|
+
function formatNumber(n) {
|
|
1609
|
+
if (!isFinite(n))
|
|
1610
|
+
return "\u2014";
|
|
1611
|
+
if (Math.abs(n) >= 1e9)
|
|
1612
|
+
return `${(n / 1e9).toFixed(1)}B`;
|
|
1613
|
+
if (Math.abs(n) >= 1e6)
|
|
1614
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
1615
|
+
if (Math.abs(n) >= 1000)
|
|
1616
|
+
return `${(n / 1000).toFixed(1)}K`;
|
|
1617
|
+
return n.toLocaleString();
|
|
1618
|
+
}
|
|
1619
|
+
function formatCurrency(n) {
|
|
1620
|
+
if (!isFinite(n))
|
|
1621
|
+
return "\u2014";
|
|
1622
|
+
const sign = n >= 0 ? "+" : "-";
|
|
1623
|
+
const abs = Math.abs(n);
|
|
1624
|
+
if (abs >= 1e6)
|
|
1625
|
+
return `${sign}$${(abs / 1e6).toFixed(2)}M`;
|
|
1626
|
+
if (abs >= 1000)
|
|
1627
|
+
return `${sign}$${(abs / 1000).toFixed(2)}K`;
|
|
1628
|
+
return `${sign}$${abs.toFixed(2)}`;
|
|
1629
|
+
}
|
|
1630
|
+
function formatCompactCurrency(n) {
|
|
1631
|
+
if (!isFinite(n))
|
|
1632
|
+
return "\u2014";
|
|
1633
|
+
const abs = Math.abs(n);
|
|
1634
|
+
const sign = n < 0 ? "-" : "";
|
|
1635
|
+
if (abs >= 1e6)
|
|
1636
|
+
return `${sign}$${(abs / 1e6).toFixed(2)}M`;
|
|
1637
|
+
if (abs >= 1000)
|
|
1638
|
+
return `${sign}$${(abs / 1000).toFixed(2)}K`;
|
|
1639
|
+
return `${sign}$${abs.toFixed(2)}`;
|
|
1640
|
+
}
|
|
1641
|
+
function formatDuration2(seconds) {
|
|
1642
|
+
if (!isFinite(seconds) || seconds < 0)
|
|
1643
|
+
return "\u2014";
|
|
1644
|
+
const d = Math.floor(seconds / 86400);
|
|
1645
|
+
const h = Math.floor(seconds % 86400 / 3600);
|
|
1646
|
+
const m = Math.floor(seconds % 3600 / 60);
|
|
1647
|
+
const s = Math.floor(seconds % 60);
|
|
1648
|
+
const parts = [];
|
|
1649
|
+
if (d > 0)
|
|
1650
|
+
parts.push(`${d}d`);
|
|
1651
|
+
if (h > 0 || d > 0)
|
|
1652
|
+
parts.push(`${h}h`);
|
|
1653
|
+
if (m > 0 || h > 0 || d > 0)
|
|
1654
|
+
parts.push(`${m}m`);
|
|
1655
|
+
parts.push(`${s}s`);
|
|
1656
|
+
return parts.join(" ");
|
|
1657
|
+
}
|
|
1658
|
+
function formatDurationCompact(seconds) {
|
|
1659
|
+
if (!isFinite(seconds) || seconds < 0)
|
|
1660
|
+
return "\u2014";
|
|
1661
|
+
const d = Math.floor(seconds / 86400);
|
|
1662
|
+
const h = Math.floor(seconds % 86400 / 3600);
|
|
1663
|
+
const m = Math.floor(seconds % 3600 / 60);
|
|
1664
|
+
if (d > 0)
|
|
1665
|
+
return `${d}d ${h}h`;
|
|
1666
|
+
if (h > 0)
|
|
1667
|
+
return `${h}h ${m}m`;
|
|
1668
|
+
if (m > 0)
|
|
1669
|
+
return `${m}m`;
|
|
1670
|
+
return `${Math.floor(seconds)}s`;
|
|
1671
|
+
}
|
|
1672
|
+
function formatTimestamp(iso) {
|
|
1673
|
+
try {
|
|
1674
|
+
const d = new Date(iso);
|
|
1675
|
+
if (isNaN(d.getTime()))
|
|
1676
|
+
return iso;
|
|
1677
|
+
return d.toLocaleTimeString("en-US", { hour12: false });
|
|
1678
|
+
} catch {
|
|
1679
|
+
return iso;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
function formatRelativeTime2(timestamp) {
|
|
1683
|
+
const now = Date.now();
|
|
1684
|
+
const diff = now - timestamp;
|
|
1685
|
+
if (diff < 0)
|
|
1686
|
+
return "just now";
|
|
1687
|
+
const seconds = Math.floor(diff / 1000);
|
|
1688
|
+
if (seconds < 60)
|
|
1689
|
+
return `${seconds}s ago`;
|
|
1690
|
+
const minutes = Math.floor(seconds / 60);
|
|
1691
|
+
if (minutes < 60)
|
|
1692
|
+
return `${minutes}m ago`;
|
|
1693
|
+
const hours = Math.floor(minutes / 60);
|
|
1694
|
+
if (hours < 24)
|
|
1695
|
+
return `${hours}h ago`;
|
|
1696
|
+
const days = Math.floor(hours / 24);
|
|
1697
|
+
return `${days}d ago`;
|
|
1698
|
+
}
|
|
1699
|
+
function formatPercent(n, decimals = 2) {
|
|
1700
|
+
if (!isFinite(n))
|
|
1701
|
+
return "\u2014";
|
|
1702
|
+
const sign = n >= 0 ? "+" : "";
|
|
1703
|
+
return `${sign}${n.toFixed(decimals)}%`;
|
|
1704
|
+
}
|
|
1705
|
+
function formatUptime(seconds) {
|
|
1706
|
+
return formatDuration2(seconds);
|
|
1707
|
+
}
|
|
1708
|
+
function formatLatency(ms) {
|
|
1709
|
+
if (!isFinite(ms))
|
|
1710
|
+
return "\u2014";
|
|
1711
|
+
if (ms < 1)
|
|
1712
|
+
return "<1ms";
|
|
1713
|
+
if (ms < 1000)
|
|
1714
|
+
return `${Math.round(ms)}ms`;
|
|
1715
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
1716
|
+
}
|
|
1717
|
+
function formatRequests(n) {
|
|
1718
|
+
return formatNumber(n);
|
|
1719
|
+
}
|
|
1720
|
+
function formatMemory(usedMB, limitMB) {
|
|
1721
|
+
const u = usedMB.toFixed(0);
|
|
1722
|
+
const l = limitMB.toFixed(0);
|
|
1723
|
+
return `${u}/${l} MB`;
|
|
1724
|
+
}
|
|
1725
|
+
function formatCpu(ms) {
|
|
1726
|
+
if (!isFinite(ms))
|
|
1727
|
+
return "\u2014";
|
|
1728
|
+
return `${ms.toFixed(1)}ms`;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// src/health.ts
|
|
1732
|
+
function healthCheck(options) {
|
|
1733
|
+
const result = {
|
|
1734
|
+
status: "ok",
|
|
1735
|
+
timestamp: new Date().toISOString()
|
|
1736
|
+
};
|
|
1737
|
+
if (options?.worker !== undefined && options?.worker !== null)
|
|
1738
|
+
result.service = options.worker;
|
|
1739
|
+
if (options?.version !== undefined && options?.version !== null)
|
|
1740
|
+
result.version = options.version;
|
|
1741
|
+
if (options?.details)
|
|
1742
|
+
result.details = options.details;
|
|
1743
|
+
return createSuccessResponse(result);
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// src/legal.ts
|
|
1747
|
+
var COPYRIGHT = `Copyright (C) ${new Date().getFullYear()} Hoox Team. All rights reserved.`;
|
|
1748
|
+
var TRADEMARKS = [
|
|
1749
|
+
"TradingView\xAE is a registered trademark of TradingView, Inc.",
|
|
1750
|
+
"Cloudflare\xAE is a registered trademark of Cloudflare, Inc."
|
|
1751
|
+
];
|
|
1752
|
+
var TRADEMARK_NOTICE = TRADEMARKS.join(`
|
|
1753
|
+
`);
|
|
1754
|
+
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.";
|
|
1755
|
+
var FULL_LEGAL_NOTICE = `${COPYRIGHT}
|
|
1756
|
+
|
|
1757
|
+
${TRADEMARK_NOTICE}
|
|
1758
|
+
|
|
1759
|
+
${DISCLAIMER}`;
|
|
1760
|
+
var DISCLAIMER_HEADER = "X-Disclaimer";
|
|
1761
|
+
|
|
1762
|
+
// src/types.ts
|
|
1763
|
+
import { z as z2 } from "zod";
|
|
1764
|
+
var TradeActionSchema = z2.enum([
|
|
1765
|
+
"LONG",
|
|
1766
|
+
"SHORT",
|
|
1767
|
+
"CLOSE_LONG",
|
|
1768
|
+
"CLOSE_SHORT"
|
|
1769
|
+
]);
|
|
1770
|
+
var WebhookPayloadSchema = z2.object({
|
|
1771
|
+
exchange: z2.string().min(1),
|
|
1772
|
+
action: TradeActionSchema,
|
|
1773
|
+
symbol: z2.string().min(1).max(20),
|
|
1774
|
+
quantity: z2.number().positive().finite(),
|
|
1775
|
+
price: z2.number().positive().finite().optional(),
|
|
1776
|
+
orderType: z2.string().optional(),
|
|
1777
|
+
leverage: z2.number().int().positive().optional(),
|
|
1778
|
+
test: z2.boolean().optional()
|
|
1779
|
+
}).strict();
|
|
1780
|
+
var TradeQueueMessageSchema = z2.object({
|
|
1781
|
+
requestId: z2.string().min(1).max(128),
|
|
1782
|
+
exchange: z2.string().min(1).max(64),
|
|
1783
|
+
action: TradeActionSchema,
|
|
1784
|
+
symbol: z2.string().min(1).max(32),
|
|
1785
|
+
quantity: z2.number().positive().finite(),
|
|
1786
|
+
price: z2.number().positive().finite().optional(),
|
|
1787
|
+
leverage: z2.number().int().positive().max(125).optional(),
|
|
1788
|
+
test: z2.boolean().optional(),
|
|
1789
|
+
queuedAt: z2.string().min(1).max(64)
|
|
1790
|
+
}).strict();
|
|
1791
|
+
var TradeSignalSchema = z2.object({
|
|
1792
|
+
id: z2.number().int().positive().optional(),
|
|
1793
|
+
source: z2.string().min(1),
|
|
1794
|
+
symbol: z2.string().min(1),
|
|
1795
|
+
action: TradeActionSchema,
|
|
1796
|
+
price: z2.number().positive().finite().optional(),
|
|
1797
|
+
quantity: z2.number().positive().finite(),
|
|
1798
|
+
leverage: z2.number().int().positive().optional(),
|
|
1799
|
+
status: z2.enum(["pending", "executed", "failed", "skipped"]).optional(),
|
|
1800
|
+
createdAt: z2.string().optional(),
|
|
1801
|
+
executedAt: z2.string().optional(),
|
|
1802
|
+
error: z2.string().optional()
|
|
1803
|
+
}).strict();
|
|
1804
|
+
var PositionSchema = z2.object({
|
|
1805
|
+
id: z2.string(),
|
|
1806
|
+
symbol: z2.string().min(1),
|
|
1807
|
+
side: TradeActionSchema,
|
|
1808
|
+
quantity: z2.number().positive(),
|
|
1809
|
+
entry_price: z2.number().positive(),
|
|
1810
|
+
current_price: z2.number().positive(),
|
|
1811
|
+
unrealized_pnl: z2.number(),
|
|
1812
|
+
timestamp: z2.number().int().positive()
|
|
1813
|
+
}).strict();
|
|
1814
|
+
var BalanceSchema = z2.object({
|
|
1815
|
+
asset: z2.string().min(1),
|
|
1816
|
+
free: z2.number().min(0),
|
|
1817
|
+
locked: z2.number().min(0),
|
|
1818
|
+
timestamp: z2.number().int().positive()
|
|
1819
|
+
}).strict();
|
|
1820
|
+
|
|
1821
|
+
// src/kvUtils.ts
|
|
1822
|
+
async function logKvTimestamp(env, prefix = "timestamp") {
|
|
1823
|
+
const timestamp = new Date().toISOString();
|
|
1824
|
+
const key = `${prefix}_${timestamp}`;
|
|
1825
|
+
try {
|
|
1826
|
+
await env.REPORT_KV.put(key, timestamp);
|
|
1827
|
+
} catch (error) {
|
|
1828
|
+
console.error(`Failed to log timestamp to KV: ${toError(error, "Unknown error")}`);
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
function headersToObject(headers) {
|
|
1832
|
+
const result = {};
|
|
1833
|
+
if (!headers)
|
|
1834
|
+
return result;
|
|
1835
|
+
try {
|
|
1836
|
+
headers.forEach((value, key) => {
|
|
1837
|
+
result[key] = value;
|
|
1838
|
+
});
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
console.error(`Error converting headers to object: ${toError(error, "Unknown error")}`);
|
|
1841
|
+
}
|
|
1842
|
+
return result;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
// src/queue-handler.ts
|
|
1846
|
+
function createQueueHandler(options) {
|
|
1847
|
+
const { maxRetries, backoffDelays, onMessage, onRetry, onDLQ, logger } = options;
|
|
1848
|
+
return async (batch) => {
|
|
1849
|
+
for (const msg of batch.messages) {
|
|
1850
|
+
const attemptNumber = msg.attempts || 0;
|
|
1851
|
+
const logId = `[${msg.id}]`;
|
|
1852
|
+
try {
|
|
1853
|
+
logger?.info(`${logId} Processing message (attempt ${attemptNumber + 1})`);
|
|
1854
|
+
await onMessage(msg.body, attemptNumber);
|
|
1855
|
+
logger?.info(`${logId} Message processed successfully`);
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1858
|
+
if (attemptNumber < maxRetries) {
|
|
1859
|
+
const delaySeconds = backoffDelays[attemptNumber] ?? backoffDelays[backoffDelays.length - 1];
|
|
1860
|
+
logger?.info(`${logId} Retrying in ${delaySeconds}s (attempt ${attemptNumber + 2}/${maxRetries + 1})`);
|
|
1861
|
+
await onRetry?.(msg.body, attemptNumber, errorMsg, delaySeconds);
|
|
1862
|
+
msg.retry({ delaySeconds });
|
|
1863
|
+
} else {
|
|
1864
|
+
logger?.error(`${logId} Max retries exceeded (${maxRetries + 1} attempts), moving to DLQ`);
|
|
1865
|
+
await onDLQ?.(msg.body, attemptNumber, errorMsg);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// src/middleware/auth.ts
|
|
1873
|
+
function timingSafeEqual(a, b) {
|
|
1874
|
+
const encoder = new TextEncoder;
|
|
1875
|
+
const aBuf = encoder.encode(a);
|
|
1876
|
+
const bBuf = encoder.encode(b);
|
|
1877
|
+
const len = Math.max(aBuf.length, bBuf.length);
|
|
1878
|
+
let result = aBuf.length === bBuf.length ? 0 : 1;
|
|
1879
|
+
for (let i = 0;i < len; i++) {
|
|
1880
|
+
const av = i < aBuf.length ? aBuf[i] : 0;
|
|
1881
|
+
const bv = i < bBuf.length ? bBuf[i] : 0;
|
|
1882
|
+
result |= av ^ bv;
|
|
1883
|
+
}
|
|
1884
|
+
return result === 0;
|
|
1885
|
+
}
|
|
1886
|
+
function resolveOperatorApiKey(env) {
|
|
1887
|
+
const preferred = env.OPERATOR_API_KEY;
|
|
1888
|
+
if (typeof preferred === "string" && preferred.length > 0) {
|
|
1889
|
+
return preferred;
|
|
1890
|
+
}
|
|
1891
|
+
const legacy = env.INTERNAL_API_KEY;
|
|
1892
|
+
if (typeof legacy === "string" && legacy.length > 0) {
|
|
1893
|
+
return legacy;
|
|
1894
|
+
}
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
async function requireAuth(request, env) {
|
|
1898
|
+
return requireOperatorAuth(request, env);
|
|
1899
|
+
}
|
|
1900
|
+
async function requireOperatorAuth(request, env) {
|
|
1901
|
+
const apiKey = resolveOperatorApiKey(env);
|
|
1902
|
+
if (!apiKey) {
|
|
1903
|
+
return new Response(JSON.stringify({
|
|
1904
|
+
error: "Operator API key not configured",
|
|
1905
|
+
hint: "Set OPERATOR_API_KEY (preferred) or INTERNAL_API_KEY on the Worker"
|
|
1906
|
+
}), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
1907
|
+
}
|
|
1908
|
+
const authHeader = request.headers.get("Authorization");
|
|
1909
|
+
const expectedHeader = `Bearer ${apiKey}`;
|
|
1910
|
+
if (!authHeader || !timingSafeEqual(authHeader, expectedHeader)) {
|
|
1911
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
1912
|
+
status: 401,
|
|
1913
|
+
headers: { "Content-Type": "application/json" }
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
return null;
|
|
1917
|
+
}
|
|
1918
|
+
function createOperatorAuthMiddleware() {
|
|
1919
|
+
return async (request, env, _ctx) => {
|
|
1920
|
+
const denied = await requireOperatorAuth(request, env);
|
|
1921
|
+
if (denied)
|
|
1922
|
+
return denied;
|
|
1923
|
+
return;
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
function normalizeKeyNames(keyName) {
|
|
1927
|
+
if (typeof keyName === "string") {
|
|
1928
|
+
return [keyName];
|
|
1929
|
+
}
|
|
1930
|
+
return [...keyName];
|
|
1931
|
+
}
|
|
1932
|
+
function collectInternalAuthKeys(env, keyNames) {
|
|
1933
|
+
const keys = [];
|
|
1934
|
+
for (const name of normalizeKeyNames(keyNames)) {
|
|
1935
|
+
const value = env[name];
|
|
1936
|
+
if (typeof value === "string" && value.length > 0) {
|
|
1937
|
+
keys.push(value);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
return keys;
|
|
1941
|
+
}
|
|
1942
|
+
function matchesAnyInternalAuthKey(providedKey, expectedKeys) {
|
|
1943
|
+
for (const expected of expectedKeys) {
|
|
1944
|
+
if (timingSafeEqual(providedKey, expected)) {
|
|
1945
|
+
return true;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
return false;
|
|
1949
|
+
}
|
|
1950
|
+
function internalAuthNotConfiguredResponse(keyNames) {
|
|
1951
|
+
const label = normalizeKeyNames(keyNames).join(" | ");
|
|
1952
|
+
return new Response(JSON.stringify({
|
|
1953
|
+
success: false,
|
|
1954
|
+
error: `Internal auth key(s) not configured: ${label}`
|
|
1955
|
+
}), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
1956
|
+
}
|
|
1957
|
+
function requireInternalAuth(request, env, keyName = "INTERNAL_KEY_BINDING") {
|
|
1958
|
+
const expectedKeys = collectInternalAuthKeys(env, keyName);
|
|
1959
|
+
if (expectedKeys.length === 0) {
|
|
1960
|
+
return internalAuthNotConfiguredResponse(keyName);
|
|
1961
|
+
}
|
|
1962
|
+
const providedKey = request.headers.get("X-Internal-Auth-Key");
|
|
1963
|
+
if (!providedKey || !matchesAnyInternalAuthKey(providedKey, expectedKeys)) {
|
|
1964
|
+
return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
1965
|
+
}
|
|
1966
|
+
return null;
|
|
1967
|
+
}
|
|
1968
|
+
function createInternalAuthMiddleware(keyName = "INTERNAL_KEY_BINDING") {
|
|
1969
|
+
return async (request, env, _ctx) => {
|
|
1970
|
+
const expectedKeys = collectInternalAuthKeys(env, keyName);
|
|
1971
|
+
if (expectedKeys.length === 0) {
|
|
1972
|
+
return internalAuthNotConfiguredResponse(keyName);
|
|
1973
|
+
}
|
|
1974
|
+
const providedKey = request.headers.get("X-Internal-Auth-Key");
|
|
1975
|
+
if (!providedKey || !matchesAnyInternalAuthKey(providedKey, expectedKeys)) {
|
|
1976
|
+
return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
1977
|
+
}
|
|
1978
|
+
return;
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
function checkInternalAuth(request, env, keyName = "INTERNAL_KEY_BINDING") {
|
|
1982
|
+
const expectedKeys = collectInternalAuthKeys(env, keyName);
|
|
1983
|
+
if (expectedKeys.length === 0) {
|
|
1984
|
+
return {
|
|
1985
|
+
authorized: false,
|
|
1986
|
+
error: `${normalizeKeyNames(keyName).join(" | ")} not configured`
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
const providedKey = request.headers.get("X-Internal-Auth-Key");
|
|
1990
|
+
if (!providedKey || !matchesAnyInternalAuthKey(providedKey, expectedKeys)) {
|
|
1991
|
+
return { authorized: false, error: "Unauthorized" };
|
|
1992
|
+
}
|
|
1993
|
+
return { authorized: true };
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
// src/service-bindings.ts
|
|
1997
|
+
var InternalAuthKeyFields = {
|
|
1998
|
+
DEFAULT: "INTERNAL_KEY_BINDING",
|
|
1999
|
+
D1_READ: "D1_READ_KEY_BINDING",
|
|
2000
|
+
D1_WRITE: "D1_WRITE_KEY_BINDING",
|
|
2001
|
+
TRADE_EXECUTE: "TRADE_EXECUTE_KEY_BINDING",
|
|
2002
|
+
TRADE_READ: "TRADE_READ_KEY_BINDING",
|
|
2003
|
+
TELEGRAM_ALERT: "TELEGRAM_INTERNAL_KEY_BINDING",
|
|
2004
|
+
WALLET_EXECUTE: "WALLET_EXECUTE_KEY_BINDING",
|
|
2005
|
+
AGENT: "AGENT_INTERNAL_KEY",
|
|
2006
|
+
D1_INTERNAL_ALIAS: "D1_INTERNAL_KEY",
|
|
2007
|
+
TRADE_INTERNAL_ALIAS: "TRADE_INTERNAL_KEY",
|
|
2008
|
+
TELEGRAM_INTERNAL_ALIAS: "TELEGRAM_INTERNAL_KEY"
|
|
2009
|
+
};
|
|
2010
|
+
var D1_READ_AUTH_KEY_FIELDS = [
|
|
2011
|
+
InternalAuthKeyFields.D1_READ,
|
|
2012
|
+
InternalAuthKeyFields.DEFAULT
|
|
2013
|
+
];
|
|
2014
|
+
var D1_WRITE_AUTH_KEY_FIELDS = [
|
|
2015
|
+
InternalAuthKeyFields.D1_WRITE,
|
|
2016
|
+
InternalAuthKeyFields.DEFAULT
|
|
2017
|
+
];
|
|
2018
|
+
var TRADE_EXECUTE_AUTH_KEY_FIELDS = [
|
|
2019
|
+
InternalAuthKeyFields.TRADE_EXECUTE,
|
|
2020
|
+
InternalAuthKeyFields.DEFAULT,
|
|
2021
|
+
InternalAuthKeyFields.AGENT
|
|
2022
|
+
];
|
|
2023
|
+
var TRADE_READ_AUTH_KEY_FIELDS = [
|
|
2024
|
+
InternalAuthKeyFields.TRADE_READ,
|
|
2025
|
+
InternalAuthKeyFields.DEFAULT
|
|
2026
|
+
];
|
|
2027
|
+
var TELEGRAM_ALERT_AUTH_KEY_FIELDS = [
|
|
2028
|
+
InternalAuthKeyFields.TELEGRAM_ALERT,
|
|
2029
|
+
InternalAuthKeyFields.DEFAULT
|
|
2030
|
+
];
|
|
2031
|
+
var WALLET_EXECUTE_AUTH_KEY_FIELDS = [
|
|
2032
|
+
InternalAuthKeyFields.WALLET_EXECUTE,
|
|
2033
|
+
InternalAuthKeyFields.DEFAULT
|
|
2034
|
+
];
|
|
2035
|
+
var DASHBOARD_D1_READ_AUTH_KEY_FIELDS = [
|
|
2036
|
+
InternalAuthKeyFields.D1_READ,
|
|
2037
|
+
InternalAuthKeyFields.D1_INTERNAL_ALIAS,
|
|
2038
|
+
InternalAuthKeyFields.DEFAULT
|
|
2039
|
+
];
|
|
2040
|
+
var DASHBOARD_TRADE_EXECUTE_AUTH_KEY_FIELDS = [
|
|
2041
|
+
InternalAuthKeyFields.TRADE_EXECUTE,
|
|
2042
|
+
InternalAuthKeyFields.TRADE_INTERNAL_ALIAS,
|
|
2043
|
+
InternalAuthKeyFields.DEFAULT
|
|
2044
|
+
];
|
|
2045
|
+
var DASHBOARD_TELEGRAM_ALERT_AUTH_KEY_FIELDS = [
|
|
2046
|
+
InternalAuthKeyFields.TELEGRAM_ALERT,
|
|
2047
|
+
InternalAuthKeyFields.TELEGRAM_INTERNAL_ALIAS,
|
|
2048
|
+
InternalAuthKeyFields.DEFAULT
|
|
2049
|
+
];
|
|
2050
|
+
function resolveInternalAuthKey(env, keyFields) {
|
|
2051
|
+
return collectInternalAuthKeys(env, keyFields)[0];
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
class ServiceAuthError extends Error {
|
|
2055
|
+
constructor(message) {
|
|
2056
|
+
super(message);
|
|
2057
|
+
this.name = "ServiceAuthError";
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
function authenticatedServiceFetch(binding, env, path, body, options) {
|
|
2061
|
+
const key = options?.internalKey ?? (options?.internalKeyFields ? resolveInternalAuthKey(env, options.internalKeyFields) : env.INTERNAL_KEY_BINDING);
|
|
2062
|
+
if (!key) {
|
|
2063
|
+
const label = options?.internalKeyFields ? Array.isArray(options.internalKeyFields) ? options.internalKeyFields.join(" | ") : options.internalKeyFields : "INTERNAL_KEY_BINDING";
|
|
2064
|
+
return Promise.reject(new ServiceAuthError(`${label} not configured`));
|
|
2065
|
+
}
|
|
2066
|
+
return serviceFetch(binding, path, body, {
|
|
2067
|
+
method: options?.method,
|
|
2068
|
+
timeout: options?.timeout,
|
|
2069
|
+
headers: {
|
|
2070
|
+
...options?.headers,
|
|
2071
|
+
"X-Internal-Auth-Key": key
|
|
2072
|
+
}
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
function serviceFetch(binding, path, body, options) {
|
|
2076
|
+
const timeout = options?.timeout ?? 30000;
|
|
2077
|
+
return binding.fetch(`http://internal${path}`, {
|
|
2078
|
+
method: options?.method ?? "POST",
|
|
2079
|
+
headers: {
|
|
2080
|
+
"Content-Type": "application/json",
|
|
2081
|
+
...options?.headers
|
|
2082
|
+
},
|
|
2083
|
+
body: body != null ? JSON.stringify(body) : undefined,
|
|
2084
|
+
signal: AbortSignal.timeout(timeout)
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// src/test-utils.ts
|
|
2089
|
+
function createMockKV(overrides) {
|
|
2090
|
+
return {
|
|
2091
|
+
get: async (_key) => null,
|
|
2092
|
+
put: async (_key, _value, _options) => {},
|
|
2093
|
+
delete: async (_key) => {},
|
|
2094
|
+
list: async (_options) => ({
|
|
2095
|
+
keys: []
|
|
2096
|
+
}),
|
|
2097
|
+
...overrides
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
function createMockR2(overrides) {
|
|
2101
|
+
return {
|
|
2102
|
+
put: async (key, _value, _options) => ({
|
|
2103
|
+
key
|
|
2104
|
+
}),
|
|
2105
|
+
get: async (_key) => null,
|
|
2106
|
+
delete: async (_key) => {},
|
|
2107
|
+
list: async (_options) => ({
|
|
2108
|
+
objects: []
|
|
2109
|
+
}),
|
|
2110
|
+
...overrides
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
function createMockPreparedStatement() {
|
|
2114
|
+
return {
|
|
2115
|
+
bind: (..._params) => createMockPreparedStatement(),
|
|
2116
|
+
run: async () => ({ success: true }),
|
|
2117
|
+
all: async () => ({ success: true, results: [] }),
|
|
2118
|
+
first: async () => null
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
function createMockD1(overrides) {
|
|
2122
|
+
return {
|
|
2123
|
+
prepare: (_sql) => createMockPreparedStatement(),
|
|
2124
|
+
dump: async () => new ArrayBuffer(0),
|
|
2125
|
+
batch: async (_statements) => [],
|
|
2126
|
+
exec: async (_sql) => ({ success: true }),
|
|
2127
|
+
...overrides
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
function createMockFetcher(response) {
|
|
2131
|
+
const defaultResponse = response ?? new Response("OK", { status: 200 });
|
|
2132
|
+
return {
|
|
2133
|
+
fetch: async (_request, _init) => defaultResponse
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
function createMockAnalyticsEngine() {
|
|
2137
|
+
return {
|
|
2138
|
+
writeDataPoint: (_data) => {}
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
function createMockAnalyticsEngineDataset() {
|
|
2142
|
+
return createMockAnalyticsEngine();
|
|
2143
|
+
}
|
|
2144
|
+
function createMockQueue() {
|
|
2145
|
+
return {
|
|
2146
|
+
send: async (_body, _options) => {},
|
|
2147
|
+
sendBatch: async (_messages) => {}
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
function createMockVectorizeIndex(overrides) {
|
|
2151
|
+
return {
|
|
2152
|
+
query: async (_vector, _options) => ({
|
|
2153
|
+
matches: []
|
|
2154
|
+
}),
|
|
2155
|
+
upsert: async (_vectors) => {},
|
|
2156
|
+
deleteByIds: async (_ids) => {},
|
|
2157
|
+
describe: async () => ({ dimension: 384, totalVectors: 0 }),
|
|
2158
|
+
...overrides
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
function createMockAi(overrides) {
|
|
2162
|
+
return {
|
|
2163
|
+
run: async (_model, _input) => ({}),
|
|
2164
|
+
...overrides
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
function createMockExecutionContext(overrides) {
|
|
2168
|
+
return {
|
|
2169
|
+
waitUntil: (_promise) => {},
|
|
2170
|
+
passThroughOnException: () => {},
|
|
2171
|
+
...overrides
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
function createMockSecretsStore(overrides) {
|
|
2175
|
+
return {
|
|
2176
|
+
get: async (_key) => null,
|
|
2177
|
+
put: async (_key, _value) => {},
|
|
2178
|
+
delete: async (_key) => {},
|
|
2179
|
+
...overrides
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
function createMockEnv(overrides) {
|
|
2183
|
+
return overrides;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
// src/path-utils.ts
|
|
2187
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2188
|
+
import { homedir as homedir2 } from "os";
|
|
2189
|
+
import { dirname, join as join3, resolve } from "path";
|
|
2190
|
+
function createHooxPath(path) {
|
|
2191
|
+
return path;
|
|
2192
|
+
}
|
|
2193
|
+
function getHooxHome() {
|
|
2194
|
+
try {
|
|
2195
|
+
const override = process.env.HOOX_HOME?.trim();
|
|
2196
|
+
if (override) {
|
|
2197
|
+
return createHooxPath(resolve(override));
|
|
2198
|
+
}
|
|
2199
|
+
const home = homedir2();
|
|
2200
|
+
if (!home || home.length === 0) {
|
|
2201
|
+
return createHooxPath(resolve(process.cwd(), ".hoox"));
|
|
2202
|
+
}
|
|
2203
|
+
return createHooxPath(join3(home, ".hoox"));
|
|
2204
|
+
} catch {
|
|
2205
|
+
return createHooxPath(resolve(process.cwd(), ".hoox"));
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
function isHooxSetupRoot(dir) {
|
|
2209
|
+
if (!dir)
|
|
2210
|
+
return false;
|
|
2211
|
+
try {
|
|
2212
|
+
const root = resolve(dir);
|
|
2213
|
+
return existsSync3(join3(root, "wrangler.jsonc")) && existsSync3(join3(root, "packages", "cli", "package.json"));
|
|
2214
|
+
} catch {
|
|
2215
|
+
return false;
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
function findHooxSetupRoot(startDir = process.cwd()) {
|
|
2219
|
+
let dir = resolve(startDir);
|
|
2220
|
+
for (;; ) {
|
|
2221
|
+
if (isHooxSetupRoot(dir))
|
|
2222
|
+
return dir;
|
|
2223
|
+
const parent = dirname(dir);
|
|
2224
|
+
if (parent === dir)
|
|
2225
|
+
return null;
|
|
2226
|
+
dir = parent;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
function resolveHooxRuntimeRoot(options) {
|
|
2230
|
+
const env = options?.env ?? process.env;
|
|
2231
|
+
const cwd = resolve(options?.cwd ?? process.cwd());
|
|
2232
|
+
const globalRepo = getHooxRepoPath();
|
|
2233
|
+
const envRepo = env.HOOX_REPO?.trim();
|
|
2234
|
+
if (envRepo) {
|
|
2235
|
+
const resolved = resolve(envRepo);
|
|
2236
|
+
if (isHooxSetupRoot(resolved)) {
|
|
2237
|
+
return {
|
|
2238
|
+
root: resolved,
|
|
2239
|
+
source: "env",
|
|
2240
|
+
checked: {
|
|
2241
|
+
env: resolved,
|
|
2242
|
+
cwd: findHooxSetupRoot(cwd),
|
|
2243
|
+
global: globalRepo
|
|
2244
|
+
}
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
return {
|
|
2248
|
+
root: null,
|
|
2249
|
+
source: "env",
|
|
2250
|
+
checked: {
|
|
2251
|
+
env: resolved,
|
|
2252
|
+
cwd: findHooxSetupRoot(cwd),
|
|
2253
|
+
global: globalRepo
|
|
2254
|
+
}
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
const local = findHooxSetupRoot(cwd);
|
|
2258
|
+
if (local) {
|
|
2259
|
+
return {
|
|
2260
|
+
root: local,
|
|
2261
|
+
source: "cwd",
|
|
2262
|
+
checked: { cwd: local, global: globalRepo }
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
if (isHooxSetupRoot(globalRepo)) {
|
|
2266
|
+
return {
|
|
2267
|
+
root: globalRepo,
|
|
2268
|
+
source: "global",
|
|
2269
|
+
checked: { cwd: null, global: globalRepo }
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
return {
|
|
2273
|
+
root: null,
|
|
2274
|
+
source: "none",
|
|
2275
|
+
checked: { cwd: null, global: globalRepo }
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
function getTuiEntryCandidates(runtimeRoot) {
|
|
2279
|
+
const root = resolve(runtimeRoot);
|
|
2280
|
+
return [
|
|
2281
|
+
join3(root, "packages", "tui", "src", "main.tsx"),
|
|
2282
|
+
join3(root, "packages", "tui", "dist", "main.js"),
|
|
2283
|
+
join3(root, "packages", "tui", "src", "main.ts")
|
|
2284
|
+
];
|
|
2285
|
+
}
|
|
2286
|
+
function resolveHooxPath(relativePath) {
|
|
2287
|
+
if (!relativePath || typeof relativePath !== "string") {
|
|
2288
|
+
throw new Error("relativePath must be a non-empty string");
|
|
2289
|
+
}
|
|
2290
|
+
if (relativePath.includes("..")) {
|
|
2291
|
+
throw new Error(`Path traversal detected in relativePath: "${relativePath}"`);
|
|
2292
|
+
}
|
|
2293
|
+
const normalizedPath = relativePath.replace(/\\/g, "/");
|
|
2294
|
+
const hooxHome = getHooxHome();
|
|
2295
|
+
const fullPath = join3(hooxHome, normalizedPath);
|
|
2296
|
+
const resolvedPath = resolve(fullPath);
|
|
2297
|
+
const resolvedHome = resolve(hooxHome);
|
|
2298
|
+
if (!resolvedPath.startsWith(resolvedHome)) {
|
|
2299
|
+
throw new Error(`Resolved path "${resolvedPath}" is outside Hoox home directory`);
|
|
2300
|
+
}
|
|
2301
|
+
return createHooxPath(resolvedPath);
|
|
2302
|
+
}
|
|
2303
|
+
function isWithinHooxHome(path) {
|
|
2304
|
+
try {
|
|
2305
|
+
const hooxHome = getHooxHome();
|
|
2306
|
+
const resolvedPath = resolve(path);
|
|
2307
|
+
const resolvedHome = resolve(hooxHome);
|
|
2308
|
+
return resolvedPath.startsWith(resolvedHome);
|
|
2309
|
+
} catch {
|
|
2310
|
+
return false;
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
function getRelativeHooxPath(absolutePath) {
|
|
2314
|
+
try {
|
|
2315
|
+
if (!isWithinHooxHome(absolutePath)) {
|
|
2316
|
+
return null;
|
|
2317
|
+
}
|
|
2318
|
+
const hooxHome = getHooxHome();
|
|
2319
|
+
const resolvedPath = resolve(absolutePath);
|
|
2320
|
+
const resolvedHome = resolve(hooxHome);
|
|
2321
|
+
const relativePath = resolvedPath.slice(resolvedHome.length);
|
|
2322
|
+
return relativePath.startsWith("/") || relativePath.startsWith("\\") ? relativePath.slice(1) : relativePath;
|
|
2323
|
+
} catch {
|
|
2324
|
+
return null;
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
function getHooxRepoPath() {
|
|
2328
|
+
return resolveHooxPath("repo");
|
|
2329
|
+
}
|
|
2330
|
+
function getHooxConfigDir() {
|
|
2331
|
+
return resolveHooxPath("config");
|
|
2332
|
+
}
|
|
2333
|
+
function getHooxDataDir() {
|
|
2334
|
+
return resolveHooxPath("data");
|
|
2335
|
+
}
|
|
2336
|
+
function getHooxWranglerPath() {
|
|
2337
|
+
return resolveHooxPath("config/wrangler.jsonc");
|
|
2338
|
+
}
|
|
2339
|
+
function getHooxStatePath() {
|
|
2340
|
+
return resolveHooxPath("data/state.json");
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// src/session.ts
|
|
2344
|
+
import { z as z3 } from "zod";
|
|
2345
|
+
var SessionStateFileSchema = z3.object({
|
|
2346
|
+
activeView: z3.string().optional(),
|
|
2347
|
+
sidebarExpanded: z3.boolean().optional(),
|
|
2348
|
+
windowSize: z3.object({
|
|
2349
|
+
cols: z3.number().int().positive(),
|
|
2350
|
+
rows: z3.number().int().positive()
|
|
2351
|
+
}).optional(),
|
|
2352
|
+
lastData: z3.number().nonnegative().optional(),
|
|
2353
|
+
savedAt: z3.string().optional()
|
|
2354
|
+
}).strict();
|
|
2355
|
+
var TUI_STATE_DIR = ".tui-state";
|
|
2356
|
+
function getSessionFilePath() {
|
|
2357
|
+
try {
|
|
2358
|
+
const hooxHome = getHooxHome();
|
|
2359
|
+
return `${hooxHome}/${TUI_STATE_DIR}/session.json`;
|
|
2360
|
+
} catch {
|
|
2361
|
+
return `${process.cwd()}/${TUI_STATE_DIR}/session.json`;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
var DEFAULT_SESSION = {
|
|
2365
|
+
activeView: "dashboard",
|
|
2366
|
+
sidebarExpanded: true,
|
|
2367
|
+
windowSize: { cols: 80, rows: 24 },
|
|
2368
|
+
lastData: 0,
|
|
2369
|
+
savedAt: new Date().toISOString()
|
|
2370
|
+
};
|
|
2371
|
+
async function saveSession(activeView, sidebarExpanded, windowSize, lastData) {
|
|
2372
|
+
try {
|
|
2373
|
+
const session = {
|
|
2374
|
+
activeView,
|
|
2375
|
+
sidebarExpanded,
|
|
2376
|
+
windowSize,
|
|
2377
|
+
lastData,
|
|
2378
|
+
savedAt: new Date().toISOString()
|
|
2379
|
+
};
|
|
2380
|
+
await Bun.write(getSessionFilePath(), JSON.stringify(session, null, 2));
|
|
2381
|
+
} catch (error) {
|
|
2382
|
+
console.error("[session] Failed to save session:", error.message);
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
async function restoreSession() {
|
|
2386
|
+
try {
|
|
2387
|
+
const file = Bun.file(getSessionFilePath());
|
|
2388
|
+
const exists = await file.exists();
|
|
2389
|
+
if (!exists)
|
|
2390
|
+
return { ...DEFAULT_SESSION };
|
|
2391
|
+
const raw = await file.text();
|
|
2392
|
+
const result = SessionStateFileSchema.safeParse(JSON.parse(raw));
|
|
2393
|
+
const parsed = result.success ? result.data : {};
|
|
2394
|
+
return {
|
|
2395
|
+
activeView: validateViewId(parsed.activeView) ? parsed.activeView : DEFAULT_SESSION.activeView,
|
|
2396
|
+
sidebarExpanded: typeof parsed.sidebarExpanded === "boolean" ? parsed.sidebarExpanded : DEFAULT_SESSION.sidebarExpanded,
|
|
2397
|
+
windowSize: parsed.windowSize ?? DEFAULT_SESSION.windowSize,
|
|
2398
|
+
lastData: typeof parsed.lastData === "number" ? parsed.lastData : DEFAULT_SESSION.lastData,
|
|
2399
|
+
savedAt: parsed.savedAt ?? DEFAULT_SESSION.savedAt
|
|
2400
|
+
};
|
|
2401
|
+
} catch {
|
|
2402
|
+
return { ...DEFAULT_SESSION };
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
var VALID_VIEW_IDS = new Set([
|
|
2406
|
+
"dashboard",
|
|
2407
|
+
"workers",
|
|
2408
|
+
"worker-detail",
|
|
2409
|
+
"trade-monitor",
|
|
2410
|
+
"logs-viewer",
|
|
2411
|
+
"service-manager",
|
|
2412
|
+
"config-editor",
|
|
2413
|
+
"setup-wizard",
|
|
2414
|
+
"settings",
|
|
2415
|
+
"queue-depth",
|
|
2416
|
+
"kv-viewer",
|
|
2417
|
+
"secrets-viewer",
|
|
2418
|
+
"db-query",
|
|
2419
|
+
"ai-chat",
|
|
2420
|
+
"edge-topology",
|
|
2421
|
+
"worker-settings"
|
|
2422
|
+
]);
|
|
2423
|
+
function validateViewId(id) {
|
|
2424
|
+
return typeof id === "string" && VALID_VIEW_IDS.has(id);
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
// src/wizard/presets.ts
|
|
2428
|
+
var PRESETS = [
|
|
2429
|
+
{
|
|
2430
|
+
name: "minimal",
|
|
2431
|
+
label: "Minimal",
|
|
2432
|
+
description: "Gateway + D1 database \u2014 webhook processing only",
|
|
2433
|
+
workers: ["hoox", "d1-worker", "analytics-worker"],
|
|
2434
|
+
integrations: []
|
|
2435
|
+
},
|
|
2436
|
+
{
|
|
2437
|
+
name: "standard",
|
|
2438
|
+
label: "Standard",
|
|
2439
|
+
description: "Trading + analytics + Telegram notifications",
|
|
2440
|
+
workers: [
|
|
2441
|
+
"hoox",
|
|
2442
|
+
"d1-worker",
|
|
2443
|
+
"trade-worker",
|
|
2444
|
+
"analytics-worker",
|
|
2445
|
+
"telegram-worker"
|
|
2446
|
+
],
|
|
2447
|
+
integrations: ["binance", "telegram"]
|
|
2448
|
+
},
|
|
2449
|
+
{
|
|
2450
|
+
name: "full",
|
|
2451
|
+
label: "Full",
|
|
2452
|
+
description: "All workers + AI agent + DeFi + email + PYNE",
|
|
2453
|
+
workers: [
|
|
2454
|
+
"hoox",
|
|
2455
|
+
"d1-worker",
|
|
2456
|
+
"trade-worker",
|
|
2457
|
+
"agent-worker",
|
|
2458
|
+
"telegram-worker",
|
|
2459
|
+
"analytics-worker",
|
|
2460
|
+
"email-worker",
|
|
2461
|
+
"web3-wallet-worker",
|
|
2462
|
+
"pyne-worker"
|
|
2463
|
+
],
|
|
2464
|
+
integrations: [
|
|
2465
|
+
"binance",
|
|
2466
|
+
"bybit",
|
|
2467
|
+
"mexc",
|
|
2468
|
+
"telegram",
|
|
2469
|
+
"openai",
|
|
2470
|
+
"wallet",
|
|
2471
|
+
"pyne"
|
|
2472
|
+
]
|
|
2473
|
+
}
|
|
2474
|
+
];
|
|
2475
|
+
var WORKER_DEPENDENCIES = {
|
|
2476
|
+
"trade-worker": ["d1-worker"],
|
|
2477
|
+
"agent-worker": ["d1-worker"],
|
|
2478
|
+
"email-worker": ["d1-worker"],
|
|
2479
|
+
"analytics-worker": ["d1-worker"],
|
|
2480
|
+
"web3-wallet-worker": ["d1-worker", "hoox"],
|
|
2481
|
+
"pyne-worker": ["trade-worker"]
|
|
2482
|
+
};
|
|
2483
|
+
function resolveDependencies(selected) {
|
|
2484
|
+
const result = new Set(selected);
|
|
2485
|
+
let changed = true;
|
|
2486
|
+
while (changed) {
|
|
2487
|
+
changed = false;
|
|
2488
|
+
for (const worker of [...result]) {
|
|
2489
|
+
const deps = WORKER_DEPENDENCIES[worker];
|
|
2490
|
+
if (deps) {
|
|
2491
|
+
for (const dep of deps) {
|
|
2492
|
+
if (!result.has(dep)) {
|
|
2493
|
+
result.add(dep);
|
|
2494
|
+
changed = true;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
return [...result];
|
|
2501
|
+
}
|
|
2502
|
+
var INTEGRATIONS = [
|
|
2503
|
+
{
|
|
2504
|
+
key: "binance",
|
|
2505
|
+
label: "Binance Exchange",
|
|
2506
|
+
workerName: "trade-worker",
|
|
2507
|
+
secrets: {
|
|
2508
|
+
BINANCE_KEY_BINDING: "Binance API Key",
|
|
2509
|
+
BINANCE_SECRET_BINDING: "Binance API Secret"
|
|
2510
|
+
}
|
|
2511
|
+
},
|
|
2512
|
+
{
|
|
2513
|
+
key: "mexc",
|
|
2514
|
+
label: "MEXC Exchange",
|
|
2515
|
+
workerName: "trade-worker",
|
|
2516
|
+
secrets: {
|
|
2517
|
+
MEXC_KEY_BINDING: "MEXC API Key",
|
|
2518
|
+
MEXC_SECRET_BINDING: "MEXC API Secret"
|
|
2519
|
+
}
|
|
2520
|
+
},
|
|
2521
|
+
{
|
|
2522
|
+
key: "bybit",
|
|
2523
|
+
label: "Bybit Exchange",
|
|
2524
|
+
workerName: "trade-worker",
|
|
2525
|
+
secrets: {
|
|
2526
|
+
BYBIT_KEY_BINDING: "Bybit API Key",
|
|
2527
|
+
BYBIT_SECRET_BINDING: "Bybit API Secret"
|
|
2528
|
+
}
|
|
2529
|
+
},
|
|
2530
|
+
{
|
|
2531
|
+
key: "wallet",
|
|
2532
|
+
label: "Web3 Wallet (on-chain execution)",
|
|
2533
|
+
workerName: "web3-wallet-worker",
|
|
2534
|
+
secrets: {
|
|
2535
|
+
WALLET_MNEMONIC_SECRET: "Wallet Mnemonic Phrase",
|
|
2536
|
+
WALLET_PK_SECRET: "Wallet Private Key"
|
|
2537
|
+
}
|
|
2538
|
+
},
|
|
2539
|
+
{
|
|
2540
|
+
key: "email",
|
|
2541
|
+
label: "Email Signal Parsing",
|
|
2542
|
+
workerName: "email-worker",
|
|
2543
|
+
secrets: {
|
|
2544
|
+
INTERNAL_KEY_BINDING: "Internal Auth Key",
|
|
2545
|
+
EMAIL_HOST_BINDING: "Email Host (IMAP server)",
|
|
2546
|
+
EMAIL_USER_BINDING: "Email Username",
|
|
2547
|
+
EMAIL_PASS_BINDING: "Email Password"
|
|
2548
|
+
},
|
|
2549
|
+
vars: { USE_IMAP: "false" }
|
|
2550
|
+
},
|
|
2551
|
+
{
|
|
2552
|
+
key: "telegram",
|
|
2553
|
+
label: "Telegram Notifications",
|
|
2554
|
+
workerName: "telegram-worker",
|
|
2555
|
+
secrets: {
|
|
2556
|
+
TG_BOT_TOKEN_BINDING: "Telegram Bot Token"
|
|
2557
|
+
}
|
|
2558
|
+
},
|
|
2559
|
+
{
|
|
2560
|
+
key: "openai",
|
|
2561
|
+
label: "OpenAI (AI Agent)",
|
|
2562
|
+
workerName: "agent-worker",
|
|
2563
|
+
secrets: {
|
|
2564
|
+
AGENT_INTERNAL_KEY: "OpenAI API Key"
|
|
2565
|
+
}
|
|
2566
|
+
},
|
|
2567
|
+
{
|
|
2568
|
+
key: "anthropic",
|
|
2569
|
+
label: "Anthropic (AI Agent)",
|
|
2570
|
+
workerName: "agent-worker",
|
|
2571
|
+
secrets: {
|
|
2572
|
+
AGENT_INTERNAL_KEY: "Anthropic API Key"
|
|
2573
|
+
}
|
|
2574
|
+
},
|
|
2575
|
+
{
|
|
2576
|
+
key: "google-ai",
|
|
2577
|
+
label: "Google AI (AI Agent)",
|
|
2578
|
+
workerName: "agent-worker",
|
|
2579
|
+
secrets: {
|
|
2580
|
+
AGENT_INTERNAL_KEY: "Google AI API Key"
|
|
2581
|
+
}
|
|
2582
|
+
},
|
|
2583
|
+
{
|
|
2584
|
+
key: "home-assistant",
|
|
2585
|
+
label: "Home Assistant (Smart Home)",
|
|
2586
|
+
workerName: "hoox",
|
|
2587
|
+
secrets: {
|
|
2588
|
+
HA_TOKEN_BINDING: "Home Assistant Token"
|
|
2589
|
+
}
|
|
2590
|
+
},
|
|
2591
|
+
{
|
|
2592
|
+
key: "pyne",
|
|
2593
|
+
label: "PYNE (Pine Script edge evaluate)",
|
|
2594
|
+
workerName: "pyne-worker",
|
|
2595
|
+
secrets: {
|
|
2596
|
+
API_KEY: "PYNE evaluate API key (X-API-Key)",
|
|
2597
|
+
ALERT_WEBHOOK_URL: "Default alert() webhook URL (optional)"
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
];
|
|
2601
|
+
var BASE_WORKERS = {
|
|
2602
|
+
"d1-worker": {
|
|
2603
|
+
enabled: true,
|
|
2604
|
+
path: "workers/d1-worker",
|
|
2605
|
+
vars: { database_name: "hoox-db" }
|
|
2606
|
+
},
|
|
2607
|
+
hoox: { enabled: true, path: "workers/hoox-worker", vars: {} },
|
|
2608
|
+
"agent-worker": { enabled: true, path: "workers/agent-worker", vars: {} },
|
|
2609
|
+
"analytics-worker": {
|
|
2610
|
+
enabled: true,
|
|
2611
|
+
path: "workers/analytics-worker",
|
|
2612
|
+
vars: {}
|
|
2613
|
+
},
|
|
2614
|
+
"pyne-worker": {
|
|
2615
|
+
enabled: true,
|
|
2616
|
+
path: "workers/pyne-worker",
|
|
2617
|
+
vars: {}
|
|
2618
|
+
}
|
|
2619
|
+
};
|
|
2620
|
+
var BASE_SECRETS = {
|
|
2621
|
+
hoox: ["WEBHOOK_API_KEY_BINDING"],
|
|
2622
|
+
"agent-worker": ["AGENT_INTERNAL_KEY", "INTERNAL_KEY_BINDING"],
|
|
2623
|
+
"analytics-worker": ["CLOUDFLARE_API_TOKEN"]
|
|
2624
|
+
};
|
|
2625
|
+
|
|
2626
|
+
// src/wizard/engine.ts
|
|
2627
|
+
var STEPS = [
|
|
2628
|
+
{
|
|
2629
|
+
id: "PREREQUISITES",
|
|
2630
|
+
label: "System Prerequisites",
|
|
2631
|
+
canGoBack: false,
|
|
2632
|
+
optional: false,
|
|
2633
|
+
validate: validatePrerequisites
|
|
2634
|
+
},
|
|
2635
|
+
{
|
|
2636
|
+
id: "CLOUDFLARE_CONFIG",
|
|
2637
|
+
label: "Cloudflare Configuration",
|
|
2638
|
+
canGoBack: true,
|
|
2639
|
+
optional: false,
|
|
2640
|
+
validate: validateCloudflareConfig
|
|
2641
|
+
},
|
|
2642
|
+
{
|
|
2643
|
+
id: "WORKER_SELECTION",
|
|
2644
|
+
label: "Worker Selection",
|
|
2645
|
+
canGoBack: true,
|
|
2646
|
+
optional: false,
|
|
2647
|
+
validate: validateWorkerSelection
|
|
2648
|
+
},
|
|
2649
|
+
{
|
|
2650
|
+
id: "PROVISIONING",
|
|
2651
|
+
label: "Infrastructure Provisioning",
|
|
2652
|
+
canGoBack: true,
|
|
2653
|
+
optional: true,
|
|
2654
|
+
validate: () => []
|
|
2655
|
+
},
|
|
2656
|
+
{
|
|
2657
|
+
id: "SECRETS",
|
|
2658
|
+
label: "Secrets Configuration",
|
|
2659
|
+
canGoBack: true,
|
|
2660
|
+
optional: false,
|
|
2661
|
+
validate: validateSecrets
|
|
2662
|
+
},
|
|
2663
|
+
{
|
|
2664
|
+
id: "CONFIG_WRITE",
|
|
2665
|
+
label: "Configuration Write",
|
|
2666
|
+
canGoBack: true,
|
|
2667
|
+
optional: false,
|
|
2668
|
+
validate: () => []
|
|
2669
|
+
},
|
|
2670
|
+
{
|
|
2671
|
+
id: "DEPLOY",
|
|
2672
|
+
label: "Deploy",
|
|
2673
|
+
canGoBack: false,
|
|
2674
|
+
optional: true,
|
|
2675
|
+
validate: () => []
|
|
2676
|
+
},
|
|
2677
|
+
{
|
|
2678
|
+
id: "DONE",
|
|
2679
|
+
label: "Complete",
|
|
2680
|
+
canGoBack: false,
|
|
2681
|
+
optional: false,
|
|
2682
|
+
validate: () => []
|
|
2683
|
+
}
|
|
2684
|
+
];
|
|
2685
|
+
var STEP_ORDER = STEPS.map((s) => s.id);
|
|
2686
|
+
function createInitialState() {
|
|
2687
|
+
return {
|
|
2688
|
+
step: "PREREQUISITES",
|
|
2689
|
+
completedSteps: [],
|
|
2690
|
+
selectedWorkers: [],
|
|
2691
|
+
selectedIntegrations: [],
|
|
2692
|
+
secrets: {},
|
|
2693
|
+
startedAt: Date.now(),
|
|
2694
|
+
updatedAt: Date.now()
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
2697
|
+
function validatePrerequisites(_state, input) {
|
|
2698
|
+
const errors = [];
|
|
2699
|
+
if (!input.checksPassed) {
|
|
2700
|
+
errors.push("System prerequisite checks must pass before continuing");
|
|
2701
|
+
}
|
|
2702
|
+
return errors;
|
|
2703
|
+
}
|
|
2704
|
+
function validateCloudflareConfig(_state, input) {
|
|
2705
|
+
const errors = [];
|
|
2706
|
+
const token = input.apiToken;
|
|
2707
|
+
const accountId = input.accountId;
|
|
2708
|
+
if (!token || String(token).trim().length === 0) {
|
|
2709
|
+
errors.push("Cloudflare API token is required");
|
|
2710
|
+
}
|
|
2711
|
+
if (!accountId || String(accountId).trim().length === 0) {
|
|
2712
|
+
errors.push("Cloudflare Account ID is required");
|
|
2713
|
+
}
|
|
2714
|
+
return errors;
|
|
2715
|
+
}
|
|
2716
|
+
function validateWorkerSelection(_state, input) {
|
|
2717
|
+
const errors = [];
|
|
2718
|
+
const preset = input.preset;
|
|
2719
|
+
const customWorkers = input.workers;
|
|
2720
|
+
if (preset === "custom" && (!customWorkers || customWorkers.length === 0)) {
|
|
2721
|
+
errors.push("At least one worker must be selected");
|
|
2722
|
+
}
|
|
2723
|
+
return errors;
|
|
2724
|
+
}
|
|
2725
|
+
function validateSecrets(state, input) {
|
|
2726
|
+
const errors = [];
|
|
2727
|
+
const secrets = input.secrets;
|
|
2728
|
+
if (!secrets || Object.keys(secrets).length === 0) {
|
|
2729
|
+
return [];
|
|
2730
|
+
}
|
|
2731
|
+
for (const integrationKey of state.selectedIntegrations) {
|
|
2732
|
+
const integration = INTEGRATIONS.find((i) => i.key === integrationKey);
|
|
2733
|
+
if (!integration)
|
|
2734
|
+
continue;
|
|
2735
|
+
const integrationSecrets = secrets[integrationKey];
|
|
2736
|
+
if (integrationSecrets) {
|
|
2737
|
+
for (const secretName of Object.keys(integration.secrets)) {
|
|
2738
|
+
const val = integrationSecrets[secretName];
|
|
2739
|
+
if (!val || String(val).trim().length === 0) {
|
|
2740
|
+
errors.push(`Secret "${integration.secrets[secretName]}" is required for ${integration.label}`);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
return errors;
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
class WizardEngine {
|
|
2749
|
+
state;
|
|
2750
|
+
constructor(initialState) {
|
|
2751
|
+
this.state = { ...createInitialState(), ...initialState };
|
|
2752
|
+
}
|
|
2753
|
+
getState() {
|
|
2754
|
+
return { ...this.state };
|
|
2755
|
+
}
|
|
2756
|
+
getCurrentStep() {
|
|
2757
|
+
return STEPS.find((s) => s.id === this.state.step) ?? STEPS[0];
|
|
2758
|
+
}
|
|
2759
|
+
getCompletedSteps() {
|
|
2760
|
+
return [...this.state.completedSteps];
|
|
2761
|
+
}
|
|
2762
|
+
canProceed() {
|
|
2763
|
+
const step = this.getCurrentStep();
|
|
2764
|
+
return step.optional || this.state.completedSteps.includes(step.id);
|
|
2765
|
+
}
|
|
2766
|
+
canGoBack() {
|
|
2767
|
+
return this.getCurrentStep().canGoBack;
|
|
2768
|
+
}
|
|
2769
|
+
execute(input) {
|
|
2770
|
+
const currentStep = this.getCurrentStep();
|
|
2771
|
+
const errors = currentStep.validate(this.state, input);
|
|
2772
|
+
if (errors.length > 0) {
|
|
2773
|
+
return errors;
|
|
2774
|
+
}
|
|
2775
|
+
this.applyInput(input);
|
|
2776
|
+
if (!this.state.completedSteps.includes(this.state.step)) {
|
|
2777
|
+
this.state.completedSteps.push(this.state.step);
|
|
2778
|
+
}
|
|
2779
|
+
const currentIdx = STEP_ORDER.indexOf(this.state.step);
|
|
2780
|
+
if (currentIdx < STEP_ORDER.length - 1) {
|
|
2781
|
+
this.state.step = STEP_ORDER[currentIdx + 1];
|
|
2782
|
+
}
|
|
2783
|
+
this.state.updatedAt = Date.now();
|
|
2784
|
+
return [];
|
|
2785
|
+
}
|
|
2786
|
+
goBack() {
|
|
2787
|
+
if (!this.canGoBack())
|
|
2788
|
+
return;
|
|
2789
|
+
const currentIdx = STEP_ORDER.indexOf(this.state.step);
|
|
2790
|
+
if (currentIdx > 0) {
|
|
2791
|
+
const previousStep = STEP_ORDER[currentIdx - 1];
|
|
2792
|
+
this.state.step = previousStep;
|
|
2793
|
+
this.state.completedSteps = this.state.completedSteps.filter((s) => s !== this.state.step || STEP_ORDER.indexOf(s) <= STEP_ORDER.indexOf(previousStep));
|
|
2794
|
+
}
|
|
2795
|
+
this.state.updatedAt = Date.now();
|
|
2796
|
+
}
|
|
2797
|
+
reset() {
|
|
2798
|
+
this.state = createInitialState();
|
|
2799
|
+
}
|
|
2800
|
+
buildConfig() {
|
|
2801
|
+
const cf = this.state.cloudflareConfig;
|
|
2802
|
+
const workers = {};
|
|
2803
|
+
for (const [name, baseCfg] of Object.entries(BASE_WORKERS)) {
|
|
2804
|
+
workers[name] = {
|
|
2805
|
+
enabled: true,
|
|
2806
|
+
path: baseCfg.path,
|
|
2807
|
+
vars: { ...baseCfg.vars },
|
|
2808
|
+
secrets: [...BASE_SECRETS[name] ?? []]
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
for (const key of this.state.selectedIntegrations) {
|
|
2812
|
+
const integration = INTEGRATIONS.find((i) => i.key === key);
|
|
2813
|
+
if (!integration)
|
|
2814
|
+
continue;
|
|
2815
|
+
const workerName = integration.workerName;
|
|
2816
|
+
if (!workers[workerName]) {
|
|
2817
|
+
workers[workerName] = {
|
|
2818
|
+
enabled: true,
|
|
2819
|
+
path: `workers/${workerName}`,
|
|
2820
|
+
vars: {},
|
|
2821
|
+
secrets: []
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
if (integration.vars) {
|
|
2825
|
+
Object.assign(workers[workerName].vars, integration.vars);
|
|
2826
|
+
}
|
|
2827
|
+
for (const secretName of Object.keys(integration.secrets)) {
|
|
2828
|
+
if (!workers[workerName].secrets.includes(secretName)) {
|
|
2829
|
+
workers[workerName].secrets.push(secretName);
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
for (const workerName of this.state.selectedWorkers) {
|
|
2834
|
+
if (!workers[workerName]) {
|
|
2835
|
+
workers[workerName] = {
|
|
2836
|
+
enabled: true,
|
|
2837
|
+
path: `workers/${workerName}`,
|
|
2838
|
+
vars: {},
|
|
2839
|
+
secrets: []
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
return {
|
|
2844
|
+
global: {
|
|
2845
|
+
cloudflare_api_token: cf?.apiToken ?? "",
|
|
2846
|
+
cloudflare_account_id: cf?.accountId ?? "",
|
|
2847
|
+
cloudflare_secret_store_id: cf?.secretStoreId ?? "",
|
|
2848
|
+
subdomain_prefix: cf?.subdomain ?? ""
|
|
2849
|
+
},
|
|
2850
|
+
workers
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
getProvisioningPlan() {
|
|
2854
|
+
const plan = {
|
|
2855
|
+
d1Databases: [],
|
|
2856
|
+
kvNamespaces: [],
|
|
2857
|
+
r2Buckets: [],
|
|
2858
|
+
queues: []
|
|
2859
|
+
};
|
|
2860
|
+
const allWorkers = [
|
|
2861
|
+
...Object.keys(BASE_WORKERS),
|
|
2862
|
+
...this.state.selectedWorkers,
|
|
2863
|
+
...this.state.selectedIntegrations.map((k) => {
|
|
2864
|
+
const i = INTEGRATIONS.find((i2) => i2.key === k);
|
|
2865
|
+
return i?.workerName ?? "";
|
|
2866
|
+
}).filter(Boolean)
|
|
2867
|
+
];
|
|
2868
|
+
const workerSet = new Set(allWorkers);
|
|
2869
|
+
if (workerSet.has("d1-worker")) {
|
|
2870
|
+
plan.d1Databases.push("hoox-db");
|
|
2871
|
+
}
|
|
2872
|
+
if (workerSet.has("hoox") || workerSet.has("analytics-worker")) {
|
|
2873
|
+
plan.kvNamespaces.push("CONFIG_KV");
|
|
2874
|
+
}
|
|
2875
|
+
if (workerSet.has("web3-wallet-worker")) {
|
|
2876
|
+
plan.kvNamespaces.push("WEB3_CACHE_KV");
|
|
2877
|
+
}
|
|
2878
|
+
plan.d1Databases = [...new Set(plan.d1Databases)];
|
|
2879
|
+
plan.kvNamespaces = [...new Set(plan.kvNamespaces)];
|
|
2880
|
+
return plan;
|
|
2881
|
+
}
|
|
2882
|
+
applyInput(input) {
|
|
2883
|
+
const step = this.state.step;
|
|
2884
|
+
switch (step) {
|
|
2885
|
+
case "PREREQUISITES":
|
|
2886
|
+
break;
|
|
2887
|
+
case "CLOUDFLARE_CONFIG":
|
|
2888
|
+
this.state.cloudflareConfig = {
|
|
2889
|
+
apiToken: String(input.apiToken ?? ""),
|
|
2890
|
+
accountId: String(input.accountId ?? ""),
|
|
2891
|
+
secretStoreId: String(input.secretStoreId ?? ""),
|
|
2892
|
+
subdomain: String(input.subdomain ?? "cryptolinx")
|
|
2893
|
+
};
|
|
2894
|
+
break;
|
|
2895
|
+
case "WORKER_SELECTION": {
|
|
2896
|
+
const preset = input.preset;
|
|
2897
|
+
this.state.preset = preset;
|
|
2898
|
+
if (preset && preset !== "custom") {
|
|
2899
|
+
const presetDef = PRESETS.find((p) => p.name === preset);
|
|
2900
|
+
if (presetDef) {
|
|
2901
|
+
const resolved = resolveDependencies(presetDef.workers);
|
|
2902
|
+
this.state.selectedWorkers = resolved;
|
|
2903
|
+
this.state.selectedIntegrations = presetDef.integrations;
|
|
2904
|
+
}
|
|
2905
|
+
} else {
|
|
2906
|
+
const workers = input.workers;
|
|
2907
|
+
const integrations = input.integrations;
|
|
2908
|
+
if (workers) {
|
|
2909
|
+
this.state.selectedWorkers = resolveDependencies(workers);
|
|
2910
|
+
}
|
|
2911
|
+
if (integrations) {
|
|
2912
|
+
this.state.selectedIntegrations = integrations;
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
break;
|
|
2916
|
+
}
|
|
2917
|
+
case "SECRETS":
|
|
2918
|
+
this.state.secrets = input.secrets ?? {};
|
|
2919
|
+
break;
|
|
2920
|
+
case "PROVISIONING":
|
|
2921
|
+
if (input.provisioningResults) {
|
|
2922
|
+
this.state.provisioningResults = input.provisioningResults;
|
|
2923
|
+
}
|
|
2924
|
+
break;
|
|
2925
|
+
case "CONFIG_WRITE":
|
|
2926
|
+
case "DEPLOY":
|
|
2927
|
+
case "DONE":
|
|
2928
|
+
break;
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
// src/wizard/persistence.ts
|
|
2934
|
+
function serializeState(state) {
|
|
2935
|
+
return JSON.stringify(state, null, 2);
|
|
2936
|
+
}
|
|
2937
|
+
function deserializeState(json) {
|
|
2938
|
+
const parsed = JSON.parse(json);
|
|
2939
|
+
return parsed;
|
|
2940
|
+
}
|
|
2941
|
+
var WIZARD_STATE_PATH = ".wizard-state.json";
|
|
2942
|
+
// src/schemas/registry.ts
|
|
2943
|
+
function deriveCalledBy(manifests) {
|
|
2944
|
+
const calledBy = {};
|
|
2945
|
+
for (const [workerName, manifest] of Object.entries(manifests)) {
|
|
2946
|
+
if (!calledBy[workerName])
|
|
2947
|
+
calledBy[workerName] = new Set;
|
|
2948
|
+
for (const svc of manifest.services) {
|
|
2949
|
+
if (!calledBy[svc.service])
|
|
2950
|
+
calledBy[svc.service] = new Set;
|
|
2951
|
+
calledBy[svc.service].add(workerName);
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
const result = {};
|
|
2955
|
+
for (const [worker, callers] of Object.entries(calledBy)) {
|
|
2956
|
+
result[worker] = [...callers].sort();
|
|
2957
|
+
}
|
|
2958
|
+
return result;
|
|
2959
|
+
}
|
|
2960
|
+
var manifests = {
|
|
2961
|
+
hoox: {
|
|
2962
|
+
name: "hoox",
|
|
2963
|
+
path: "workers/hoox-worker",
|
|
2964
|
+
vars: {
|
|
2965
|
+
WEBHOOK_API_KEY_BINDING: {
|
|
2966
|
+
type: "secret",
|
|
2967
|
+
description: "External webhook auth key"
|
|
2968
|
+
},
|
|
2969
|
+
INTERNAL_KEY_BINDING: {
|
|
2970
|
+
type: "secret",
|
|
2971
|
+
description: "Inter-worker auth key"
|
|
2972
|
+
},
|
|
2973
|
+
HA_TOKEN_BINDING: { type: "secret", description: "Home Assistant token" }
|
|
2974
|
+
},
|
|
2975
|
+
services: [
|
|
2976
|
+
{
|
|
2977
|
+
binding: "TRADE_SERVICE",
|
|
2978
|
+
service: "trade-worker",
|
|
2979
|
+
description: "Trading functionality"
|
|
2980
|
+
},
|
|
2981
|
+
{
|
|
2982
|
+
binding: "TELEGRAM_SERVICE",
|
|
2983
|
+
service: "telegram-worker",
|
|
2984
|
+
description: "Notifications"
|
|
2985
|
+
}
|
|
2986
|
+
],
|
|
2987
|
+
infrastructure: {
|
|
2988
|
+
kv: [
|
|
2989
|
+
{ binding: "SESSIONS_KV", description: "Webhook session storage" },
|
|
2990
|
+
{
|
|
2991
|
+
binding: "CONFIG_KV",
|
|
2992
|
+
description: "Configuration + rate limiter state"
|
|
2993
|
+
}
|
|
2994
|
+
],
|
|
2995
|
+
vectorize: [{ binding: "VECTORIZE_INDEX", index: "my-rag-index" }],
|
|
2996
|
+
ai: true,
|
|
2997
|
+
queues: { producer: ["trade-execution"] },
|
|
2998
|
+
durableObjects: [
|
|
2999
|
+
{ name: "IDEMPOTENCY_STORE", className: "IdempotencyStore" }
|
|
3000
|
+
]
|
|
3001
|
+
},
|
|
3002
|
+
middleware: [
|
|
3003
|
+
"requireAuth",
|
|
3004
|
+
"requireInternalAuth",
|
|
3005
|
+
"cors",
|
|
3006
|
+
"rateLimit",
|
|
3007
|
+
"logger",
|
|
3008
|
+
"validate"
|
|
3009
|
+
]
|
|
3010
|
+
},
|
|
3011
|
+
"trade-worker": {
|
|
3012
|
+
name: "trade-worker",
|
|
3013
|
+
path: "workers/trade-worker",
|
|
3014
|
+
vars: {
|
|
3015
|
+
INTERNAL_KEY_BINDING: {
|
|
3016
|
+
type: "secret",
|
|
3017
|
+
description: "Inter-worker auth key"
|
|
3018
|
+
},
|
|
3019
|
+
TELEGRAM_INTERNAL_KEY_BINDING: {
|
|
3020
|
+
type: "secret",
|
|
3021
|
+
description: "Telegram outbound auth"
|
|
3022
|
+
},
|
|
3023
|
+
BINANCE_KEY_BINDING: { type: "secret", description: "Binance API key" },
|
|
3024
|
+
BINANCE_SECRET_BINDING: {
|
|
3025
|
+
type: "secret",
|
|
3026
|
+
description: "Binance API secret"
|
|
3027
|
+
},
|
|
3028
|
+
MEXC_KEY_BINDING: { type: "secret", description: "MEXC API key" },
|
|
3029
|
+
MEXC_SECRET_BINDING: { type: "secret", description: "MEXC API secret" },
|
|
3030
|
+
BYBIT_KEY_BINDING: { type: "secret", description: "Bybit API key" },
|
|
3031
|
+
BYBIT_SECRET_BINDING: { type: "secret", description: "Bybit API secret" }
|
|
3032
|
+
},
|
|
3033
|
+
services: [
|
|
3034
|
+
{
|
|
3035
|
+
binding: "D1_SERVICE",
|
|
3036
|
+
service: "d1-worker",
|
|
3037
|
+
description: "Database operations"
|
|
3038
|
+
},
|
|
3039
|
+
{
|
|
3040
|
+
binding: "TELEGRAM_SERVICE",
|
|
3041
|
+
service: "telegram-worker",
|
|
3042
|
+
description: "Notifications"
|
|
3043
|
+
}
|
|
3044
|
+
],
|
|
3045
|
+
infrastructure: {
|
|
3046
|
+
kv: [{ binding: "CONFIG_KV", description: "Configuration" }],
|
|
3047
|
+
d1: [
|
|
3048
|
+
{
|
|
3049
|
+
binding: "DB",
|
|
3050
|
+
database: "trade-data-db",
|
|
3051
|
+
description: "Trade operations"
|
|
3052
|
+
}
|
|
3053
|
+
],
|
|
3054
|
+
r2: [
|
|
3055
|
+
{
|
|
3056
|
+
binding: "REPORTS_BUCKET",
|
|
3057
|
+
bucket: "trade-reports",
|
|
3058
|
+
description: "Trade reports"
|
|
3059
|
+
},
|
|
3060
|
+
{
|
|
3061
|
+
binding: "SYSTEM_LOGS_BUCKET",
|
|
3062
|
+
bucket: "hoox-system-logs",
|
|
3063
|
+
description: "Verbose exchange logs"
|
|
3064
|
+
}
|
|
3065
|
+
],
|
|
3066
|
+
queues: { consumer: ["trade-execution"] }
|
|
3067
|
+
},
|
|
3068
|
+
middleware: ["requireInternalAuth"]
|
|
3069
|
+
},
|
|
3070
|
+
"telegram-worker": {
|
|
3071
|
+
name: "telegram-worker",
|
|
3072
|
+
path: "workers/telegram-worker",
|
|
3073
|
+
vars: {
|
|
3074
|
+
INTERNAL_KEY_BINDING: {
|
|
3075
|
+
type: "secret",
|
|
3076
|
+
description: "Inter-worker auth key"
|
|
3077
|
+
},
|
|
3078
|
+
TG_BOT_TOKEN_BINDING: {
|
|
3079
|
+
type: "secret",
|
|
3080
|
+
description: "Telegram bot token"
|
|
3081
|
+
},
|
|
3082
|
+
TG_CHAT_ID_BINDING: { type: "secret", description: "Default chat ID" },
|
|
3083
|
+
TELEGRAM_SECRET_TOKEN: {
|
|
3084
|
+
type: "secret",
|
|
3085
|
+
description: "Webhook verification token"
|
|
3086
|
+
}
|
|
3087
|
+
},
|
|
3088
|
+
services: [],
|
|
3089
|
+
infrastructure: {
|
|
3090
|
+
kv: [{ binding: "CONFIG_KV", description: "Configuration" }],
|
|
3091
|
+
r2: [
|
|
3092
|
+
{
|
|
3093
|
+
binding: "UPLOADS_BUCKET",
|
|
3094
|
+
bucket: "user-uploads",
|
|
3095
|
+
description: "User uploaded files"
|
|
3096
|
+
}
|
|
3097
|
+
],
|
|
3098
|
+
vectorize: [{ binding: "VECTORIZE_INDEX", index: "my-rag-index" }],
|
|
3099
|
+
ai: true
|
|
3100
|
+
},
|
|
3101
|
+
middleware: ["requireInternalAuth"]
|
|
3102
|
+
},
|
|
3103
|
+
"d1-worker": {
|
|
3104
|
+
name: "d1-worker",
|
|
3105
|
+
path: "workers/d1-worker",
|
|
3106
|
+
vars: {
|
|
3107
|
+
INTERNAL_KEY_BINDING: {
|
|
3108
|
+
type: "secret",
|
|
3109
|
+
description: "Inter-worker auth key"
|
|
3110
|
+
}
|
|
3111
|
+
},
|
|
3112
|
+
services: [],
|
|
3113
|
+
infrastructure: {
|
|
3114
|
+
kv: [{ binding: "CONFIG_KV", description: "Configuration" }],
|
|
3115
|
+
d1: [
|
|
3116
|
+
{
|
|
3117
|
+
binding: "DB",
|
|
3118
|
+
database: "trade-data-db",
|
|
3119
|
+
description: "Main database"
|
|
3120
|
+
}
|
|
3121
|
+
]
|
|
3122
|
+
},
|
|
3123
|
+
middleware: ["requireInternalAuth"]
|
|
3124
|
+
},
|
|
3125
|
+
"web3-wallet-worker": {
|
|
3126
|
+
name: "web3-wallet-worker",
|
|
3127
|
+
path: "workers/web3-wallet-worker",
|
|
3128
|
+
vars: {
|
|
3129
|
+
WALLET_PK_SECRET: { type: "secret", description: "Wallet private key" },
|
|
3130
|
+
WALLET_MNEMONIC_SECRET: {
|
|
3131
|
+
type: "secret",
|
|
3132
|
+
description: "Wallet mnemonic phrase"
|
|
3133
|
+
}
|
|
3134
|
+
},
|
|
3135
|
+
services: [
|
|
3136
|
+
{
|
|
3137
|
+
binding: "TELEGRAM_SERVICE",
|
|
3138
|
+
service: "telegram-worker",
|
|
3139
|
+
description: "Notifications"
|
|
3140
|
+
}
|
|
3141
|
+
],
|
|
3142
|
+
infrastructure: {},
|
|
3143
|
+
middleware: ["requireInternalAuth"]
|
|
3144
|
+
},
|
|
3145
|
+
"agent-worker": {
|
|
3146
|
+
name: "agent-worker",
|
|
3147
|
+
path: "workers/agent-worker",
|
|
3148
|
+
vars: {
|
|
3149
|
+
AGENT_INTERNAL_KEY: { type: "secret", description: "Agent worker auth" },
|
|
3150
|
+
INTERNAL_KEY_BINDING: {
|
|
3151
|
+
type: "secret",
|
|
3152
|
+
description: "Inter-worker auth key"
|
|
3153
|
+
}
|
|
3154
|
+
},
|
|
3155
|
+
services: [
|
|
3156
|
+
{
|
|
3157
|
+
binding: "TRADE_SERVICE",
|
|
3158
|
+
service: "trade-worker",
|
|
3159
|
+
description: "Trading functionality"
|
|
3160
|
+
},
|
|
3161
|
+
{
|
|
3162
|
+
binding: "TELEGRAM_SERVICE",
|
|
3163
|
+
service: "telegram-worker",
|
|
3164
|
+
description: "Notifications"
|
|
3165
|
+
}
|
|
3166
|
+
],
|
|
3167
|
+
infrastructure: {
|
|
3168
|
+
kv: [{ binding: "CONFIG_KV", description: "Configuration" }],
|
|
3169
|
+
d1: [
|
|
3170
|
+
{
|
|
3171
|
+
binding: "DB",
|
|
3172
|
+
database: "trade-data-db",
|
|
3173
|
+
description: "Portfolio queries"
|
|
3174
|
+
}
|
|
3175
|
+
],
|
|
3176
|
+
ai: true
|
|
3177
|
+
},
|
|
3178
|
+
middleware: ["requireInternalAuth"],
|
|
3179
|
+
cron: ["*/15 * * * *"]
|
|
3180
|
+
},
|
|
3181
|
+
"email-worker": {
|
|
3182
|
+
name: "email-worker",
|
|
3183
|
+
path: "workers/email-worker",
|
|
3184
|
+
vars: {
|
|
3185
|
+
INTERNAL_KEY_BINDING: {
|
|
3186
|
+
type: "secret",
|
|
3187
|
+
description: "Inter-worker auth key"
|
|
3188
|
+
},
|
|
3189
|
+
EMAIL_HOST_BINDING: { type: "secret", description: "Email IMAP host" },
|
|
3190
|
+
EMAIL_USER_BINDING: { type: "secret", description: "Email username" },
|
|
3191
|
+
EMAIL_PASS_BINDING: { type: "secret", description: "Email password" },
|
|
3192
|
+
TRADE_WORKER_NAME: {
|
|
3193
|
+
type: "plaintext",
|
|
3194
|
+
description: "Trade worker service name",
|
|
3195
|
+
default: "trade-worker"
|
|
3196
|
+
},
|
|
3197
|
+
USE_IMAP: {
|
|
3198
|
+
type: "plaintext",
|
|
3199
|
+
description: "Use IMAP for email",
|
|
3200
|
+
default: "false"
|
|
3201
|
+
},
|
|
3202
|
+
MAILGUN_API_KEY: { type: "secret", description: "Mailgun API key" },
|
|
3203
|
+
EMAIL_SCAN_SUBJECT: { type: "secret", description: "Email scan subject" }
|
|
3204
|
+
},
|
|
3205
|
+
services: [
|
|
3206
|
+
{
|
|
3207
|
+
binding: "TRADE_SERVICE",
|
|
3208
|
+
service: "trade-worker",
|
|
3209
|
+
description: "Trading functionality"
|
|
3210
|
+
}
|
|
3211
|
+
],
|
|
3212
|
+
infrastructure: {
|
|
3213
|
+
kv: [{ binding: "CONFIG_KV", description: "Configuration" }]
|
|
3214
|
+
},
|
|
3215
|
+
middleware: ["requireInternalAuth"],
|
|
3216
|
+
cron: ["*/5 * * * *"]
|
|
3217
|
+
},
|
|
3218
|
+
"analytics-worker": {
|
|
3219
|
+
name: "analytics-worker",
|
|
3220
|
+
path: "workers/analytics-worker",
|
|
3221
|
+
vars: {
|
|
3222
|
+
CLOUDFLARE_API_TOKEN: {
|
|
3223
|
+
type: "secret",
|
|
3224
|
+
description: "CF API token for Analytics SQL"
|
|
3225
|
+
},
|
|
3226
|
+
CLOUDFLARE_ACCOUNT_ID: {
|
|
3227
|
+
type: "plaintext",
|
|
3228
|
+
description: "CF Account ID"
|
|
3229
|
+
}
|
|
3230
|
+
},
|
|
3231
|
+
services: [],
|
|
3232
|
+
infrastructure: {
|
|
3233
|
+
analyticsEngine: true
|
|
3234
|
+
},
|
|
3235
|
+
middleware: []
|
|
3236
|
+
},
|
|
3237
|
+
"report-worker": {
|
|
3238
|
+
name: "report-worker",
|
|
3239
|
+
path: "workers/report-worker",
|
|
3240
|
+
vars: {
|
|
3241
|
+
CF_API_TOKEN_BINDING: {
|
|
3242
|
+
type: "secret",
|
|
3243
|
+
description: "CF API token for Browser Rendering"
|
|
3244
|
+
},
|
|
3245
|
+
ACCOUNT_ID: { type: "plaintext", description: "CF Account ID" }
|
|
3246
|
+
},
|
|
3247
|
+
services: [
|
|
3248
|
+
{
|
|
3249
|
+
binding: "D1_SERVICE",
|
|
3250
|
+
service: "d1-worker",
|
|
3251
|
+
description: "Database queries"
|
|
3252
|
+
},
|
|
3253
|
+
{
|
|
3254
|
+
binding: "TELEGRAM_SERVICE",
|
|
3255
|
+
service: "telegram-worker",
|
|
3256
|
+
description: "Notifications"
|
|
3257
|
+
}
|
|
3258
|
+
],
|
|
3259
|
+
infrastructure: {
|
|
3260
|
+
r2: [
|
|
3261
|
+
{
|
|
3262
|
+
binding: "REPORTS_BUCKET",
|
|
3263
|
+
bucket: "trade-reports",
|
|
3264
|
+
description: "PDF reports"
|
|
3265
|
+
}
|
|
3266
|
+
]
|
|
3267
|
+
},
|
|
3268
|
+
middleware: ["requireInternalAuth"],
|
|
3269
|
+
cron: ["0 8 * * *", "0 18 * * *"]
|
|
3270
|
+
},
|
|
3271
|
+
"pyne-worker": {
|
|
3272
|
+
name: "pyne-worker",
|
|
3273
|
+
path: "workers/pyne-worker",
|
|
3274
|
+
vars: {
|
|
3275
|
+
API_KEY: {
|
|
3276
|
+
type: "secret",
|
|
3277
|
+
description: "X-API-Key for /run and management endpoints"
|
|
3278
|
+
},
|
|
3279
|
+
ALERT_WEBHOOK_URL: {
|
|
3280
|
+
type: "secret",
|
|
3281
|
+
description: "Default HTTPS webhook for alert()/alertcondition()"
|
|
3282
|
+
},
|
|
3283
|
+
TRADE_WORKER_NAME: {
|
|
3284
|
+
type: "plaintext",
|
|
3285
|
+
description: "Trade worker service name",
|
|
3286
|
+
default: "trade-worker"
|
|
3287
|
+
}
|
|
3288
|
+
},
|
|
3289
|
+
services: [
|
|
3290
|
+
{
|
|
3291
|
+
binding: "TRADE_SERVICE",
|
|
3292
|
+
service: "trade-worker",
|
|
3293
|
+
description: "Forward strategy trade events"
|
|
3294
|
+
}
|
|
3295
|
+
],
|
|
3296
|
+
infrastructure: {
|
|
3297
|
+
r2: [
|
|
3298
|
+
{
|
|
3299
|
+
binding: "OHLCV_DATA",
|
|
3300
|
+
bucket: "pyne-worker-ohlcv",
|
|
3301
|
+
description: "OHLCV klines + script registry"
|
|
3302
|
+
}
|
|
3303
|
+
]
|
|
3304
|
+
},
|
|
3305
|
+
middleware: [],
|
|
3306
|
+
cron: ["* * * * *"]
|
|
3307
|
+
},
|
|
3308
|
+
dashboard: {
|
|
3309
|
+
name: "dashboard",
|
|
3310
|
+
path: "workers/dashboard",
|
|
3311
|
+
vars: {
|
|
3312
|
+
DASHBOARD_USER: {
|
|
3313
|
+
type: "secret",
|
|
3314
|
+
description: "Dashboard admin username"
|
|
3315
|
+
},
|
|
3316
|
+
DASHBOARD_PASS: {
|
|
3317
|
+
type: "secret",
|
|
3318
|
+
description: "Dashboard admin password"
|
|
3319
|
+
},
|
|
3320
|
+
SESSION_SECRET: { type: "secret", description: "Session encryption key" },
|
|
3321
|
+
PYNE_API_KEY: {
|
|
3322
|
+
type: "secret",
|
|
3323
|
+
description: "Dashboard \u2192 pyne-worker X-API-Key (match API_KEY)"
|
|
3324
|
+
}
|
|
3325
|
+
},
|
|
3326
|
+
services: [
|
|
3327
|
+
{
|
|
3328
|
+
binding: "D1_SERVICE",
|
|
3329
|
+
service: "d1-worker",
|
|
3330
|
+
description: "Database queries"
|
|
3331
|
+
},
|
|
3332
|
+
{
|
|
3333
|
+
binding: "AGENT_SERVICE",
|
|
3334
|
+
service: "agent-worker",
|
|
3335
|
+
description: "AI risk data"
|
|
3336
|
+
},
|
|
3337
|
+
{
|
|
3338
|
+
binding: "PYNE_SERVICE",
|
|
3339
|
+
service: "pyne-worker",
|
|
3340
|
+
description: "PYNE edge evaluate"
|
|
3341
|
+
}
|
|
3342
|
+
],
|
|
3343
|
+
infrastructure: {
|
|
3344
|
+
kv: [{ binding: "CONFIG_KV", description: "Configuration" }],
|
|
3345
|
+
ai: true
|
|
3346
|
+
},
|
|
3347
|
+
middleware: []
|
|
3348
|
+
}
|
|
3349
|
+
};
|
|
3350
|
+
var CALLED_BY = deriveCalledBy(manifests);
|
|
3351
|
+
var WORKER_MANIFESTS = manifests;
|
|
3352
|
+
var WORKER_NAMES = Object.keys(manifests).sort();
|
|
3353
|
+
|
|
3354
|
+
// src/schemas/validators.ts
|
|
3355
|
+
import { parse } from "jsonc-parser";
|
|
3356
|
+
function validateWranglerJsonc(workerName, manifest, jsoncContent) {
|
|
3357
|
+
const errors = [];
|
|
3358
|
+
let parsed;
|
|
3359
|
+
try {
|
|
3360
|
+
parsed = parse(jsoncContent);
|
|
3361
|
+
} catch {
|
|
3362
|
+
errors.push({
|
|
3363
|
+
worker: workerName,
|
|
3364
|
+
severity: "error",
|
|
3365
|
+
message: "Failed to parse wrangler.jsonc",
|
|
3366
|
+
file: "wrangler.jsonc"
|
|
3367
|
+
});
|
|
3368
|
+
return errors;
|
|
3369
|
+
}
|
|
3370
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3371
|
+
errors.push({
|
|
3372
|
+
worker: workerName,
|
|
3373
|
+
severity: "error",
|
|
3374
|
+
message: "Failed to parse wrangler.jsonc \u2014 result is not a valid object",
|
|
3375
|
+
file: "wrangler.jsonc"
|
|
3376
|
+
});
|
|
3377
|
+
return errors;
|
|
3378
|
+
}
|
|
3379
|
+
const wrangler = parsed;
|
|
3380
|
+
const declaredVars = wrangler.vars ?? {};
|
|
3381
|
+
for (const [name, def] of Object.entries(manifest.vars)) {
|
|
3382
|
+
if (!(name in declaredVars)) {
|
|
3383
|
+
errors.push({
|
|
3384
|
+
worker: workerName,
|
|
3385
|
+
severity: "error",
|
|
3386
|
+
message: `Missing var: ${name} (${def.description})`,
|
|
3387
|
+
file: "wrangler.jsonc"
|
|
3388
|
+
});
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
for (const [name, def] of Object.entries(manifest.vars)) {
|
|
3392
|
+
if (def.type === "secret" && !name.endsWith("_BINDING") && !name.endsWith("_SECRET") && !name.startsWith("CLOUDFLARE_") && name !== "TELEGRAM_SECRET_TOKEN" && name !== "MAILGUN_API_KEY" && name !== "EMAIL_SCAN_SUBJECT" && name !== "DASHBOARD_USER" && name !== "DASHBOARD_PASS" && name !== "SESSION_SECRET" && name !== "AGENT_INTERNAL_KEY") {
|
|
3393
|
+
errors.push({
|
|
3394
|
+
worker: workerName,
|
|
3395
|
+
severity: "warning",
|
|
3396
|
+
message: `Secret var "${name}" does not end with _BINDING or _SECRET suffix`,
|
|
3397
|
+
file: "wrangler.jsonc"
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
const declaredServices = wrangler.services ?? [];
|
|
3402
|
+
for (const expected of manifest.services) {
|
|
3403
|
+
const match = declaredServices.find((s) => s.binding === expected.binding && s.service === expected.service);
|
|
3404
|
+
if (!match) {
|
|
3405
|
+
const declared = declaredServices.find((s) => s.binding === expected.binding);
|
|
3406
|
+
if (declared) {
|
|
3407
|
+
errors.push({
|
|
3408
|
+
worker: workerName,
|
|
3409
|
+
severity: "error",
|
|
3410
|
+
message: `Service "${expected.binding}" points to "${declared.service}" but manifest expects "${expected.service}"`,
|
|
3411
|
+
file: "wrangler.jsonc"
|
|
3412
|
+
});
|
|
3413
|
+
} else {
|
|
3414
|
+
errors.push({
|
|
3415
|
+
worker: workerName,
|
|
3416
|
+
severity: "error",
|
|
3417
|
+
message: `Missing service binding: ${expected.binding} -> ${expected.service}`,
|
|
3418
|
+
file: "wrangler.jsonc"
|
|
3419
|
+
});
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
for (const declared of declaredServices) {
|
|
3424
|
+
const expected = manifest.services.find((s) => s.binding === declared.binding);
|
|
3425
|
+
if (!expected) {
|
|
3426
|
+
errors.push({
|
|
3427
|
+
worker: workerName,
|
|
3428
|
+
severity: "warning",
|
|
3429
|
+
message: `Unexpected service binding: ${declared.binding}`,
|
|
3430
|
+
file: "wrangler.jsonc"
|
|
3431
|
+
});
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
return errors;
|
|
3435
|
+
}
|
|
3436
|
+
function validateRootSecrets(workerName, manifest, rootJsoncContent) {
|
|
3437
|
+
const errors = [];
|
|
3438
|
+
let parsed;
|
|
3439
|
+
try {
|
|
3440
|
+
parsed = parse(rootJsoncContent);
|
|
3441
|
+
} catch {
|
|
3442
|
+
errors.push({
|
|
3443
|
+
worker: workerName,
|
|
3444
|
+
severity: "error",
|
|
3445
|
+
message: "Failed to parse root wrangler.jsonc"
|
|
3446
|
+
});
|
|
3447
|
+
return errors;
|
|
3448
|
+
}
|
|
3449
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3450
|
+
errors.push({
|
|
3451
|
+
worker: workerName,
|
|
3452
|
+
severity: "error",
|
|
3453
|
+
message: "Failed to parse root wrangler.jsonc \u2014 result is not a valid object"
|
|
3454
|
+
});
|
|
3455
|
+
return errors;
|
|
3456
|
+
}
|
|
3457
|
+
const rootWrangler = parsed;
|
|
3458
|
+
const rootSecrets = rootWrangler.workers?.[workerName]?.secrets ?? [];
|
|
3459
|
+
const expectedSecrets = Object.entries(manifest.vars).filter(([_, def]) => def.type === "secret").map(([name]) => name);
|
|
3460
|
+
for (const expected of expectedSecrets) {
|
|
3461
|
+
if (!rootSecrets.includes(expected)) {
|
|
3462
|
+
errors.push({
|
|
3463
|
+
worker: workerName,
|
|
3464
|
+
severity: "error",
|
|
3465
|
+
message: `Secret "${expected}" missing from root wrangler.jsonc workers.${workerName}.secrets`,
|
|
3466
|
+
file: "wrangler.jsonc (root)"
|
|
3467
|
+
});
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
return errors;
|
|
3471
|
+
}
|
|
3472
|
+
function validateDevVars(workerName, manifest, devVarsContent) {
|
|
3473
|
+
const errors = [];
|
|
3474
|
+
const vars = new Map;
|
|
3475
|
+
for (const line of devVarsContent.split(`
|
|
3476
|
+
`)) {
|
|
3477
|
+
const trimmed = line.trim();
|
|
3478
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
3479
|
+
continue;
|
|
3480
|
+
const eqIdx = trimmed.indexOf("=");
|
|
3481
|
+
if (eqIdx < 1)
|
|
3482
|
+
continue;
|
|
3483
|
+
vars.set(trimmed.substring(0, eqIdx).trim(), trimmed.substring(eqIdx + 1).trim());
|
|
3484
|
+
}
|
|
3485
|
+
for (const [name, def] of Object.entries(manifest.vars)) {
|
|
3486
|
+
if (def.type !== "secret")
|
|
3487
|
+
continue;
|
|
3488
|
+
if (!vars.has(name)) {
|
|
3489
|
+
errors.push({
|
|
3490
|
+
worker: workerName,
|
|
3491
|
+
severity: "error",
|
|
3492
|
+
message: `Missing "${name}" in .dev.vars`,
|
|
3493
|
+
file: ".dev.vars"
|
|
3494
|
+
});
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
return errors;
|
|
3498
|
+
}
|
|
3499
|
+
function generateWranglerJsonc(manifest) {
|
|
3500
|
+
const parts = [];
|
|
3501
|
+
parts.push(`{`);
|
|
3502
|
+
parts.push(` "name": "${manifest.name}",`);
|
|
3503
|
+
parts.push(` "main": "src/index.ts",`);
|
|
3504
|
+
const varKeys = Object.keys(manifest.vars);
|
|
3505
|
+
if (varKeys.length > 0) {
|
|
3506
|
+
parts.push(` "vars": {`);
|
|
3507
|
+
for (const [i, name] of varKeys.entries()) {
|
|
3508
|
+
const def = manifest.vars[name];
|
|
3509
|
+
const comma = i < varKeys.length - 1 ? "," : "";
|
|
3510
|
+
const val = def.type === "secret" ? "__SECRET__" : JSON.stringify(def.default ?? "");
|
|
3511
|
+
parts.push(` "${name}": ${val}${comma}`);
|
|
3512
|
+
}
|
|
3513
|
+
parts.push(` },`);
|
|
3514
|
+
}
|
|
3515
|
+
if (manifest.services.length > 0) {
|
|
3516
|
+
parts.push(` "services": [`);
|
|
3517
|
+
for (const [i, svc] of manifest.services.entries()) {
|
|
3518
|
+
const comma = i < manifest.services.length - 1 ? "," : "";
|
|
3519
|
+
parts.push(` { "binding": "${svc.binding}", "service": "${svc.service}" }${comma}`);
|
|
3520
|
+
}
|
|
3521
|
+
parts.push(` ],`);
|
|
3522
|
+
}
|
|
3523
|
+
const infra = manifest.infrastructure;
|
|
3524
|
+
if (infra.kv && infra.kv.length > 0) {
|
|
3525
|
+
parts.push(` "kv_namespaces": [`);
|
|
3526
|
+
for (const [i, binding] of infra.kv.entries()) {
|
|
3527
|
+
const comma = i < infra.kv.length - 1 ? "," : "";
|
|
3528
|
+
parts.push(` { "binding": "${binding.binding}", "id": "" }${comma}`);
|
|
3529
|
+
}
|
|
3530
|
+
parts.push(` ],`);
|
|
3531
|
+
}
|
|
3532
|
+
if (infra.d1 && infra.d1.length > 0) {
|
|
3533
|
+
parts.push(` "d1_databases": [`);
|
|
3534
|
+
for (const [i, binding] of infra.d1.entries()) {
|
|
3535
|
+
const comma = i < infra.d1.length - 1 ? "," : "";
|
|
3536
|
+
parts.push(` { "binding": "${binding.binding}", "database_name": "${binding.database}", "database_id": "" }${comma}`);
|
|
3537
|
+
}
|
|
3538
|
+
parts.push(` ],`);
|
|
3539
|
+
}
|
|
3540
|
+
if (infra.r2 && infra.r2.length > 0) {
|
|
3541
|
+
parts.push(` "r2_buckets": [`);
|
|
3542
|
+
for (const [i, binding] of infra.r2.entries()) {
|
|
3543
|
+
const comma = i < infra.r2.length - 1 ? "," : "";
|
|
3544
|
+
parts.push(` { "binding": "${binding.binding}", "bucket_name": "${binding.bucket}" }${comma}`);
|
|
3545
|
+
}
|
|
3546
|
+
parts.push(` ],`);
|
|
3547
|
+
}
|
|
3548
|
+
if (infra.ai) {
|
|
3549
|
+
parts.push(` "ai": { "binding": "AI" },`);
|
|
3550
|
+
}
|
|
3551
|
+
if (infra.analyticsEngine) {
|
|
3552
|
+
parts.push(` "analytics_engine_datasets": [{ "binding": "ANALYTICS_ENGINE", "dataset": "hoox-analytics" }],`);
|
|
3553
|
+
}
|
|
3554
|
+
if (infra.vectorize && infra.vectorize.length > 0) {
|
|
3555
|
+
parts.push(` "vectorize": [`);
|
|
3556
|
+
for (const [i, binding] of infra.vectorize.entries()) {
|
|
3557
|
+
const comma = i < infra.vectorize.length - 1 ? "," : "";
|
|
3558
|
+
parts.push(` { "binding": "${binding.binding}", "index_name": "${binding.index}" }${comma}`);
|
|
3559
|
+
}
|
|
3560
|
+
parts.push(` ],`);
|
|
3561
|
+
}
|
|
3562
|
+
if (infra.queues) {
|
|
3563
|
+
if (infra.queues.producer && infra.queues.producer.length > 0) {
|
|
3564
|
+
parts.push(` "queues": { "producers": [`);
|
|
3565
|
+
for (const [i, q] of infra.queues.producer.entries()) {
|
|
3566
|
+
const comma = i < infra.queues.producer.length - 1 ? "," : "";
|
|
3567
|
+
parts.push(` { "queue": "${q}", "binding": "" }${comma}`);
|
|
3568
|
+
}
|
|
3569
|
+
parts.push(` ] },`);
|
|
3570
|
+
}
|
|
3571
|
+
if (infra.queues.consumer && infra.queues.consumer.length > 0) {
|
|
3572
|
+
parts.push(` "queues": { "consumers": [`);
|
|
3573
|
+
for (const [i, q] of infra.queues.consumer.entries()) {
|
|
3574
|
+
const comma = i < infra.queues.consumer.length - 1 ? "," : "";
|
|
3575
|
+
parts.push(` { "queue": "${q}" }${comma}`);
|
|
3576
|
+
}
|
|
3577
|
+
parts.push(` ] },`);
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
if (infra.durableObjects && infra.durableObjects.length > 0) {
|
|
3581
|
+
parts.push(` "durable_objects": { "bindings": [`);
|
|
3582
|
+
for (const [i, doBinding] of infra.durableObjects.entries()) {
|
|
3583
|
+
const comma = i < infra.durableObjects.length - 1 ? "," : "";
|
|
3584
|
+
parts.push(` { "name": "${doBinding.name}", "class_name": "${doBinding.className}" }${comma}`);
|
|
3585
|
+
}
|
|
3586
|
+
parts.push(` ] },`);
|
|
3587
|
+
}
|
|
3588
|
+
parts.push(`}`);
|
|
3589
|
+
return parts.join(`
|
|
3590
|
+
`) + `
|
|
3591
|
+
`;
|
|
3592
|
+
}
|
|
3593
|
+
function generateDevVars(manifest) {
|
|
3594
|
+
const lines = [];
|
|
3595
|
+
lines.push(`# ${manifest.name} \u2014 Environment Variables`);
|
|
3596
|
+
lines.push(`# Auto-generated from worker manifest schema`);
|
|
3597
|
+
lines.push(``);
|
|
3598
|
+
for (const [name, def] of Object.entries(manifest.vars)) {
|
|
3599
|
+
if (def.type === "secret") {
|
|
3600
|
+
lines.push(`${name}=placeholder_${name.toLowerCase()}`);
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
lines.push(``);
|
|
3604
|
+
return lines.join(`
|
|
3605
|
+
`);
|
|
3606
|
+
}
|
|
3607
|
+
function validateAll(workerName, manifest, files) {
|
|
3608
|
+
const errors = [
|
|
3609
|
+
...validateWranglerJsonc(workerName, manifest, files.wranglerJsonc),
|
|
3610
|
+
...validateRootSecrets(workerName, manifest, files.rootWranglerJsonc),
|
|
3611
|
+
...validateDevVars(workerName, manifest, files.devVars)
|
|
3612
|
+
];
|
|
3613
|
+
return {
|
|
3614
|
+
worker: workerName,
|
|
3615
|
+
passed: errors.filter((e) => e.severity === "error").length === 0,
|
|
3616
|
+
errors
|
|
3617
|
+
};
|
|
3618
|
+
}
|
|
3619
|
+
// src/stores/ui-store.ts
|
|
3620
|
+
import { create } from "zustand";
|
|
3621
|
+
import { immer } from "zustand/middleware/immer";
|
|
3622
|
+
var DEFAULT_VIEW = "dashboard";
|
|
3623
|
+
var initialState = {
|
|
3624
|
+
activeView: DEFAULT_VIEW,
|
|
3625
|
+
sidebarExpanded: true,
|
|
3626
|
+
modal: null,
|
|
3627
|
+
commandPaletteOpen: false,
|
|
3628
|
+
previousView: null
|
|
3629
|
+
};
|
|
3630
|
+
var useUIStore = create()(immer((set) => ({
|
|
3631
|
+
...initialState,
|
|
3632
|
+
setView: (view) => set((state) => {
|
|
3633
|
+
if (state.activeView !== view) {
|
|
3634
|
+
state.previousView = state.activeView;
|
|
3635
|
+
}
|
|
3636
|
+
state.activeView = view;
|
|
3637
|
+
state.commandPaletteOpen = false;
|
|
3638
|
+
}),
|
|
3639
|
+
toggleSidebar: () => set((state) => {
|
|
3640
|
+
state.sidebarExpanded = !state.sidebarExpanded;
|
|
3641
|
+
}),
|
|
3642
|
+
openPalette: () => set((state) => {
|
|
3643
|
+
state.commandPaletteOpen = true;
|
|
3644
|
+
}),
|
|
3645
|
+
closePalette: () => set((state) => {
|
|
3646
|
+
state.commandPaletteOpen = false;
|
|
3647
|
+
}),
|
|
3648
|
+
showModal: (modal) => set((state) => {
|
|
3649
|
+
state.modal = modal;
|
|
3650
|
+
}),
|
|
3651
|
+
dismissModal: () => set((state) => {
|
|
3652
|
+
state.modal = null;
|
|
3653
|
+
}),
|
|
3654
|
+
goBack: () => set((state) => {
|
|
3655
|
+
if (state.previousView) {
|
|
3656
|
+
state.activeView = state.previousView;
|
|
3657
|
+
state.previousView = null;
|
|
3658
|
+
}
|
|
3659
|
+
})
|
|
3660
|
+
})));
|
|
3661
|
+
|
|
3662
|
+
// src/stores/service-store.ts
|
|
3663
|
+
import { create as create2 } from "zustand";
|
|
3664
|
+
import { immer as immer2 } from "zustand/middleware/immer";
|
|
3665
|
+
function summarizeCliError(details) {
|
|
3666
|
+
const raw = details.stderr.trim() || details.stdout.trim() || details.command || "Unknown CLI error";
|
|
3667
|
+
const firstLine = raw.split(`
|
|
3668
|
+
`, 1)[0] ?? "";
|
|
3669
|
+
const trimmed = firstLine.slice(0, 120).trimEnd();
|
|
3670
|
+
return trimmed.length > 0 ? trimmed : details.command;
|
|
3671
|
+
}
|
|
3672
|
+
var BACKOFF_SEQUENCE = [1000, 2000, 4000, 8000, 16000];
|
|
3673
|
+
var MAX_RETRIES2 = 5;
|
|
3674
|
+
function ringPush(arr, item, max) {
|
|
3675
|
+
const next = [...arr, item];
|
|
3676
|
+
return next.length > max ? next.slice(next.length - max) : next;
|
|
3677
|
+
}
|
|
3678
|
+
var tradeStreamAbort = null;
|
|
3679
|
+
var logStreamAbort = null;
|
|
3680
|
+
var fetchWorkersGeneration = 0;
|
|
3681
|
+
var MAX_TRADES = 500;
|
|
3682
|
+
var MAX_ALERTS = 100;
|
|
3683
|
+
var MAX_LOGS = 1000;
|
|
3684
|
+
var initialState2 = {
|
|
3685
|
+
workers: [],
|
|
3686
|
+
tradeStream: [],
|
|
3687
|
+
alerts: [],
|
|
3688
|
+
logs: [],
|
|
3689
|
+
metrics: null,
|
|
3690
|
+
connectionStatus: "offline",
|
|
3691
|
+
lastUpdated: 0,
|
|
3692
|
+
selectedWorkerId: null,
|
|
3693
|
+
retryCount: 0,
|
|
3694
|
+
lastError: null,
|
|
3695
|
+
lastErrorDetails: null,
|
|
3696
|
+
lastSuccessfulFetch: 0,
|
|
3697
|
+
reconnectDelay: 0,
|
|
3698
|
+
disconnectedAt: null
|
|
3699
|
+
};
|
|
3700
|
+
function getBackoffDelay(retryCount) {
|
|
3701
|
+
if (retryCount <= 0)
|
|
3702
|
+
return 0;
|
|
3703
|
+
const index = Math.min(retryCount - 1, BACKOFF_SEQUENCE.length - 1);
|
|
3704
|
+
return BACKOFF_SEQUENCE[index];
|
|
3705
|
+
}
|
|
3706
|
+
var useServiceStore = create2()(immer2((set, get) => ({
|
|
3707
|
+
...initialState2,
|
|
3708
|
+
fetchWorkers: async () => {
|
|
3709
|
+
const gen = ++fetchWorkersGeneration;
|
|
3710
|
+
const { hooxFetch: hooxFetch2 } = await Promise.resolve().then(() => (init_api_client(), exports_api_client));
|
|
3711
|
+
try {
|
|
3712
|
+
const data = await hooxFetch2("/v1/workers");
|
|
3713
|
+
if (gen !== fetchWorkersGeneration)
|
|
3714
|
+
return;
|
|
3715
|
+
set((state) => {
|
|
3716
|
+
state.workers = data;
|
|
3717
|
+
state.lastUpdated = Date.now();
|
|
3718
|
+
state.lastSuccessfulFetch = Date.now();
|
|
3719
|
+
if (state.connectionStatus === "polling" || state.connectionStatus === "reconnecting" || state.connectionStatus === "offline") {
|
|
3720
|
+
state.connectionStatus = "connected";
|
|
3721
|
+
state.retryCount = 0;
|
|
3722
|
+
state.lastError = null;
|
|
3723
|
+
state.lastErrorDetails = null;
|
|
3724
|
+
state.reconnectDelay = 0;
|
|
3725
|
+
state.disconnectedAt = null;
|
|
3726
|
+
}
|
|
3727
|
+
});
|
|
3728
|
+
} catch (error) {
|
|
3729
|
+
if (gen !== fetchWorkersGeneration)
|
|
3730
|
+
return;
|
|
3731
|
+
const msg = error instanceof Error ? error.message : "Unknown fetch error";
|
|
3732
|
+
get().addAlert({
|
|
3733
|
+
id: crypto.randomUUID(),
|
|
3734
|
+
type: "connection",
|
|
3735
|
+
severity: "error",
|
|
3736
|
+
message: msg,
|
|
3737
|
+
timestamp: Date.now(),
|
|
3738
|
+
acknowledged: false
|
|
3739
|
+
});
|
|
3740
|
+
set((state) => {
|
|
3741
|
+
state.lastError = msg;
|
|
3742
|
+
state.disconnectedAt = state.disconnectedAt ?? Date.now();
|
|
3743
|
+
if (state.connectionStatus === "connected" || state.connectionStatus === "polling") {
|
|
3744
|
+
state.connectionStatus = "reconnecting";
|
|
3745
|
+
state.retryCount = state.retryCount + 1;
|
|
3746
|
+
if (state.retryCount >= MAX_RETRIES2) {
|
|
3747
|
+
state.connectionStatus = "offline";
|
|
3748
|
+
state.lastError = `Connection lost after ${MAX_RETRIES2} retries`;
|
|
3749
|
+
} else {
|
|
3750
|
+
state.reconnectDelay = getBackoffDelay(state.retryCount);
|
|
3751
|
+
}
|
|
3752
|
+
} else if (state.connectionStatus === "reconnecting") {
|
|
3753
|
+
state.retryCount = state.retryCount + 1;
|
|
3754
|
+
if (state.retryCount >= MAX_RETRIES2) {
|
|
3755
|
+
state.connectionStatus = "offline";
|
|
3756
|
+
state.lastError = `Connection lost after ${MAX_RETRIES2} retries`;
|
|
3757
|
+
state.reconnectDelay = 0;
|
|
3758
|
+
} else {
|
|
3759
|
+
state.reconnectDelay = getBackoffDelay(state.retryCount);
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
});
|
|
3763
|
+
}
|
|
3764
|
+
},
|
|
3765
|
+
streamTrades: async () => {
|
|
3766
|
+
const { subscribeSSE: subscribeSSE2 } = await Promise.resolve().then(() => (init_sse(), exports_sse));
|
|
3767
|
+
tradeStreamAbort?.();
|
|
3768
|
+
tradeStreamAbort = null;
|
|
3769
|
+
try {
|
|
3770
|
+
const sub = subscribeSSE2("/v1/trades/stream", (trade) => {
|
|
3771
|
+
get().pushTrade(trade);
|
|
3772
|
+
});
|
|
3773
|
+
const resolved = await Promise.resolve(sub);
|
|
3774
|
+
tradeStreamAbort = resolved.abort.bind(resolved);
|
|
3775
|
+
} catch (error) {
|
|
3776
|
+
const msg = error instanceof Error ? error.message : "SSE trade stream failed";
|
|
3777
|
+
get().addAlert({
|
|
3778
|
+
id: crypto.randomUUID(),
|
|
3779
|
+
type: "connection",
|
|
3780
|
+
severity: "error",
|
|
3781
|
+
message: msg,
|
|
3782
|
+
timestamp: Date.now(),
|
|
3783
|
+
acknowledged: false
|
|
3784
|
+
});
|
|
3785
|
+
get().handleConnectionFailure(msg);
|
|
3786
|
+
}
|
|
3787
|
+
},
|
|
3788
|
+
streamLogs: async () => {
|
|
3789
|
+
const { subscribeSSE: subscribeSSE2 } = await Promise.resolve().then(() => (init_sse(), exports_sse));
|
|
3790
|
+
logStreamAbort?.();
|
|
3791
|
+
logStreamAbort = null;
|
|
3792
|
+
try {
|
|
3793
|
+
const sub = subscribeSSE2("/v1/logs/stream", (log) => {
|
|
3794
|
+
get().pushLog(log);
|
|
3795
|
+
});
|
|
3796
|
+
const resolved = await Promise.resolve(sub);
|
|
3797
|
+
logStreamAbort = resolved.abort.bind(resolved);
|
|
3798
|
+
} catch (error) {
|
|
3799
|
+
const msg = error instanceof Error ? error.message : "SSE log stream failed";
|
|
3800
|
+
get().addAlert({
|
|
3801
|
+
id: crypto.randomUUID(),
|
|
3802
|
+
type: "connection",
|
|
3803
|
+
severity: "error",
|
|
3804
|
+
message: msg,
|
|
3805
|
+
timestamp: Date.now(),
|
|
3806
|
+
acknowledged: false
|
|
3807
|
+
});
|
|
3808
|
+
get().handleConnectionFailure(msg);
|
|
3809
|
+
}
|
|
3810
|
+
},
|
|
3811
|
+
stopStreams: () => {
|
|
3812
|
+
tradeStreamAbort?.();
|
|
3813
|
+
logStreamAbort?.();
|
|
3814
|
+
tradeStreamAbort = null;
|
|
3815
|
+
logStreamAbort = null;
|
|
3816
|
+
},
|
|
3817
|
+
addAlert: (alert) => set((state) => {
|
|
3818
|
+
state.alerts = ringPush(state.alerts, alert, MAX_ALERTS);
|
|
3819
|
+
}),
|
|
3820
|
+
addAlerts: (alerts) => set((state) => {
|
|
3821
|
+
const merged = [...state.alerts, ...alerts];
|
|
3822
|
+
state.alerts = merged.length > MAX_ALERTS ? merged.slice(merged.length - MAX_ALERTS) : merged;
|
|
3823
|
+
}),
|
|
3824
|
+
setConnectionStatus: (status) => set((state) => {
|
|
3825
|
+
if (status === "connected") {
|
|
3826
|
+
state.retryCount = 0;
|
|
3827
|
+
state.lastError = null;
|
|
3828
|
+
state.lastErrorDetails = null;
|
|
3829
|
+
state.reconnectDelay = 0;
|
|
3830
|
+
state.disconnectedAt = null;
|
|
3831
|
+
}
|
|
3832
|
+
if (status === "offline" || status === "reconnecting") {
|
|
3833
|
+
state.disconnectedAt = state.disconnectedAt ?? Date.now();
|
|
3834
|
+
}
|
|
3835
|
+
state.connectionStatus = status;
|
|
3836
|
+
}),
|
|
3837
|
+
selectWorker: (id) => set((state) => {
|
|
3838
|
+
state.selectedWorkerId = id;
|
|
3839
|
+
}),
|
|
3840
|
+
setWorkers: (workers) => set((state) => {
|
|
3841
|
+
state.workers = workers;
|
|
3842
|
+
state.lastUpdated = Date.now();
|
|
3843
|
+
}),
|
|
3844
|
+
pushTrade: (trade) => set((state) => {
|
|
3845
|
+
state.tradeStream = ringPush(state.tradeStream, trade, MAX_TRADES);
|
|
3846
|
+
}),
|
|
3847
|
+
pushLog: (log) => set((state) => {
|
|
3848
|
+
state.logs = ringPush(state.logs, log, MAX_LOGS);
|
|
3849
|
+
}),
|
|
3850
|
+
setMetrics: (metrics) => set((state) => {
|
|
3851
|
+
state.metrics = metrics;
|
|
3852
|
+
state.lastUpdated = metrics.lastUpdated;
|
|
3853
|
+
}),
|
|
3854
|
+
handleConnectionSuccess: () => set((state) => {
|
|
3855
|
+
state.lastSuccessfulFetch = Date.now();
|
|
3856
|
+
state.retryCount = 0;
|
|
3857
|
+
state.lastError = null;
|
|
3858
|
+
state.lastErrorDetails = null;
|
|
3859
|
+
state.reconnectDelay = 0;
|
|
3860
|
+
state.disconnectedAt = null;
|
|
3861
|
+
state.connectionStatus = "connected";
|
|
3862
|
+
}),
|
|
3863
|
+
handleConnectionFailure: (errorMessage) => set((state) => {
|
|
3864
|
+
state.lastError = errorMessage;
|
|
3865
|
+
state.disconnectedAt = state.disconnectedAt ?? Date.now();
|
|
3866
|
+
if (state.connectionStatus === "connected" || state.connectionStatus === "polling") {
|
|
3867
|
+
state.connectionStatus = "reconnecting";
|
|
3868
|
+
state.retryCount = 1;
|
|
3869
|
+
state.reconnectDelay = getBackoffDelay(1);
|
|
3870
|
+
} else if (state.connectionStatus === "reconnecting") {
|
|
3871
|
+
state.retryCount = state.retryCount + 1;
|
|
3872
|
+
if (state.retryCount >= MAX_RETRIES2) {
|
|
3873
|
+
state.connectionStatus = "offline";
|
|
3874
|
+
state.lastError = `Connection lost after ${MAX_RETRIES2} retries`;
|
|
3875
|
+
state.reconnectDelay = 0;
|
|
3876
|
+
} else {
|
|
3877
|
+
state.reconnectDelay = getBackoffDelay(state.retryCount);
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
}),
|
|
3881
|
+
resetRetries: () => set((state) => {
|
|
3882
|
+
state.retryCount = 0;
|
|
3883
|
+
state.lastError = null;
|
|
3884
|
+
state.lastErrorDetails = null;
|
|
3885
|
+
state.reconnectDelay = 0;
|
|
3886
|
+
state.disconnectedAt = null;
|
|
3887
|
+
}),
|
|
3888
|
+
forceRetry: () => set((state) => {
|
|
3889
|
+
if (state.connectionStatus === "offline") {
|
|
3890
|
+
state.connectionStatus = "polling";
|
|
3891
|
+
state.retryCount = 0;
|
|
3892
|
+
state.lastError = null;
|
|
3893
|
+
state.lastErrorDetails = null;
|
|
3894
|
+
state.reconnectDelay = 0;
|
|
3895
|
+
}
|
|
3896
|
+
}),
|
|
3897
|
+
setLastErrorDetails: (details) => set((state) => {
|
|
3898
|
+
state.lastErrorDetails = details;
|
|
3899
|
+
state.lastError = details === null ? null : summarizeCliError(details);
|
|
3900
|
+
}),
|
|
3901
|
+
clearError: () => set((state) => {
|
|
3902
|
+
state.lastError = null;
|
|
3903
|
+
state.lastErrorDetails = null;
|
|
3904
|
+
}),
|
|
3905
|
+
addCliErrorAlert: (details) => set((state) => {
|
|
3906
|
+
state.lastErrorDetails = details;
|
|
3907
|
+
state.lastError = summarizeCliError(details);
|
|
3908
|
+
const summary = summarizeCliError(details);
|
|
3909
|
+
state.alerts = ringPush(state.alerts, {
|
|
3910
|
+
id: crypto.randomUUID(),
|
|
3911
|
+
type: "connection",
|
|
3912
|
+
severity: "error",
|
|
3913
|
+
message: `CLI failure: ${summary}`,
|
|
3914
|
+
timestamp: Date.now(),
|
|
3915
|
+
acknowledged: false
|
|
3916
|
+
}, MAX_ALERTS);
|
|
3917
|
+
})
|
|
3918
|
+
})));
|
|
3919
|
+
|
|
3920
|
+
// src/stores/config-store.ts
|
|
3921
|
+
import { create as create3 } from "zustand";
|
|
3922
|
+
import { immer as immer3 } from "zustand/middleware/immer";
|
|
3923
|
+
import { persist, createJSONStorage } from "zustand/middleware";
|
|
3924
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
|
|
3925
|
+
import { homedir as homedir3 } from "os";
|
|
3926
|
+
import { join as join4 } from "path";
|
|
3927
|
+
var bunConfigStorage = {
|
|
3928
|
+
getItem: async (_name) => {
|
|
3929
|
+
try {
|
|
3930
|
+
const file = Bun.file(join4(homedir3(), ".hoox", "config.json"));
|
|
3931
|
+
const exists = await file.exists();
|
|
3932
|
+
if (!exists)
|
|
3933
|
+
return null;
|
|
3934
|
+
return await file.text();
|
|
3935
|
+
} catch {
|
|
3936
|
+
return null;
|
|
3937
|
+
}
|
|
3938
|
+
},
|
|
3939
|
+
setItem: async (_name, value) => {
|
|
3940
|
+
const dir = join4(homedir3(), ".hoox");
|
|
3941
|
+
if (!existsSync4(dir)) {
|
|
3942
|
+
mkdirSync2(dir, { recursive: true, mode: HOOX_DIR_MODE });
|
|
3943
|
+
}
|
|
3944
|
+
try {
|
|
3945
|
+
chmodSync2(dir, HOOX_DIR_MODE);
|
|
3946
|
+
} catch {}
|
|
3947
|
+
const filePath = join4(dir, "config.json");
|
|
3948
|
+
await Bun.write(filePath, value);
|
|
3949
|
+
try {
|
|
3950
|
+
chmodSync2(filePath, HOOX_CONFIG_FILE_MODE);
|
|
3951
|
+
} catch {}
|
|
3952
|
+
},
|
|
3953
|
+
removeItem: async (_name) => {}
|
|
3954
|
+
};
|
|
3955
|
+
var DEFAULT_SHORTCUTS = {
|
|
3956
|
+
"command-palette": "Ctrl+P",
|
|
3957
|
+
"toggle-sidebar": "Ctrl+B",
|
|
3958
|
+
"force-refresh": "Ctrl+R",
|
|
3959
|
+
quit: "Ctrl+Q",
|
|
3960
|
+
"view-1": "Ctrl+1",
|
|
3961
|
+
"view-2": "Ctrl+2",
|
|
3962
|
+
"view-3": "Ctrl+3",
|
|
3963
|
+
"view-4": "Ctrl+4",
|
|
3964
|
+
"view-5": "Ctrl+5",
|
|
3965
|
+
"view-6": "Ctrl+6",
|
|
3966
|
+
"view-7": "Ctrl+7",
|
|
3967
|
+
"view-8": "Ctrl+8",
|
|
3968
|
+
"view-9": "Ctrl+9",
|
|
3969
|
+
"view-10": "Ctrl+0"
|
|
3970
|
+
};
|
|
3971
|
+
var DEFAULT_LOG_FILTER = {
|
|
3972
|
+
levels: ["info", "warn", "error"],
|
|
3973
|
+
workers: [],
|
|
3974
|
+
searchText: ""
|
|
3975
|
+
};
|
|
3976
|
+
var DEFAULT_NOTIFICATIONS = {
|
|
3977
|
+
alerts: true,
|
|
3978
|
+
trades: false,
|
|
3979
|
+
debug: false,
|
|
3980
|
+
system: true
|
|
3981
|
+
};
|
|
3982
|
+
var defaults = {
|
|
3983
|
+
theme: "dark",
|
|
3984
|
+
refreshIntervalMs: 500,
|
|
3985
|
+
defaultView: "dashboard",
|
|
3986
|
+
activeExchanges: [],
|
|
3987
|
+
keyboardShortcuts: { ...DEFAULT_SHORTCUTS },
|
|
3988
|
+
logFilters: { ...DEFAULT_LOG_FILTER },
|
|
3989
|
+
notifications: { ...DEFAULT_NOTIFICATIONS },
|
|
3990
|
+
soundEnabled: false
|
|
3991
|
+
};
|
|
3992
|
+
var useConfigStore = create3()(persist(immer3((set) => ({
|
|
3993
|
+
...defaults,
|
|
3994
|
+
updateConfig: (partial) => set((state) => {
|
|
3995
|
+
if (partial.theme !== undefined)
|
|
3996
|
+
state.theme = partial.theme;
|
|
3997
|
+
if (partial.refreshIntervalMs !== undefined)
|
|
3998
|
+
state.refreshIntervalMs = partial.refreshIntervalMs;
|
|
3999
|
+
if (partial.defaultView !== undefined)
|
|
4000
|
+
state.defaultView = partial.defaultView;
|
|
4001
|
+
if (partial.activeExchanges !== undefined)
|
|
4002
|
+
state.activeExchanges = partial.activeExchanges;
|
|
4003
|
+
if (partial.keyboardShortcuts !== undefined)
|
|
4004
|
+
state.keyboardShortcuts = partial.keyboardShortcuts;
|
|
4005
|
+
if (partial.logFilters !== undefined)
|
|
4006
|
+
state.logFilters = partial.logFilters;
|
|
4007
|
+
if (partial.notifications !== undefined)
|
|
4008
|
+
state.notifications = partial.notifications;
|
|
4009
|
+
if (partial.soundEnabled !== undefined)
|
|
4010
|
+
state.soundEnabled = partial.soundEnabled;
|
|
4011
|
+
}),
|
|
4012
|
+
resetDefaults: () => set((state) => {
|
|
4013
|
+
Object.assign(state, defaults);
|
|
4014
|
+
}),
|
|
4015
|
+
setShortcut: (action, key) => set((state) => {
|
|
4016
|
+
state.keyboardShortcuts[action] = key;
|
|
4017
|
+
}),
|
|
4018
|
+
toggleNotification: (channel) => set((state) => {
|
|
4019
|
+
state.notifications[channel] = !state.notifications[channel];
|
|
4020
|
+
})
|
|
4021
|
+
})), {
|
|
4022
|
+
name: "hoox-config",
|
|
4023
|
+
storage: createJSONStorage(() => bunConfigStorage),
|
|
4024
|
+
partialize: (state) => ({
|
|
4025
|
+
theme: state.theme,
|
|
4026
|
+
refreshIntervalMs: state.refreshIntervalMs,
|
|
4027
|
+
defaultView: state.defaultView,
|
|
4028
|
+
activeExchanges: state.activeExchanges,
|
|
4029
|
+
keyboardShortcuts: state.keyboardShortcuts,
|
|
4030
|
+
logFilters: state.logFilters,
|
|
4031
|
+
notifications: state.notifications,
|
|
4032
|
+
soundEnabled: state.soundEnabled
|
|
4033
|
+
}),
|
|
4034
|
+
merge: (persisted, current) => ({
|
|
4035
|
+
...current,
|
|
4036
|
+
...persisted
|
|
4037
|
+
})
|
|
4038
|
+
}));
|
|
4039
|
+
|
|
4040
|
+
// src/index.ts
|
|
4041
|
+
init_api_client();
|
|
4042
|
+
init_sse();
|
|
4043
|
+
init_operator_transport();
|
|
4044
|
+
export {
|
|
4045
|
+
writeSecureFileSync,
|
|
4046
|
+
writeConfigSync,
|
|
4047
|
+
writeConfig,
|
|
4048
|
+
validateWranglerJsonc,
|
|
4049
|
+
validateRootSecrets,
|
|
4050
|
+
validateDevVars,
|
|
4051
|
+
validateConfig,
|
|
4052
|
+
validateAll,
|
|
4053
|
+
useUIStore,
|
|
4054
|
+
useServiceStore,
|
|
4055
|
+
useConfigStore,
|
|
4056
|
+
trackAnalytics,
|
|
4057
|
+
toError,
|
|
4058
|
+
subscribeSSE,
|
|
4059
|
+
stripJsonc,
|
|
4060
|
+
setAgentConfigEmbeddedValue,
|
|
4061
|
+
serviceFetch,
|
|
4062
|
+
serializeState,
|
|
4063
|
+
serializeAgentConfigForKv,
|
|
4064
|
+
saveSession,
|
|
4065
|
+
restoreSession,
|
|
4066
|
+
resolveOperatorTransportProfile,
|
|
4067
|
+
resolveInternalAuthKey,
|
|
4068
|
+
resolveHooxRuntimeRoot,
|
|
4069
|
+
resolveHooxPath,
|
|
4070
|
+
resolveDependencies,
|
|
4071
|
+
readConfigSync,
|
|
4072
|
+
readConfig,
|
|
4073
|
+
parseDashboardManifest,
|
|
4074
|
+
parseAgentConfigJson,
|
|
4075
|
+
operatorUrl,
|
|
4076
|
+
logKvTimestamp,
|
|
4077
|
+
loadDashboardManifestsFromRoot,
|
|
4078
|
+
loadDashboardKvManifestFromRoot,
|
|
4079
|
+
kvManifestFromDashboardManifests,
|
|
4080
|
+
isWithinHooxHome,
|
|
4081
|
+
isUnixModeGroupOrWorldAccessible,
|
|
4082
|
+
isHooxSetupRoot,
|
|
4083
|
+
isDashboardSectionFlatKv,
|
|
4084
|
+
isDashboardSectionEditable,
|
|
4085
|
+
isConfigWorldOrGroupReadable,
|
|
4086
|
+
isAgentConfigSection,
|
|
4087
|
+
isAgentConfigEmbeddedField,
|
|
4088
|
+
hooxFetch,
|
|
4089
|
+
healthCheck,
|
|
4090
|
+
headersToObject,
|
|
4091
|
+
hasOperatorClientCredentials,
|
|
4092
|
+
getTuiEntryCandidates,
|
|
4093
|
+
getRelativeHooxPath,
|
|
4094
|
+
getHooxWranglerPath,
|
|
4095
|
+
getHooxStatePath,
|
|
4096
|
+
getHooxRepoPath,
|
|
4097
|
+
getHooxHome,
|
|
4098
|
+
getHooxDataDir,
|
|
4099
|
+
getHooxConfigDir,
|
|
4100
|
+
getAgentConfigEmbeddedValue,
|
|
4101
|
+
generateWranglerJsonc,
|
|
4102
|
+
generateDevVars,
|
|
4103
|
+
formatUptime,
|
|
4104
|
+
formatTimestamp,
|
|
4105
|
+
formatRequests,
|
|
4106
|
+
formatRelativeTime as formatRelativeTimeFromTime,
|
|
4107
|
+
formatRelativeTime2 as formatRelativeTime,
|
|
4108
|
+
formatPercent,
|
|
4109
|
+
formatNumber,
|
|
4110
|
+
formatMemory,
|
|
4111
|
+
formatLatency,
|
|
4112
|
+
formatDurationCompact,
|
|
4113
|
+
formatDuration2 as formatDuration,
|
|
4114
|
+
formatCurrency,
|
|
4115
|
+
formatCpu,
|
|
4116
|
+
formatConfigPermissionWarning,
|
|
4117
|
+
formatCompactCurrency,
|
|
4118
|
+
findHooxSetupRoot,
|
|
4119
|
+
expandAgentConfigToFieldMap,
|
|
4120
|
+
ensureHooxDirSecure,
|
|
4121
|
+
deserializeState,
|
|
4122
|
+
dashboardWorkerDir,
|
|
4123
|
+
createSuccessResponse,
|
|
4124
|
+
createQueueHandler,
|
|
4125
|
+
createMockVectorizeIndex,
|
|
4126
|
+
createMockSecretsStore,
|
|
4127
|
+
createMockR2,
|
|
4128
|
+
createMockQueue,
|
|
4129
|
+
createMockKV,
|
|
4130
|
+
createMockFetcher,
|
|
4131
|
+
createMockExecutionContext,
|
|
4132
|
+
createMockEnv,
|
|
4133
|
+
createMockD1,
|
|
4134
|
+
createMockAnalyticsEngineDataset,
|
|
4135
|
+
createMockAnalyticsEngine,
|
|
4136
|
+
createMockAi,
|
|
4137
|
+
createJsonResponse,
|
|
4138
|
+
createErrorResponse,
|
|
4139
|
+
createCronHandler,
|
|
4140
|
+
buildOperatorAuthHeaders,
|
|
4141
|
+
buildDashboardKvKey,
|
|
4142
|
+
authenticatedServiceFetch,
|
|
4143
|
+
applyAgentConfigFieldUpdates,
|
|
4144
|
+
WorkerStatusColor,
|
|
4145
|
+
WorkerAPIError,
|
|
4146
|
+
WizardEngine,
|
|
4147
|
+
WebhookPayloadSchema,
|
|
4148
|
+
WORKER_NAMES,
|
|
4149
|
+
WORKER_MANIFESTS,
|
|
4150
|
+
WORKER_DEPENDENCIES,
|
|
4151
|
+
WIZARD_STATE_PATH,
|
|
4152
|
+
WALLET_EXECUTE_AUTH_KEY_FIELDS,
|
|
4153
|
+
TradeSignalSchema,
|
|
4154
|
+
TradeQueueMessageSchema,
|
|
4155
|
+
TradeActionSchema,
|
|
4156
|
+
TRADE_READ_AUTH_KEY_FIELDS,
|
|
4157
|
+
TRADE_EXECUTE_AUTH_KEY_FIELDS,
|
|
4158
|
+
TRADEMARK_NOTICE,
|
|
4159
|
+
TRADEMARKS,
|
|
4160
|
+
TELEGRAM_ALERT_AUTH_KEY_FIELDS,
|
|
4161
|
+
ServiceAuthError,
|
|
4162
|
+
PositionSchema,
|
|
4163
|
+
PRESETS,
|
|
4164
|
+
LogLevelColor,
|
|
4165
|
+
KVKeys,
|
|
4166
|
+
InternalAuthKeyFields,
|
|
4167
|
+
INTEGRATIONS,
|
|
4168
|
+
HOOX_DIR_MODE,
|
|
4169
|
+
HOOX_CONFIG_FILE_MODE,
|
|
4170
|
+
FULL_LEGAL_NOTICE,
|
|
4171
|
+
ExchangeRouter,
|
|
4172
|
+
Errors,
|
|
4173
|
+
DISCLAIMER_HEADER,
|
|
4174
|
+
DISCLAIMER,
|
|
4175
|
+
DASHBOARD_WORKER_PREFIX,
|
|
4176
|
+
DASHBOARD_WORKER_IDS,
|
|
4177
|
+
DASHBOARD_TRADE_EXECUTE_AUTH_KEY_FIELDS,
|
|
4178
|
+
DASHBOARD_TELEGRAM_ALERT_AUTH_KEY_FIELDS,
|
|
4179
|
+
DASHBOARD_SECTION_PREFIX,
|
|
4180
|
+
DASHBOARD_SECTIONS_UI_SKIP,
|
|
4181
|
+
DASHBOARD_SECTIONS_NOT_FLAT_KV,
|
|
4182
|
+
DASHBOARD_SECTIONS_AGENT_CONFIG,
|
|
4183
|
+
DASHBOARD_FIELD_KV_OVERRIDES,
|
|
4184
|
+
DASHBOARD_D1_READ_AUTH_KEY_FIELDS,
|
|
4185
|
+
D1_WRITE_AUTH_KEY_FIELDS,
|
|
4186
|
+
D1_READ_AUTH_KEY_FIELDS,
|
|
4187
|
+
D1Repository,
|
|
4188
|
+
CoolBracketPalette,
|
|
4189
|
+
ConnectionStatusColor,
|
|
4190
|
+
Colors,
|
|
4191
|
+
COPYRIGHT,
|
|
4192
|
+
CALLED_BY,
|
|
4193
|
+
BaseExchangeClient2 as BaseExchangeClient,
|
|
4194
|
+
BalanceSchema,
|
|
4195
|
+
AlertSeverityColor,
|
|
4196
|
+
AGENT_CONFIG_KV_KEY,
|
|
4197
|
+
AGENT_CONFIG_EMBEDDED_FIELDS
|
|
4198
|
+
};
|