doer-agent 0.7.6 → 0.7.7
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/dist/agent-http-proxy-rpc.js +168 -24
- package/package.json +1 -1
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
1
|
+
import { appendFile, readFile, writeFile, mkdir, unlink } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { StringCodec } from "nats";
|
|
4
4
|
const proxyRpcCodec = StringCodec();
|
|
5
5
|
const PROXY_ID_PATTERN = /^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$/;
|
|
6
6
|
const MAX_PROXY_BODY_BYTES = 5 * 1024 * 1024;
|
|
7
|
+
const MAX_PROXY_LOGS = 100;
|
|
7
8
|
function getProxyRegistryPath(workspaceRoot) {
|
|
8
9
|
return path.join(workspaceRoot, ".doer-agent", "http-proxies.json");
|
|
9
10
|
}
|
|
11
|
+
function getProxyLogsPath(workspaceRoot, proxyId) {
|
|
12
|
+
return path.join(workspaceRoot, ".doer-agent", "http-proxy-logs", `${proxyId}.jsonl`);
|
|
13
|
+
}
|
|
10
14
|
function slugify(value) {
|
|
11
15
|
const slug = value
|
|
12
16
|
.trim()
|
|
@@ -91,10 +95,39 @@ function normalizeProxyRecord(value) {
|
|
|
91
95
|
name: normalizeName(row.name),
|
|
92
96
|
host,
|
|
93
97
|
port,
|
|
98
|
+
enabled: row.enabled !== false,
|
|
94
99
|
createdAt,
|
|
95
100
|
updatedAt,
|
|
96
101
|
};
|
|
97
102
|
}
|
|
103
|
+
function normalizeProxyLogRecord(value) {
|
|
104
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const row = value;
|
|
108
|
+
const id = typeof row.id === "string" && row.id.trim() ? row.id.trim() : "";
|
|
109
|
+
const at = typeof row.at === "string" && row.at.trim() ? row.at.trim() : "";
|
|
110
|
+
const method = typeof row.method === "string" && row.method.trim() ? row.method.trim().toUpperCase() : "";
|
|
111
|
+
const requestPath = typeof row.path === "string" && row.path.trim() ? row.path.trim() : "/";
|
|
112
|
+
const status = typeof row.status === "number" && Number.isInteger(row.status) ? row.status : null;
|
|
113
|
+
const durationMs = typeof row.durationMs === "number" && Number.isFinite(row.durationMs) ? Math.max(0, Math.round(row.durationMs)) : 0;
|
|
114
|
+
const requestBytes = typeof row.requestBytes === "number" && Number.isFinite(row.requestBytes) ? Math.max(0, Math.round(row.requestBytes)) : 0;
|
|
115
|
+
const responseBytes = typeof row.responseBytes === "number" && Number.isFinite(row.responseBytes) ? Math.max(0, Math.round(row.responseBytes)) : 0;
|
|
116
|
+
if (!id || !at || !method) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
id,
|
|
121
|
+
at,
|
|
122
|
+
method,
|
|
123
|
+
path: requestPath,
|
|
124
|
+
status,
|
|
125
|
+
durationMs,
|
|
126
|
+
requestBytes,
|
|
127
|
+
responseBytes,
|
|
128
|
+
error: typeof row.error === "string" && row.error.trim() ? row.error.trim().slice(0, 500) : null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
98
131
|
async function readProxyRegistry(workspaceRoot) {
|
|
99
132
|
const raw = await readFile(getProxyRegistryPath(workspaceRoot), "utf8").catch(() => "");
|
|
100
133
|
if (!raw) {
|
|
@@ -118,6 +151,33 @@ async function writeProxyRegistry(workspaceRoot, proxies) {
|
|
|
118
151
|
await mkdir(path.dirname(registryPath), { recursive: true });
|
|
119
152
|
await writeFile(registryPath, `${JSON.stringify({ proxies }, null, 2)}\n`, "utf8");
|
|
120
153
|
}
|
|
154
|
+
function normalizeLimit(value, fallback) {
|
|
155
|
+
const numeric = Number(value);
|
|
156
|
+
if (!Number.isFinite(numeric)) {
|
|
157
|
+
return fallback;
|
|
158
|
+
}
|
|
159
|
+
return Math.max(1, Math.min(Math.floor(numeric), 1000));
|
|
160
|
+
}
|
|
161
|
+
async function readProxyLogs(workspaceRoot, proxyId, limit) {
|
|
162
|
+
const raw = await readFile(getProxyLogsPath(workspaceRoot, proxyId), "utf8").catch(() => "");
|
|
163
|
+
if (!raw) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
return raw
|
|
167
|
+
.split("\n")
|
|
168
|
+
.map((line) => line.trim())
|
|
169
|
+
.filter(Boolean)
|
|
170
|
+
.slice(-limit)
|
|
171
|
+
.map((line) => {
|
|
172
|
+
try {
|
|
173
|
+
return normalizeProxyLogRecord(JSON.parse(line));
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
.filter((log) => Boolean(log));
|
|
180
|
+
}
|
|
121
181
|
async function createProxy(workspaceRoot, request) {
|
|
122
182
|
const proxies = await readProxyRegistry(workspaceRoot);
|
|
123
183
|
const name = normalizeName(request.name);
|
|
@@ -136,54 +196,132 @@ async function createProxy(workspaceRoot, request) {
|
|
|
136
196
|
name,
|
|
137
197
|
host,
|
|
138
198
|
port,
|
|
199
|
+
enabled: request.enabled !== false,
|
|
139
200
|
createdAt: now,
|
|
140
201
|
updatedAt: now,
|
|
141
202
|
};
|
|
142
203
|
await writeProxyRegistry(workspaceRoot, [...proxies, proxy].sort((a, b) => b.createdAt.localeCompare(a.createdAt)));
|
|
143
204
|
return proxy;
|
|
144
205
|
}
|
|
206
|
+
async function updateProxy(workspaceRoot, request) {
|
|
207
|
+
const proxyId = normalizeProxyId(request.proxyId);
|
|
208
|
+
const proxies = await readProxyRegistry(workspaceRoot);
|
|
209
|
+
const proxy = proxies.find((item) => item.id === proxyId);
|
|
210
|
+
if (!proxy) {
|
|
211
|
+
throw new Error("proxy not found");
|
|
212
|
+
}
|
|
213
|
+
const updated = {
|
|
214
|
+
...proxy,
|
|
215
|
+
name: request.name === undefined ? proxy.name : normalizeName(request.name),
|
|
216
|
+
host: request.host === undefined ? proxy.host : normalizeHost(request.host),
|
|
217
|
+
port: request.port === undefined ? proxy.port : normalizePort(request.port),
|
|
218
|
+
enabled: request.enabled === undefined ? proxy.enabled : request.enabled !== false,
|
|
219
|
+
updatedAt: new Date().toISOString(),
|
|
220
|
+
};
|
|
221
|
+
await writeProxyRegistry(workspaceRoot, proxies.map((item) => (item.id === proxyId ? updated : item)));
|
|
222
|
+
return updated;
|
|
223
|
+
}
|
|
145
224
|
async function deleteProxy(workspaceRoot, proxyId) {
|
|
146
225
|
const proxies = await readProxyRegistry(workspaceRoot);
|
|
147
226
|
await writeProxyRegistry(workspaceRoot, proxies.filter((proxy) => proxy.id !== proxyId));
|
|
227
|
+
await unlink(getProxyLogsPath(workspaceRoot, proxyId)).catch(() => undefined);
|
|
148
228
|
}
|
|
149
|
-
async function
|
|
229
|
+
async function appendProxyLog(workspaceRoot, proxyId, log) {
|
|
230
|
+
const entry = {
|
|
231
|
+
id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
|
|
232
|
+
at: new Date().toISOString(),
|
|
233
|
+
...log,
|
|
234
|
+
};
|
|
235
|
+
const logsPath = getProxyLogsPath(workspaceRoot, proxyId);
|
|
236
|
+
await mkdir(path.dirname(logsPath), { recursive: true });
|
|
237
|
+
await appendFile(logsPath, `${JSON.stringify(entry)}\n`, "utf8");
|
|
238
|
+
}
|
|
239
|
+
async function readProxyLogsForRequest(workspaceRoot, request) {
|
|
150
240
|
const proxyId = normalizeProxyId(request.proxyId);
|
|
151
241
|
const proxy = (await readProxyRegistry(workspaceRoot)).find((item) => item.id === proxyId);
|
|
152
242
|
if (!proxy) {
|
|
153
243
|
throw new Error("proxy not found");
|
|
154
244
|
}
|
|
245
|
+
return {
|
|
246
|
+
proxy,
|
|
247
|
+
events: await readProxyLogs(workspaceRoot, proxyId, normalizeLimit(request.limit, MAX_PROXY_LOGS)),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
async function handleProxyFetch(workspaceRoot, request) {
|
|
251
|
+
const proxyId = normalizeProxyId(request.proxyId);
|
|
252
|
+
const proxy = (await readProxyRegistry(workspaceRoot)).find((item) => item.id === proxyId);
|
|
253
|
+
const startedAt = Date.now();
|
|
155
254
|
const method = normalizeMethod(request.method);
|
|
156
255
|
const requestPath = normalizePath(request.path);
|
|
157
|
-
const headers = normalizeHeaders(request.headers);
|
|
158
256
|
const bodyBase64 = typeof request.bodyBase64 === "string" ? request.bodyBase64 : "";
|
|
159
257
|
const body = bodyBase64 ? Buffer.from(bodyBase64, "base64") : undefined;
|
|
258
|
+
const requestBytes = body?.byteLength ?? 0;
|
|
259
|
+
let status = null;
|
|
260
|
+
let responseBytes = 0;
|
|
261
|
+
let errorMessage = null;
|
|
262
|
+
const finishLog = async () => {
|
|
263
|
+
await appendProxyLog(workspaceRoot, proxyId, {
|
|
264
|
+
method,
|
|
265
|
+
path: requestPath,
|
|
266
|
+
status,
|
|
267
|
+
durationMs: Date.now() - startedAt,
|
|
268
|
+
requestBytes,
|
|
269
|
+
responseBytes,
|
|
270
|
+
error: errorMessage,
|
|
271
|
+
}).catch(() => undefined);
|
|
272
|
+
};
|
|
273
|
+
if (!proxy) {
|
|
274
|
+
throw new Error("proxy not found");
|
|
275
|
+
}
|
|
276
|
+
if (!proxy.enabled) {
|
|
277
|
+
errorMessage = "proxy disabled";
|
|
278
|
+
await finishLog();
|
|
279
|
+
throw new Error("proxy disabled");
|
|
280
|
+
}
|
|
281
|
+
const headers = normalizeHeaders(request.headers);
|
|
160
282
|
if ((body?.byteLength ?? 0) > MAX_PROXY_BODY_BYTES) {
|
|
283
|
+
errorMessage = "proxy request body too large";
|
|
284
|
+
await finishLog();
|
|
161
285
|
throw new Error("proxy request body too large");
|
|
162
286
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
287
|
+
try {
|
|
288
|
+
const url = new URL(requestPath, `http://${proxy.host}:${proxy.port}`);
|
|
289
|
+
const response = await fetch(url, {
|
|
290
|
+
method,
|
|
291
|
+
headers,
|
|
292
|
+
body: method === "GET" || method === "HEAD" ? undefined : body,
|
|
293
|
+
redirect: "manual",
|
|
294
|
+
});
|
|
295
|
+
status = response.status;
|
|
296
|
+
const responseBuffer = Buffer.from(await response.arrayBuffer());
|
|
297
|
+
responseBytes = responseBuffer.byteLength;
|
|
298
|
+
if (responseBuffer.byteLength > MAX_PROXY_BODY_BYTES) {
|
|
299
|
+
errorMessage = "proxy response body too large";
|
|
300
|
+
await finishLog();
|
|
301
|
+
throw new Error("proxy response body too large");
|
|
302
|
+
}
|
|
303
|
+
const responseHeaders = {};
|
|
304
|
+
response.headers.forEach((value, key) => {
|
|
305
|
+
responseHeaders[key] = value;
|
|
306
|
+
});
|
|
307
|
+
await finishLog();
|
|
308
|
+
return {
|
|
309
|
+
status: response.status,
|
|
310
|
+
statusText: response.statusText,
|
|
311
|
+
headers: responseHeaders,
|
|
312
|
+
bodyBase64: responseBuffer.toString("base64"),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
if (!errorMessage) {
|
|
317
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
318
|
+
await finishLog();
|
|
319
|
+
}
|
|
320
|
+
throw error;
|
|
173
321
|
}
|
|
174
|
-
const responseHeaders = {};
|
|
175
|
-
response.headers.forEach((value, key) => {
|
|
176
|
-
responseHeaders[key] = value;
|
|
177
|
-
});
|
|
178
|
-
return {
|
|
179
|
-
status: response.status,
|
|
180
|
-
statusText: response.statusText,
|
|
181
|
-
headers: responseHeaders,
|
|
182
|
-
bodyBase64: responseBuffer.toString("base64"),
|
|
183
|
-
};
|
|
184
322
|
}
|
|
185
323
|
async function executeProxyRpc(args) {
|
|
186
|
-
const action = args.request.action === "create" || args.request.action === "delete" || args.request.action === "handle"
|
|
324
|
+
const action = args.request.action === "create" || args.request.action === "update" || args.request.action === "delete" || args.request.action === "logs" || args.request.action === "handle"
|
|
187
325
|
? args.request.action
|
|
188
326
|
: "list";
|
|
189
327
|
if (action === "list") {
|
|
@@ -192,10 +330,16 @@ async function executeProxyRpc(args) {
|
|
|
192
330
|
if (action === "create") {
|
|
193
331
|
return { ok: true, action, proxy: await createProxy(args.workspaceRoot, args.request) };
|
|
194
332
|
}
|
|
333
|
+
if (action === "update") {
|
|
334
|
+
return { ok: true, action, proxy: await updateProxy(args.workspaceRoot, args.request) };
|
|
335
|
+
}
|
|
195
336
|
if (action === "delete") {
|
|
196
337
|
await deleteProxy(args.workspaceRoot, normalizeProxyId(args.request.proxyId));
|
|
197
338
|
return { ok: true, action };
|
|
198
339
|
}
|
|
340
|
+
if (action === "logs") {
|
|
341
|
+
return { ok: true, action, ...await readProxyLogsForRequest(args.workspaceRoot, args.request) };
|
|
342
|
+
}
|
|
199
343
|
return { ok: true, action, response: await handleProxyFetch(args.workspaceRoot, args.request) };
|
|
200
344
|
}
|
|
201
345
|
export async function handleHttpProxyRpcMessage(args) {
|