teleton 0.8.0 → 0.8.2
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 +28 -11
- package/dist/{chunk-H36RFKRI.js → chunk-2IZU3REP.js} +572 -174
- package/dist/chunk-3UFPFWYP.js +12 -0
- package/dist/{chunk-NUGDTPE4.js → chunk-4L66JHQE.js} +2 -1
- package/dist/{chunk-TVRZJIZX.js → chunk-55SKE6YH.js} +4 -4
- package/dist/{setup-server-QXED3D2L.js → chunk-57URFK6M.js} +161 -210
- package/dist/chunk-5SEMA47R.js +75 -0
- package/dist/{chunk-JHYZYFZJ.js → chunk-7YKSXOQQ.js} +17 -2
- package/dist/{chunk-IJBWWQE4.js → chunk-C4NKJT2Z.js} +12 -0
- package/dist/{chunk-RQBAMUCV.js → chunk-GGXJLMOH.js} +1451 -743
- package/dist/{chunk-WIKM24GZ.js → chunk-H7MFXJZK.js} +7 -2
- package/dist/{chunk-U56QTM46.js → chunk-HEDJCLA6.js} +85 -44
- package/dist/{chunk-QVBSUYVX.js → chunk-J73TA3UM.js} +17 -9
- package/dist/{chunk-P36I6OIV.js → chunk-LC4TV3KL.js} +13 -2
- package/dist/{chunk-RCMD3U65.js → chunk-NQ6FZKCE.js} +13 -0
- package/dist/{chunk-SD4NLLYG.js → chunk-VYKW7FMV.js} +224 -93
- package/dist/chunk-W25Z7CM6.js +487 -0
- package/dist/{chunk-OJCLKU5Z.js → chunk-WFTC3JJW.js} +16 -0
- package/dist/{server-H3QA252W.js → chunk-XBSCYMKM.js} +369 -374
- package/dist/{chunk-PHSAHTK4.js → chunk-YOSUPUAJ.js} +75 -7
- package/dist/cli/index.js +67 -22
- package/dist/{client-LNZTDQSA.js → client-YOOHI776.js} +4 -4
- package/dist/{get-my-gifts-OMGKOEPM.js → get-my-gifts-Y7EN7RK4.js} +3 -3
- package/dist/index.js +15 -14
- package/dist/{memory-AS7WKGTW.js → memory-Q6EWGK2S.js} +7 -5
- package/dist/memory-hook-WUXJNVT5.js +18 -0
- package/dist/{migrate-POHWYEIW.js → migrate-WFU6COBN.js} +5 -5
- package/dist/server-GYZXKIKU.js +787 -0
- package/dist/server-YODFBZKG.js +392 -0
- package/dist/setup-server-IZBUOJRU.js +215 -0
- package/dist/{store-GAFULOOX.js → store-7M4XV6M5.js} +6 -6
- package/dist/{task-dependency-resolver-3FIKQ7Z6.js → task-dependency-resolver-L6UUMTHK.js} +3 -3
- package/dist/{task-executor-RUTFG6VG.js → task-executor-XBNJLUCS.js} +3 -3
- package/dist/{tasks-BEZ4QRI2.js → tasks-WQIKXDX5.js} +1 -1
- package/dist/{tool-adapter-IH5VGBOO.js → tool-adapter-IVX2XQJE.js} +1 -1
- package/dist/{tool-index-H3SHOJC3.js → tool-index-NYH57UWP.js} +9 -6
- package/dist/{transcript-IMNE6KU3.js → transcript-IM7G25OS.js} +2 -2
- package/dist/web/assets/index-BfYCdwLI.js +80 -0
- package/dist/web/assets/{index-BrVqauzj.css → index-DmlyQVhR.css} +1 -1
- package/dist/web/assets/{index.es-DkU1GvWU.js → index.es-DitvF-9H.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +14 -5
- package/dist/chunk-XBE4JB7C.js +0 -8
- package/dist/web/assets/index-DYeEkvJ6.js +0 -72
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSetupRoutes
|
|
3
|
+
} from "./chunk-57URFK6M.js";
|
|
4
|
+
import {
|
|
5
|
+
createConfigRoutes,
|
|
6
|
+
createHooksRoutes,
|
|
7
|
+
createLogsRoutes,
|
|
8
|
+
createMarketplaceRoutes,
|
|
9
|
+
createMcpRoutes,
|
|
10
|
+
createMemoryRoutes,
|
|
11
|
+
createPluginsRoutes,
|
|
12
|
+
createSoulRoutes,
|
|
13
|
+
createStatusRoutes,
|
|
14
|
+
createTasksRoutes,
|
|
15
|
+
createTonProxyRoutes,
|
|
16
|
+
createToolsRoutes,
|
|
17
|
+
createWorkspaceRoutes,
|
|
18
|
+
logInterceptor
|
|
19
|
+
} from "./chunk-XBSCYMKM.js";
|
|
20
|
+
import {
|
|
21
|
+
ensureTlsCert
|
|
22
|
+
} from "./chunk-5SEMA47R.js";
|
|
23
|
+
import "./chunk-WFTC3JJW.js";
|
|
24
|
+
import "./chunk-GGXJLMOH.js";
|
|
25
|
+
import "./chunk-7YKSXOQQ.js";
|
|
26
|
+
import "./chunk-55SKE6YH.js";
|
|
27
|
+
import "./chunk-W25Z7CM6.js";
|
|
28
|
+
import "./chunk-3UFPFWYP.js";
|
|
29
|
+
import "./chunk-7TECSLJ4.js";
|
|
30
|
+
import "./chunk-J73TA3UM.js";
|
|
31
|
+
import "./chunk-YOSUPUAJ.js";
|
|
32
|
+
import "./chunk-LC4TV3KL.js";
|
|
33
|
+
import "./chunk-VYKW7FMV.js";
|
|
34
|
+
import "./chunk-HEDJCLA6.js";
|
|
35
|
+
import "./chunk-VFA7QMCZ.js";
|
|
36
|
+
import "./chunk-C4NKJT2Z.js";
|
|
37
|
+
import "./chunk-XQUHC3JZ.js";
|
|
38
|
+
import "./chunk-R4YSJ4EY.js";
|
|
39
|
+
import {
|
|
40
|
+
TELETON_ROOT
|
|
41
|
+
} from "./chunk-EYWNOHMJ.js";
|
|
42
|
+
import {
|
|
43
|
+
createLogger
|
|
44
|
+
} from "./chunk-NQ6FZKCE.js";
|
|
45
|
+
import "./chunk-4L66JHQE.js";
|
|
46
|
+
import "./chunk-3RG5ZIWI.js";
|
|
47
|
+
|
|
48
|
+
// src/api/server.ts
|
|
49
|
+
import { Hono as Hono6 } from "hono";
|
|
50
|
+
import { bodyLimit } from "hono/body-limit";
|
|
51
|
+
import { timeout } from "hono/timeout";
|
|
52
|
+
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
53
|
+
import { serve } from "@hono/node-server";
|
|
54
|
+
import { createServer as createHttpsServer } from "https";
|
|
55
|
+
import { randomBytes, createHash as createHash2 } from "crypto";
|
|
56
|
+
import { HTTPException as HTTPException3 } from "hono/http-exception";
|
|
57
|
+
|
|
58
|
+
// src/api/deps.ts
|
|
59
|
+
import { HTTPException } from "hono/http-exception";
|
|
60
|
+
function createDepsAdapter(apiDeps) {
|
|
61
|
+
const handler = {
|
|
62
|
+
get(target, prop, receiver) {
|
|
63
|
+
const value = Reflect.get(target, prop, receiver);
|
|
64
|
+
if (prop === "config" || prop === "configPath" || prop === "lifecycle" || prop === "userHookEvaluator" || prop === "marketplace") {
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
if (value === null || value === void 0) {
|
|
68
|
+
throw new HTTPException(503, {
|
|
69
|
+
message: `Service unavailable: ${String(prop)} is not initialized (agent not running)`
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
return new Proxy(apiDeps, handler);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/api/schemas/common.ts
|
|
79
|
+
import { z } from "zod";
|
|
80
|
+
var ProblemDetailSchema = z.object({
|
|
81
|
+
type: z.string().default("about:blank"),
|
|
82
|
+
title: z.string(),
|
|
83
|
+
status: z.number(),
|
|
84
|
+
detail: z.string().optional(),
|
|
85
|
+
instance: z.string().optional()
|
|
86
|
+
});
|
|
87
|
+
function createProblem(status, title, detail, instance) {
|
|
88
|
+
return {
|
|
89
|
+
type: "about:blank",
|
|
90
|
+
title,
|
|
91
|
+
status,
|
|
92
|
+
...detail ? { detail } : {},
|
|
93
|
+
...instance ? { instance } : {}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function createProblemResponse(c, status, title, detail, headers) {
|
|
97
|
+
const body = createProblem(status, title, detail, c.req.path);
|
|
98
|
+
return c.json(body, status, {
|
|
99
|
+
"Content-Type": "application/problem+json",
|
|
100
|
+
...headers ?? {}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/api/middleware/request-id.ts
|
|
105
|
+
import { randomUUID } from "crypto";
|
|
106
|
+
var requestId = async (c, next) => {
|
|
107
|
+
const id = c.req.header("X-Request-Id") || randomUUID();
|
|
108
|
+
c.set("requestId", id);
|
|
109
|
+
c.header("X-Request-Id", id);
|
|
110
|
+
await next();
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// src/api/middleware/auth.ts
|
|
114
|
+
import { createHash, timingSafeEqual } from "crypto";
|
|
115
|
+
import { HTTPException as HTTPException2 } from "hono/http-exception";
|
|
116
|
+
var MAX_FAILED = 10;
|
|
117
|
+
var WINDOW_MS = 5 * 60 * 1e3;
|
|
118
|
+
var BLOCK_MS = 15 * 60 * 1e3;
|
|
119
|
+
function normalizeIp(ip) {
|
|
120
|
+
if (ip.startsWith("::ffff:")) return ip.slice(7);
|
|
121
|
+
return ip;
|
|
122
|
+
}
|
|
123
|
+
function hashApiKey(key) {
|
|
124
|
+
return createHash("sha256").update(key).digest("hex");
|
|
125
|
+
}
|
|
126
|
+
function createAuthMiddleware(config) {
|
|
127
|
+
const failedAttempts = /* @__PURE__ */ new Map();
|
|
128
|
+
const cleanupInterval = setInterval(() => {
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
for (const [ip, attempt] of failedAttempts) {
|
|
131
|
+
if (attempt.blockedUntil < now && now - attempt.blockedUntil > WINDOW_MS) {
|
|
132
|
+
failedAttempts.delete(ip);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}, WINDOW_MS);
|
|
136
|
+
cleanupInterval.unref();
|
|
137
|
+
return async (c, next) => {
|
|
138
|
+
const rawIp = c.env?.ip ?? c.req.header("x-real-ip") ?? "unknown";
|
|
139
|
+
const sourceIp = normalizeIp(rawIp);
|
|
140
|
+
if (config.allowedIps.length > 0 && !config.allowedIps.includes(sourceIp)) {
|
|
141
|
+
throw new HTTPException2(403, {
|
|
142
|
+
res: createProblemResponse(c, 403, "Forbidden", "IP address not in whitelist")
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const attempt = failedAttempts.get(sourceIp);
|
|
146
|
+
if (attempt && attempt.blockedUntil > Date.now()) {
|
|
147
|
+
const retryAfter = Math.ceil((attempt.blockedUntil - Date.now()) / 1e3);
|
|
148
|
+
throw new HTTPException2(429, {
|
|
149
|
+
res: createProblemResponse(
|
|
150
|
+
c,
|
|
151
|
+
429,
|
|
152
|
+
"Too Many Requests",
|
|
153
|
+
`IP blocked due to too many failed auth attempts. Retry after ${retryAfter}s`,
|
|
154
|
+
{ "Retry-After": String(retryAfter) }
|
|
155
|
+
)
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const authHeader = c.req.header("Authorization");
|
|
159
|
+
if (!authHeader) {
|
|
160
|
+
throw new HTTPException2(401, {
|
|
161
|
+
res: createProblemResponse(
|
|
162
|
+
c,
|
|
163
|
+
401,
|
|
164
|
+
"Unauthorized",
|
|
165
|
+
"Missing Authorization header. Use: Authorization: Bearer tltn_..."
|
|
166
|
+
)
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const match = authHeader.match(/^Bearer\s+(tltn_.+)$/);
|
|
170
|
+
if (!match) {
|
|
171
|
+
throw new HTTPException2(401, {
|
|
172
|
+
res: createProblemResponse(
|
|
173
|
+
c,
|
|
174
|
+
401,
|
|
175
|
+
"Unauthorized",
|
|
176
|
+
"Invalid Authorization format. Expected: Bearer tltn_..."
|
|
177
|
+
)
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const apiKey = match[1];
|
|
181
|
+
const keyHash = hashApiKey(apiKey);
|
|
182
|
+
const storedBuf = Buffer.from(config.keyHash, "hex");
|
|
183
|
+
const providedBuf = Buffer.from(keyHash, "hex");
|
|
184
|
+
if (storedBuf.length !== providedBuf.length || !timingSafeEqual(storedBuf, providedBuf)) {
|
|
185
|
+
const existing = failedAttempts.get(sourceIp);
|
|
186
|
+
const count = (existing?.count ?? 0) + 1;
|
|
187
|
+
if (count >= MAX_FAILED) {
|
|
188
|
+
failedAttempts.set(sourceIp, {
|
|
189
|
+
count,
|
|
190
|
+
blockedUntil: Date.now() + BLOCK_MS
|
|
191
|
+
});
|
|
192
|
+
} else {
|
|
193
|
+
failedAttempts.set(sourceIp, {
|
|
194
|
+
count,
|
|
195
|
+
blockedUntil: 0
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
throw new HTTPException2(401, {
|
|
199
|
+
res: createProblemResponse(c, 401, "Unauthorized", "Invalid API key")
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
failedAttempts.delete(sourceIp);
|
|
203
|
+
c.set("keyPrefix", apiKey.slice(0, 10));
|
|
204
|
+
await next();
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/api/middleware/rate-limit.ts
|
|
209
|
+
import { rateLimiter } from "hono-rate-limiter";
|
|
210
|
+
function keyGenerator(c) {
|
|
211
|
+
return c.get("keyPrefix") || "anonymous";
|
|
212
|
+
}
|
|
213
|
+
function createLimiter(windowMs, limit) {
|
|
214
|
+
return rateLimiter({
|
|
215
|
+
windowMs,
|
|
216
|
+
limit,
|
|
217
|
+
keyGenerator,
|
|
218
|
+
handler: (c) => {
|
|
219
|
+
const retryAfter = Math.ceil(windowMs / 1e3);
|
|
220
|
+
return createProblemResponse(
|
|
221
|
+
c,
|
|
222
|
+
429,
|
|
223
|
+
"Too Many Requests",
|
|
224
|
+
`Rate limit exceeded. Try again in ${retryAfter}s`,
|
|
225
|
+
{ "Retry-After": String(retryAfter) }
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
var globalRateLimit = createLimiter(6e4, 60);
|
|
231
|
+
var mutatingRateLimit = async (c, next) => {
|
|
232
|
+
const method = c.req.method;
|
|
233
|
+
if (method === "GET" || method === "HEAD" || method === "OPTIONS") {
|
|
234
|
+
return next();
|
|
235
|
+
}
|
|
236
|
+
return createLimiter(6e4, 10)(c, next);
|
|
237
|
+
};
|
|
238
|
+
var readRateLimit = async (c, next) => {
|
|
239
|
+
if (c.req.method !== "GET") {
|
|
240
|
+
return next();
|
|
241
|
+
}
|
|
242
|
+
return createLimiter(6e4, 300)(c, next);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// src/api/middleware/audit.ts
|
|
246
|
+
var auditLog = createLogger("Audit");
|
|
247
|
+
var auditMiddleware = async (c, next) => {
|
|
248
|
+
const method = c.req.method;
|
|
249
|
+
if (method === "GET" || method === "HEAD" || method === "OPTIONS") {
|
|
250
|
+
return next();
|
|
251
|
+
}
|
|
252
|
+
const start = Date.now();
|
|
253
|
+
await next();
|
|
254
|
+
const durationMs = Date.now() - start;
|
|
255
|
+
auditLog.warn({
|
|
256
|
+
audit: true,
|
|
257
|
+
requestId: c.get("requestId"),
|
|
258
|
+
event: "api_mutation",
|
|
259
|
+
method,
|
|
260
|
+
path: c.req.path,
|
|
261
|
+
statusCode: c.res.status,
|
|
262
|
+
durationMs,
|
|
263
|
+
sourceIp: c.env?.ip ?? "unknown",
|
|
264
|
+
keyPrefix: c.get("keyPrefix") ?? "unknown"
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// src/api/routes/agent.ts
|
|
269
|
+
import { Hono } from "hono";
|
|
270
|
+
var log = createLogger("ManagementAPI");
|
|
271
|
+
function createAgentRoutes(lifecycle) {
|
|
272
|
+
const app = new Hono();
|
|
273
|
+
app.post("/restart", async (c) => {
|
|
274
|
+
if (!lifecycle) {
|
|
275
|
+
return createProblemResponse(c, 503, "Service Unavailable", "Agent lifecycle not available");
|
|
276
|
+
}
|
|
277
|
+
const state = lifecycle.getState();
|
|
278
|
+
if (state === "starting" || state === "stopping") {
|
|
279
|
+
return createProblemResponse(c, 409, "Conflict", `Agent is currently ${state}, please wait`);
|
|
280
|
+
}
|
|
281
|
+
(async () => {
|
|
282
|
+
try {
|
|
283
|
+
if (lifecycle.getState() === "running") {
|
|
284
|
+
await lifecycle.stop();
|
|
285
|
+
}
|
|
286
|
+
await lifecycle.start();
|
|
287
|
+
log.info("Agent restarted via Management API");
|
|
288
|
+
} catch (err) {
|
|
289
|
+
log.error({ err }, "Agent restart failed");
|
|
290
|
+
}
|
|
291
|
+
})().catch(() => {
|
|
292
|
+
});
|
|
293
|
+
return c.json({ state: "restarting" });
|
|
294
|
+
});
|
|
295
|
+
return app;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/api/routes/system.ts
|
|
299
|
+
import { Hono as Hono2 } from "hono";
|
|
300
|
+
import { readFileSync } from "fs";
|
|
301
|
+
import { join, dirname } from "path";
|
|
302
|
+
import { fileURLToPath } from "url";
|
|
303
|
+
import os from "os";
|
|
304
|
+
var API_VERSION = "1.0.0";
|
|
305
|
+
function readPackageVersion() {
|
|
306
|
+
try {
|
|
307
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
308
|
+
const candidates = [
|
|
309
|
+
join(__dirname, "../../package.json"),
|
|
310
|
+
join(__dirname, "../../../package.json")
|
|
311
|
+
];
|
|
312
|
+
for (const p of candidates) {
|
|
313
|
+
try {
|
|
314
|
+
const pkg = JSON.parse(readFileSync(p, "utf-8"));
|
|
315
|
+
return pkg.version ?? "unknown";
|
|
316
|
+
} catch {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} catch {
|
|
321
|
+
}
|
|
322
|
+
return "unknown";
|
|
323
|
+
}
|
|
324
|
+
var cachedVersion = readPackageVersion();
|
|
325
|
+
function createSystemRoutes() {
|
|
326
|
+
const app = new Hono2();
|
|
327
|
+
app.get("/version", (c) => {
|
|
328
|
+
return c.json({
|
|
329
|
+
teleton: cachedVersion,
|
|
330
|
+
node: process.version,
|
|
331
|
+
os: process.platform,
|
|
332
|
+
arch: process.arch,
|
|
333
|
+
apiVersion: API_VERSION
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
app.get("/info", (c) => {
|
|
337
|
+
const cpus = os.cpus();
|
|
338
|
+
const totalMem = os.totalmem();
|
|
339
|
+
const freeMem = os.freemem();
|
|
340
|
+
return c.json({
|
|
341
|
+
cpu: {
|
|
342
|
+
model: cpus[0]?.model ?? "unknown",
|
|
343
|
+
cores: cpus.length,
|
|
344
|
+
loadAvg: os.loadavg()
|
|
345
|
+
},
|
|
346
|
+
memory: {
|
|
347
|
+
total: totalMem,
|
|
348
|
+
free: freeMem,
|
|
349
|
+
used: totalMem - freeMem,
|
|
350
|
+
heapUsed: process.memoryUsage().heapUsed,
|
|
351
|
+
heapTotal: process.memoryUsage().heapTotal
|
|
352
|
+
},
|
|
353
|
+
uptime: {
|
|
354
|
+
process: Math.floor(process.uptime()),
|
|
355
|
+
system: Math.floor(os.uptime())
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
return app;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/api/routes/auth.ts
|
|
363
|
+
import { Hono as Hono3 } from "hono";
|
|
364
|
+
function createAuthRoutes() {
|
|
365
|
+
const app = new Hono3();
|
|
366
|
+
app.post("/validate", (c) => {
|
|
367
|
+
const keyPrefix = c.req.header("authorization")?.slice(7, 17) ?? "unknown";
|
|
368
|
+
return c.json({ valid: true, keyPrefix });
|
|
369
|
+
});
|
|
370
|
+
return app;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/api/routes/logs.ts
|
|
374
|
+
import { Hono as Hono4 } from "hono";
|
|
375
|
+
import { streamSSE } from "hono/streaming";
|
|
376
|
+
function createApiLogsRoutes() {
|
|
377
|
+
const app = new Hono4();
|
|
378
|
+
app.get("/recent", (c) => {
|
|
379
|
+
const linesParam = c.req.query("lines");
|
|
380
|
+
const lines = Math.min(Math.max(parseInt(linesParam || "100", 10) || 100, 1), 1e3);
|
|
381
|
+
const entries = [];
|
|
382
|
+
return c.json({
|
|
383
|
+
lines: entries.slice(-lines),
|
|
384
|
+
count: entries.length,
|
|
385
|
+
note: "Use GET /v1/logs/stream (SSE) for live log streaming"
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
app.get("/stream", (c) => {
|
|
389
|
+
return streamSSE(c, async (stream) => {
|
|
390
|
+
let aborted = false;
|
|
391
|
+
stream.onAbort(() => {
|
|
392
|
+
aborted = true;
|
|
393
|
+
if (cleanup) cleanup();
|
|
394
|
+
});
|
|
395
|
+
const cleanup = logInterceptor.addListener((entry) => {
|
|
396
|
+
if (!aborted) {
|
|
397
|
+
void stream.writeSSE({
|
|
398
|
+
data: JSON.stringify(entry),
|
|
399
|
+
event: "log"
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
await stream.writeSSE({
|
|
404
|
+
data: JSON.stringify({
|
|
405
|
+
level: "log",
|
|
406
|
+
message: "Management API log stream connected",
|
|
407
|
+
timestamp: Date.now()
|
|
408
|
+
}),
|
|
409
|
+
event: "log"
|
|
410
|
+
});
|
|
411
|
+
await new Promise((resolve) => {
|
|
412
|
+
if (aborted) return resolve();
|
|
413
|
+
stream.onAbort(() => resolve());
|
|
414
|
+
});
|
|
415
|
+
if (cleanup) cleanup();
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
return app;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// src/api/routes/memory.ts
|
|
422
|
+
import { Hono as Hono5 } from "hono";
|
|
423
|
+
var log2 = createLogger("ManagementAPI");
|
|
424
|
+
function createApiMemoryRoutes(getDb) {
|
|
425
|
+
const app = new Hono5();
|
|
426
|
+
app.delete("/sessions/:chatId", (c) => {
|
|
427
|
+
const db = getDb();
|
|
428
|
+
if (!db) {
|
|
429
|
+
return createProblemResponse(c, 503, "Service Unavailable", "Database not available");
|
|
430
|
+
}
|
|
431
|
+
const chatId = c.req.param("chatId");
|
|
432
|
+
const result = db.prepare("DELETE FROM sessions WHERE chat_id = ?").run(chatId);
|
|
433
|
+
if (result.changes === 0) {
|
|
434
|
+
return createProblemResponse(c, 404, "Not Found", `No session found for chat ${chatId}`);
|
|
435
|
+
}
|
|
436
|
+
log2.info(`Session deleted for chat ${chatId} via Management API`);
|
|
437
|
+
return c.json({ deleted: result.changes, chatId });
|
|
438
|
+
});
|
|
439
|
+
app.post("/sessions/prune", async (c) => {
|
|
440
|
+
const db = getDb();
|
|
441
|
+
if (!db) {
|
|
442
|
+
return createProblemResponse(c, 503, "Service Unavailable", "Database not available");
|
|
443
|
+
}
|
|
444
|
+
let maxAgeDays = 30;
|
|
445
|
+
try {
|
|
446
|
+
const body = await c.req.json();
|
|
447
|
+
if (body.maxAgeDays && body.maxAgeDays > 0) {
|
|
448
|
+
maxAgeDays = body.maxAgeDays;
|
|
449
|
+
}
|
|
450
|
+
} catch {
|
|
451
|
+
}
|
|
452
|
+
const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
|
|
453
|
+
const result = db.prepare("DELETE FROM sessions WHERE updated_at < ?").run(cutoff);
|
|
454
|
+
log2.info(`Pruned ${result.changes} sessions older than ${maxAgeDays} days via Management API`);
|
|
455
|
+
return c.json({ pruned: result.changes, maxAgeDays });
|
|
456
|
+
});
|
|
457
|
+
return app;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/api/server.ts
|
|
461
|
+
var log3 = createLogger("ManagementAPI");
|
|
462
|
+
var KEY_PREFIX = "tltn_";
|
|
463
|
+
function generateApiKey() {
|
|
464
|
+
return KEY_PREFIX + randomBytes(32).toString("base64url");
|
|
465
|
+
}
|
|
466
|
+
function hashApiKey2(key) {
|
|
467
|
+
return createHash2("sha256").update(key).digest("hex");
|
|
468
|
+
}
|
|
469
|
+
var SSE_PATHS = ["/v1/agent/events", "/v1/logs/stream"];
|
|
470
|
+
var ApiServer = class {
|
|
471
|
+
app;
|
|
472
|
+
server = null;
|
|
473
|
+
deps;
|
|
474
|
+
config;
|
|
475
|
+
tls = null;
|
|
476
|
+
apiKey = null;
|
|
477
|
+
keyHash;
|
|
478
|
+
constructor(deps, config) {
|
|
479
|
+
this.deps = deps;
|
|
480
|
+
this.config = config;
|
|
481
|
+
this.app = new Hono6();
|
|
482
|
+
if (config.key_hash) {
|
|
483
|
+
this.keyHash = config.key_hash;
|
|
484
|
+
} else {
|
|
485
|
+
this.apiKey = generateApiKey();
|
|
486
|
+
this.keyHash = hashApiKey2(this.apiKey);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/** Update live deps (e.g., when agent starts/stops) */
|
|
490
|
+
updateDeps(partial) {
|
|
491
|
+
Object.assign(this.deps, partial);
|
|
492
|
+
}
|
|
493
|
+
setupMiddleware() {
|
|
494
|
+
this.app.use("*", async (c, next) => {
|
|
495
|
+
const incoming = c.env?.incoming;
|
|
496
|
+
if (incoming?.socket?.remoteAddress) {
|
|
497
|
+
c.env.ip = incoming.socket.remoteAddress;
|
|
498
|
+
}
|
|
499
|
+
await next();
|
|
500
|
+
});
|
|
501
|
+
this.app.use("*", requestId);
|
|
502
|
+
this.app.use(
|
|
503
|
+
"*",
|
|
504
|
+
bodyLimit({
|
|
505
|
+
maxSize: 2 * 1024 * 1024,
|
|
506
|
+
onError: (c) => {
|
|
507
|
+
return c.json(
|
|
508
|
+
createProblem(413, "Payload Too Large", "Request body exceeds 2MB limit"),
|
|
509
|
+
413,
|
|
510
|
+
{
|
|
511
|
+
"Content-Type": "application/problem+json"
|
|
512
|
+
}
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
})
|
|
516
|
+
);
|
|
517
|
+
this.app.use("*", async (c, next) => {
|
|
518
|
+
if (SSE_PATHS.some((p) => c.req.path === p)) {
|
|
519
|
+
return next();
|
|
520
|
+
}
|
|
521
|
+
return timeout(3e4)(c, next);
|
|
522
|
+
});
|
|
523
|
+
this.app.use("*", async (c, next) => {
|
|
524
|
+
await next();
|
|
525
|
+
c.res.headers.set("X-Content-Type-Options", "nosniff");
|
|
526
|
+
c.res.headers.set("X-Frame-Options", "DENY");
|
|
527
|
+
c.res.headers.set("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
setupRoutes() {
|
|
531
|
+
this.app.get("/healthz", (c) => c.json({ status: "ok" }));
|
|
532
|
+
this.app.get("/readyz", (c) => {
|
|
533
|
+
const lifecycle = this.deps.lifecycle;
|
|
534
|
+
if (!lifecycle) {
|
|
535
|
+
return c.json({ status: "not_ready", reason: "lifecycle not initialized" }, 503);
|
|
536
|
+
}
|
|
537
|
+
const state = lifecycle.getState();
|
|
538
|
+
if (state === "running") {
|
|
539
|
+
return c.json({ status: "ready", state });
|
|
540
|
+
}
|
|
541
|
+
return c.json({ status: "not_ready", state }, 503);
|
|
542
|
+
});
|
|
543
|
+
const authMw = createAuthMiddleware({
|
|
544
|
+
keyHash: this.keyHash,
|
|
545
|
+
allowedIps: this.config.allowed_ips
|
|
546
|
+
});
|
|
547
|
+
this.app.use("/v1/*", authMw);
|
|
548
|
+
this.app.use("/v1/*", globalRateLimit);
|
|
549
|
+
this.app.use("/v1/*", mutatingRateLimit);
|
|
550
|
+
this.app.use("/v1/*", readRateLimit);
|
|
551
|
+
this.app.use("/v1/*", auditMiddleware);
|
|
552
|
+
this.app.get("/v1/openapi.json", (c) => {
|
|
553
|
+
return c.json({
|
|
554
|
+
openapi: "3.1.0",
|
|
555
|
+
info: {
|
|
556
|
+
title: "Teleton Management API",
|
|
557
|
+
version: "1.0.0",
|
|
558
|
+
description: "HTTPS management API for remote teleton agent administration"
|
|
559
|
+
},
|
|
560
|
+
servers: [{ url: `https://localhost:${this.config.port}` }]
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
const adaptedDeps = createDepsAdapter(this.deps);
|
|
564
|
+
this.app.route("/v1/status", createStatusRoutes(adaptedDeps));
|
|
565
|
+
this.app.route("/v1/tools", createToolsRoutes(adaptedDeps));
|
|
566
|
+
this.app.route("/v1/logs", createLogsRoutes(adaptedDeps));
|
|
567
|
+
this.app.route("/v1/memory", createMemoryRoutes(adaptedDeps));
|
|
568
|
+
this.app.route("/v1/soul", createSoulRoutes(adaptedDeps));
|
|
569
|
+
this.app.route("/v1/plugins", createPluginsRoutes(adaptedDeps));
|
|
570
|
+
this.app.route("/v1/mcp", createMcpRoutes(adaptedDeps));
|
|
571
|
+
this.app.route("/v1/workspace", createWorkspaceRoutes(adaptedDeps));
|
|
572
|
+
this.app.route("/v1/tasks", createTasksRoutes(adaptedDeps));
|
|
573
|
+
this.app.route("/v1/config", createConfigRoutes(adaptedDeps));
|
|
574
|
+
this.app.route("/v1/marketplace", createMarketplaceRoutes(adaptedDeps));
|
|
575
|
+
this.app.route("/v1/hooks", createHooksRoutes(adaptedDeps));
|
|
576
|
+
this.app.route("/v1/ton-proxy", createTonProxyRoutes(adaptedDeps));
|
|
577
|
+
this.app.route("/v1/setup", createSetupRoutes());
|
|
578
|
+
this.app.post("/v1/agent/start", async (c) => {
|
|
579
|
+
const lifecycle = this.deps.lifecycle;
|
|
580
|
+
if (!lifecycle) {
|
|
581
|
+
return c.json(
|
|
582
|
+
createProblem(503, "Service Unavailable", "Agent lifecycle not available"),
|
|
583
|
+
503,
|
|
584
|
+
{
|
|
585
|
+
"Content-Type": "application/problem+json"
|
|
586
|
+
}
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
const state = lifecycle.getState();
|
|
590
|
+
if (state === "running") {
|
|
591
|
+
return c.json({ state: "running" }, 409);
|
|
592
|
+
}
|
|
593
|
+
if (state === "stopping") {
|
|
594
|
+
return c.json(
|
|
595
|
+
createProblem(409, "Conflict", "Agent is currently stopping, please wait"),
|
|
596
|
+
409,
|
|
597
|
+
{
|
|
598
|
+
"Content-Type": "application/problem+json"
|
|
599
|
+
}
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
lifecycle.start().catch((err) => {
|
|
603
|
+
log3.error({ err }, "Agent start failed");
|
|
604
|
+
});
|
|
605
|
+
return c.json({ state: "starting" });
|
|
606
|
+
});
|
|
607
|
+
this.app.post("/v1/agent/stop", async (c) => {
|
|
608
|
+
const lifecycle = this.deps.lifecycle;
|
|
609
|
+
if (!lifecycle) {
|
|
610
|
+
return c.json(
|
|
611
|
+
createProblem(503, "Service Unavailable", "Agent lifecycle not available"),
|
|
612
|
+
503,
|
|
613
|
+
{
|
|
614
|
+
"Content-Type": "application/problem+json"
|
|
615
|
+
}
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
const state = lifecycle.getState();
|
|
619
|
+
if (state === "stopped") {
|
|
620
|
+
return c.json({ state: "stopped" }, 409);
|
|
621
|
+
}
|
|
622
|
+
if (state === "starting") {
|
|
623
|
+
return c.json(
|
|
624
|
+
createProblem(409, "Conflict", "Agent is currently starting, please wait"),
|
|
625
|
+
409,
|
|
626
|
+
{
|
|
627
|
+
"Content-Type": "application/problem+json"
|
|
628
|
+
}
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
lifecycle.stop().catch((err) => {
|
|
632
|
+
log3.error({ err }, "Agent stop failed");
|
|
633
|
+
});
|
|
634
|
+
return c.json({ state: "stopping" });
|
|
635
|
+
});
|
|
636
|
+
this.app.get("/v1/agent/status", (c) => {
|
|
637
|
+
const lifecycle = this.deps.lifecycle;
|
|
638
|
+
if (!lifecycle) {
|
|
639
|
+
return c.json(
|
|
640
|
+
createProblem(503, "Service Unavailable", "Agent lifecycle not available"),
|
|
641
|
+
503,
|
|
642
|
+
{
|
|
643
|
+
"Content-Type": "application/problem+json"
|
|
644
|
+
}
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
return c.json({
|
|
648
|
+
state: lifecycle.getState(),
|
|
649
|
+
uptime: lifecycle.getUptime(),
|
|
650
|
+
error: lifecycle.getError() ?? null
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
this.app.get("/v1/agent/events", (c) => {
|
|
654
|
+
const lifecycle = this.deps.lifecycle;
|
|
655
|
+
if (!lifecycle) {
|
|
656
|
+
return c.json(
|
|
657
|
+
createProblem(503, "Service Unavailable", "Agent lifecycle not available"),
|
|
658
|
+
503,
|
|
659
|
+
{
|
|
660
|
+
"Content-Type": "application/problem+json"
|
|
661
|
+
}
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
return streamSSE2(c, async (stream) => {
|
|
665
|
+
let aborted = false;
|
|
666
|
+
stream.onAbort(() => {
|
|
667
|
+
aborted = true;
|
|
668
|
+
});
|
|
669
|
+
const now = Date.now();
|
|
670
|
+
await stream.writeSSE({
|
|
671
|
+
event: "status",
|
|
672
|
+
id: String(now),
|
|
673
|
+
data: JSON.stringify({
|
|
674
|
+
state: lifecycle.getState(),
|
|
675
|
+
error: lifecycle.getError() ?? null,
|
|
676
|
+
timestamp: now
|
|
677
|
+
}),
|
|
678
|
+
retry: 3e3
|
|
679
|
+
});
|
|
680
|
+
const onStateChange = (event) => {
|
|
681
|
+
if (aborted) return;
|
|
682
|
+
void stream.writeSSE({
|
|
683
|
+
event: "status",
|
|
684
|
+
id: String(event.timestamp),
|
|
685
|
+
data: JSON.stringify({
|
|
686
|
+
state: event.state,
|
|
687
|
+
error: event.error ?? null,
|
|
688
|
+
timestamp: event.timestamp
|
|
689
|
+
})
|
|
690
|
+
});
|
|
691
|
+
};
|
|
692
|
+
lifecycle.on("stateChange", onStateChange);
|
|
693
|
+
while (!aborted) {
|
|
694
|
+
await stream.sleep(3e4);
|
|
695
|
+
if (aborted) break;
|
|
696
|
+
await stream.writeSSE({
|
|
697
|
+
event: "ping",
|
|
698
|
+
data: ""
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
lifecycle.off("stateChange", onStateChange);
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
this.app.route("/v1/agent", createAgentRoutes(this.deps.lifecycle));
|
|
705
|
+
this.app.route("/v1/system", createSystemRoutes());
|
|
706
|
+
this.app.route("/v1/auth", createAuthRoutes());
|
|
707
|
+
this.app.route("/v1/api-logs", createApiLogsRoutes());
|
|
708
|
+
this.app.route(
|
|
709
|
+
"/v1/api-memory",
|
|
710
|
+
createApiMemoryRoutes(() => this.deps.memory?.db ?? null)
|
|
711
|
+
);
|
|
712
|
+
this.app.onError((err, c) => {
|
|
713
|
+
log3.error({ err }, "Management API error");
|
|
714
|
+
if (err instanceof HTTPException3) {
|
|
715
|
+
if (err.res) return err.res;
|
|
716
|
+
return c.json(createProblem(err.status, err.message || "Error"), err.status, {
|
|
717
|
+
"Content-Type": "application/problem+json"
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
return c.json(
|
|
721
|
+
createProblem(500, "Internal Server Error", err.message || "An unexpected error occurred"),
|
|
722
|
+
500,
|
|
723
|
+
{ "Content-Type": "application/problem+json" }
|
|
724
|
+
);
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
async start() {
|
|
728
|
+
const tls = await ensureTlsCert(TELETON_ROOT);
|
|
729
|
+
this.tls = tls;
|
|
730
|
+
this.setupMiddleware();
|
|
731
|
+
this.setupRoutes();
|
|
732
|
+
return new Promise((resolve, reject) => {
|
|
733
|
+
try {
|
|
734
|
+
this.server = serve(
|
|
735
|
+
{
|
|
736
|
+
fetch: this.app.fetch,
|
|
737
|
+
port: this.config.port,
|
|
738
|
+
createServer: createHttpsServer,
|
|
739
|
+
serverOptions: {
|
|
740
|
+
cert: tls.cert,
|
|
741
|
+
key: tls.key
|
|
742
|
+
}
|
|
743
|
+
},
|
|
744
|
+
(info) => {
|
|
745
|
+
this.server.maxConnections = 20;
|
|
746
|
+
log3.info(`Management API server running on https://localhost:${info.port}`);
|
|
747
|
+
if (this.apiKey) {
|
|
748
|
+
log3.info(
|
|
749
|
+
`API key: ${KEY_PREFIX}${this.apiKey.slice(KEY_PREFIX.length, KEY_PREFIX.length + 4)}...`
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
log3.info(`TLS fingerprint: ${tls.fingerprint.slice(0, 16)}...`);
|
|
753
|
+
resolve();
|
|
754
|
+
}
|
|
755
|
+
);
|
|
756
|
+
this.server.on("error", (err) => {
|
|
757
|
+
log3.error({ err }, "Management API server error");
|
|
758
|
+
reject(err);
|
|
759
|
+
});
|
|
760
|
+
} catch (error) {
|
|
761
|
+
reject(error);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
async stop() {
|
|
766
|
+
if (this.server) {
|
|
767
|
+
return new Promise((resolve) => {
|
|
768
|
+
this.server.closeAllConnections();
|
|
769
|
+
this.server.close(() => {
|
|
770
|
+
log3.info("Management API server stopped");
|
|
771
|
+
resolve();
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
getCredentials() {
|
|
777
|
+
if (!this.tls) return null;
|
|
778
|
+
return {
|
|
779
|
+
apiKey: this.apiKey ?? "",
|
|
780
|
+
fingerprint: this.tls.fingerprint,
|
|
781
|
+
port: this.config.port
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
export {
|
|
786
|
+
ApiServer
|
|
787
|
+
};
|