agent-hitch 0.1.0 → 0.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 +240 -126
- package/README.zh-CN.md +330 -0
- package/dist/bin/hitch.js +9 -0
- package/dist/bin/hitch.js.map +1 -0
- package/dist/scripts/check-release.js +25 -0
- package/dist/scripts/check-release.js.map +1 -0
- package/dist/scripts/check-syntax.js +32 -0
- package/dist/scripts/check-syntax.js.map +1 -0
- package/dist/src/adapters.js +461 -0
- package/dist/src/adapters.js.map +1 -0
- package/dist/src/artifacts.js +1081 -0
- package/dist/src/artifacts.js.map +1 -0
- package/dist/src/cli.js +699 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.js +53 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/controller-runtime/hash.js +353 -0
- package/dist/src/controller-runtime/hash.js.map +1 -0
- package/dist/src/controller-runtime/store.js +323 -0
- package/dist/src/controller-runtime/store.js.map +1 -0
- package/dist/src/daemon.js +461 -0
- package/dist/src/daemon.js.map +1 -0
- package/dist/src/domain/types.js +10 -0
- package/dist/src/domain/types.js.map +1 -0
- package/dist/src/domain/validate.js +240 -0
- package/dist/src/domain/validate.js.map +1 -0
- package/dist/src/engine.js +465 -0
- package/dist/src/engine.js.map +1 -0
- package/dist/src/errors.js +14 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/eval-tools.js +318 -0
- package/dist/src/eval-tools.js.map +1 -0
- package/dist/src/evals.js +313 -0
- package/dist/src/evals.js.map +1 -0
- package/dist/src/events.js +80 -0
- package/dist/src/events.js.map +1 -0
- package/dist/src/feedback/service.js +315 -0
- package/dist/src/feedback/service.js.map +1 -0
- package/dist/src/fs.js +45 -0
- package/dist/src/fs.js.map +1 -0
- package/dist/src/harbor-backend.js +280 -0
- package/dist/src/harbor-backend.js.map +1 -0
- package/dist/src/harness-reference.js +83 -0
- package/dist/src/harness-reference.js.map +1 -0
- package/dist/src/line-stream.js +20 -0
- package/dist/src/line-stream.js.map +1 -0
- package/dist/src/locks.js +38 -0
- package/dist/src/locks.js.map +1 -0
- package/dist/src/package-root.js +40 -0
- package/dist/src/package-root.js.map +1 -0
- package/dist/src/process.js +50 -0
- package/dist/src/process.js.map +1 -0
- package/dist/src/registry.js +81 -0
- package/dist/src/registry.js.map +1 -0
- package/dist/src/scheduler.js +162 -0
- package/dist/src/scheduler.js.map +1 -0
- package/dist/src/trajectories/contract.js +17 -0
- package/dist/src/trajectories/contract.js.map +1 -0
- package/dist/src/trajectories/format.js +127 -0
- package/dist/src/trajectories/format.js.map +1 -0
- package/dist/src/trajectories/projector.js +385 -0
- package/dist/src/trajectories/projector.js.map +1 -0
- package/dist/src/trajectories/store.js +281 -0
- package/dist/src/trajectories/store.js.map +1 -0
- package/dist/src/workspaces.js +858 -0
- package/dist/src/workspaces.js.map +1 -0
- package/docs/schemas/controller-runtime-manifest.schema.json +57 -0
- package/docs/schemas/controller-runtime-ref.schema.json +16 -0
- package/docs/schemas/message-feedback.schema.json +37 -0
- package/docs/schemas/trajectory-ref.schema.json +30 -0
- package/integrations/harbor/hitch_harbor_agent.py +176 -4
- package/package.json +19 -11
- package/bin/hitch.js +0 -9
- package/src/adapters.js +0 -361
- package/src/artifacts.js +0 -948
- package/src/cli.js +0 -505
- package/src/config.js +0 -51
- package/src/daemon.js +0 -416
- package/src/engine.js +0 -386
- package/src/errors.js +0 -12
- package/src/eval-tools.js +0 -331
- package/src/evals.js +0 -276
- package/src/events.js +0 -69
- package/src/fs.js +0 -43
- package/src/harbor-backend.js +0 -285
- package/src/harness-reference.js +0 -78
- package/src/line-stream.js +0 -17
- package/src/locks.js +0 -33
- package/src/process.js +0 -49
- package/src/registry.js +0 -84
- package/src/scheduler.js +0 -156
- package/src/workspaces.js +0 -893
package/src/daemon.js
DELETED
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { createServer } from "node:http";
|
|
3
|
-
import { createReadStream } from "node:fs";
|
|
4
|
-
import { open, readFile, rm, stat } from "node:fs/promises";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { Scheduler } from "./scheduler.js";
|
|
7
|
-
import { atomicWriteJSON, ensureDir, readJSON, removeIfExists, writePrivateFile } from "./fs.js";
|
|
8
|
-
import { discoverAgents } from "./registry.js";
|
|
9
|
-
import { SCHEMA_VERSION, statePaths } from "./config.js";
|
|
10
|
-
import { HitchError, invalidInput } from "./errors.js";
|
|
11
|
-
import { reclaimStaleLock } from "./locks.js";
|
|
12
|
-
|
|
13
|
-
export class DaemonServer {
|
|
14
|
-
constructor({ root, port, maxConcurrent, logger = defaultLogger }) {
|
|
15
|
-
this.paths = statePaths(root);
|
|
16
|
-
this.rootId = createHash("sha256").update(this.paths.root).digest("hex").slice(0, 24);
|
|
17
|
-
this.instanceId = randomBytes(16).toString("hex");
|
|
18
|
-
this.port = port;
|
|
19
|
-
this.maxConcurrent = maxConcurrent;
|
|
20
|
-
this.logger = logger;
|
|
21
|
-
this.startedAt = new Date();
|
|
22
|
-
this.ready = false;
|
|
23
|
-
this.closing = false;
|
|
24
|
-
this.closed = new Promise((resolve) => { this.resolveClosed = resolve; });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async start() {
|
|
28
|
-
await ensureDir(this.paths.root);
|
|
29
|
-
await acquireInstanceLock(this.paths.lock, this.instanceId);
|
|
30
|
-
this.ownsLock = true;
|
|
31
|
-
try {
|
|
32
|
-
this.token = await ensureToken(this.paths.token);
|
|
33
|
-
this.agents = await discoverAgents();
|
|
34
|
-
this.scheduler = new Scheduler({
|
|
35
|
-
runsRoot: this.paths.runs,
|
|
36
|
-
root: this.paths.root,
|
|
37
|
-
maxConcurrent: this.maxConcurrent,
|
|
38
|
-
onEvent: (event) => this.logger("event", { type: event.type, run_id: event.run_id }),
|
|
39
|
-
});
|
|
40
|
-
await this.scheduler.initialize();
|
|
41
|
-
|
|
42
|
-
this.server = createServer((request, response) => {
|
|
43
|
-
this.handle(request, response).catch((error) => {
|
|
44
|
-
this.logger("request_error", { error: error.message, path: request.url });
|
|
45
|
-
const status = errorStatus(error);
|
|
46
|
-
json(response, status, {
|
|
47
|
-
error: {
|
|
48
|
-
code: error.code || "internal_error",
|
|
49
|
-
message: error.message,
|
|
50
|
-
exit_code: Number.isInteger(error?.exitCode) ? error.exitCode : 12,
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
this.server.requestTimeout = 30_000;
|
|
56
|
-
this.server.headersTimeout = 10_000;
|
|
57
|
-
|
|
58
|
-
await new Promise((resolve, reject) => {
|
|
59
|
-
this.server.once("error", reject);
|
|
60
|
-
this.server.listen(this.port, "127.0.0.1", resolve);
|
|
61
|
-
});
|
|
62
|
-
this.port = this.server.address().port;
|
|
63
|
-
await atomicWriteJSON(this.paths.daemon, {
|
|
64
|
-
schema_version: SCHEMA_VERSION,
|
|
65
|
-
pid: process.pid,
|
|
66
|
-
port: this.port,
|
|
67
|
-
root_id: this.rootId,
|
|
68
|
-
instance_id: this.instanceId,
|
|
69
|
-
root: this.paths.root,
|
|
70
|
-
started_at: this.startedAt.toISOString(),
|
|
71
|
-
});
|
|
72
|
-
this.ready = true;
|
|
73
|
-
this.logger("started", { pid: process.pid, port: this.port, instance_id: this.instanceId });
|
|
74
|
-
return this;
|
|
75
|
-
} catch (error) {
|
|
76
|
-
if (this.server?.listening) await new Promise((resolve) => this.server.close(resolve));
|
|
77
|
-
await this.scheduler?.shutdown().catch(() => {});
|
|
78
|
-
await releaseInstanceLock(this.paths.lock, this.instanceId);
|
|
79
|
-
this.ownsLock = false;
|
|
80
|
-
throw error;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async close() {
|
|
85
|
-
if (this.closing) return this.closed;
|
|
86
|
-
this.closing = true;
|
|
87
|
-
this.ready = false;
|
|
88
|
-
let failure;
|
|
89
|
-
try {
|
|
90
|
-
await this.scheduler?.shutdown();
|
|
91
|
-
if (this.server) await new Promise((resolve) => this.server.close(resolve));
|
|
92
|
-
} catch (error) {
|
|
93
|
-
failure = error;
|
|
94
|
-
} finally {
|
|
95
|
-
try {
|
|
96
|
-
const state = await readJSON(this.paths.daemon, null);
|
|
97
|
-
if (state?.instance_id === this.instanceId) await removeIfExists(this.paths.daemon);
|
|
98
|
-
if (this.ownsLock) {
|
|
99
|
-
await releaseInstanceLock(this.paths.lock, this.instanceId);
|
|
100
|
-
this.ownsLock = false;
|
|
101
|
-
}
|
|
102
|
-
} catch (cleanupError) {
|
|
103
|
-
failure ||= cleanupError;
|
|
104
|
-
}
|
|
105
|
-
this.logger("stopped", { pid: process.pid, instance_id: this.instanceId });
|
|
106
|
-
this.resolveClosed();
|
|
107
|
-
}
|
|
108
|
-
if (failure) throw failure;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async handle(request, response) {
|
|
112
|
-
const url = new URL(request.url, `http://127.0.0.1:${this.port}`);
|
|
113
|
-
if (request.method === "GET" && url.pathname === "/health") {
|
|
114
|
-
return json(response, 200, this.health());
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (!authorized(request, this.token)) {
|
|
118
|
-
return json(response, 401, { error: { code: "unauthorized", message: "missing or invalid daemon token" } });
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (request.method === "GET" && ["/v1/harnesses", "/v1/agents"].includes(url.pathname)) {
|
|
122
|
-
this.agents = await discoverAgents();
|
|
123
|
-
const key = url.pathname === "/v1/agents" ? "agents" : "harnesses";
|
|
124
|
-
return json(response, 200, { schema_version: SCHEMA_VERSION, [key]: this.agents });
|
|
125
|
-
}
|
|
126
|
-
if (request.method === "POST" && url.pathname === "/v1/runs") {
|
|
127
|
-
const requestBody = await readBodyJSON(request);
|
|
128
|
-
const runId = await this.scheduler.submit(requestBody);
|
|
129
|
-
return json(response, 202, { schema_version: SCHEMA_VERSION, run_id: runId, status: "queued" });
|
|
130
|
-
}
|
|
131
|
-
if (request.method === "POST" && url.pathname === "/shutdown") {
|
|
132
|
-
json(response, 202, { schema_version: SCHEMA_VERSION, status: "shutting_down" });
|
|
133
|
-
setImmediate(() => this.close().catch((error) => this.logger("shutdown_error", { error: error.message })));
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const runMatch = url.pathname.match(/^\/v1\/runs\/(run_[a-f0-9]+)(?:\/(events|cancel))?$/);
|
|
138
|
-
if (runMatch) {
|
|
139
|
-
const [, runId, action] = runMatch;
|
|
140
|
-
if (request.method === "GET" && !action) {
|
|
141
|
-
const status = await this.scheduler.status(runId);
|
|
142
|
-
return status
|
|
143
|
-
? json(response, 200, { schema_version: SCHEMA_VERSION, ...status })
|
|
144
|
-
: json(response, 404, { error: { code: "run_not_found", message: `run not found: ${runId}` } });
|
|
145
|
-
}
|
|
146
|
-
if (request.method === "GET" && action === "events") {
|
|
147
|
-
const offsetValue = url.searchParams.get("offset") || "0";
|
|
148
|
-
if (!/^\d+$/.test(offsetValue)) throw invalidInput("events offset must be a non-negative integer");
|
|
149
|
-
const offset = Number(offsetValue);
|
|
150
|
-
if (!Number.isSafeInteger(offset)) throw invalidInput("events offset is too large");
|
|
151
|
-
const eventsPath = path.join(this.paths.runs, runId, "events.jsonl");
|
|
152
|
-
try {
|
|
153
|
-
const info = await stat(eventsPath);
|
|
154
|
-
if (offset > info.size) throw invalidInput(`events offset ${offset} exceeds current size ${info.size}`);
|
|
155
|
-
const completeSize = await completeLineSize(eventsPath, info.size);
|
|
156
|
-
if (offset > completeSize) {
|
|
157
|
-
throw invalidInput(`events offset ${offset} exceeds committed size ${completeSize}`);
|
|
158
|
-
}
|
|
159
|
-
if (!await isLineBoundary(eventsPath, offset)) {
|
|
160
|
-
throw invalidInput(`events offset ${offset} is not at an event boundary`);
|
|
161
|
-
}
|
|
162
|
-
response.writeHead(200, {
|
|
163
|
-
"content-type": "application/x-ndjson",
|
|
164
|
-
"x-hitch-next-offset": String(completeSize),
|
|
165
|
-
"accept-ranges": "bytes",
|
|
166
|
-
});
|
|
167
|
-
if (offset === completeSize) {
|
|
168
|
-
response.end();
|
|
169
|
-
} else {
|
|
170
|
-
await streamFileRange(eventsPath, offset, completeSize - 1, response);
|
|
171
|
-
}
|
|
172
|
-
} catch (error) {
|
|
173
|
-
if (error?.code === "ENOENT") return json(response, 404, { error: { code: "events_not_found", message: "events not available" } });
|
|
174
|
-
if (response.headersSent) {
|
|
175
|
-
response.destroy(error);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
throw error;
|
|
179
|
-
}
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
if (request.method === "POST" && action === "cancel") {
|
|
183
|
-
const cancelled = await this.scheduler.cancel(runId);
|
|
184
|
-
return cancelled
|
|
185
|
-
? json(response, 202, { schema_version: SCHEMA_VERSION, run_id: runId, status: "cancelling" })
|
|
186
|
-
: json(response, 409, { error: { code: "not_cancellable", message: "run is not queued or running" } });
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
json(response, 404, { error: { code: "not_found", message: "endpoint not found" } });
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
health() {
|
|
194
|
-
return {
|
|
195
|
-
schema_version: SCHEMA_VERSION,
|
|
196
|
-
status: this.ready ? "running" : this.closing ? "stopping" : "starting",
|
|
197
|
-
pid: process.pid,
|
|
198
|
-
port: this.port,
|
|
199
|
-
instance_id: this.instanceId,
|
|
200
|
-
root_id: this.rootId,
|
|
201
|
-
uptime_seconds: Math.floor((Date.now() - this.startedAt.getTime()) / 1000),
|
|
202
|
-
agents: this.agents?.filter((agent) => agent.status === "available").map((agent) => agent.id) || [],
|
|
203
|
-
harnesses: this.agents?.filter((agent) => agent.status === "available").map((agent) => agent.id) || [],
|
|
204
|
-
scheduler: this.scheduler?.snapshot() || null,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export async function daemonClient(root) {
|
|
210
|
-
const paths = statePaths(root);
|
|
211
|
-
const state = await readJSON(paths.daemon, null);
|
|
212
|
-
if (!state?.port) throw new Error("daemon is not running");
|
|
213
|
-
const token = (await readFile(paths.token, "utf8")).trim();
|
|
214
|
-
const performRequest = async (pathname, options = {}) => {
|
|
215
|
-
const headers = { ...options.headers, authorization: `Bearer ${token}` };
|
|
216
|
-
if (options.body && !headers["content-type"]) headers["content-type"] = "application/json";
|
|
217
|
-
const response = await fetch(`http://127.0.0.1:${state.port}${pathname}`, { ...options, headers });
|
|
218
|
-
const contentType = response.headers.get("content-type") || "";
|
|
219
|
-
const mediaType = contentType.split(";", 1)[0].trim().toLowerCase();
|
|
220
|
-
const isJSONDocument = mediaType === "application/json" || mediaType.endsWith("+json");
|
|
221
|
-
const payload = isJSONDocument ? await response.json() : await response.text();
|
|
222
|
-
if (!response.ok) {
|
|
223
|
-
const message = payload?.error?.message || `daemon request failed (${response.status})`;
|
|
224
|
-
const error = new HitchError(message, {
|
|
225
|
-
code: payload?.error?.code || httpErrorCode(response.status),
|
|
226
|
-
exitCode: Number.isInteger(payload?.error?.exit_code)
|
|
227
|
-
? payload.error.exit_code
|
|
228
|
-
: httpExitCode(response.status),
|
|
229
|
-
});
|
|
230
|
-
error.status = response.status;
|
|
231
|
-
throw error;
|
|
232
|
-
}
|
|
233
|
-
return { payload, headers: response.headers, status: response.status };
|
|
234
|
-
};
|
|
235
|
-
const request = async (pathname, options) => (await performRequest(pathname, options)).payload;
|
|
236
|
-
return { state, request, requestWithMetadata: performRequest };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
async function acquireInstanceLock(file, instanceId) {
|
|
240
|
-
const owner = {
|
|
241
|
-
schema_version: SCHEMA_VERSION,
|
|
242
|
-
instance_id: instanceId,
|
|
243
|
-
pid: process.pid,
|
|
244
|
-
created_at: new Date().toISOString(),
|
|
245
|
-
};
|
|
246
|
-
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
247
|
-
try {
|
|
248
|
-
const handle = await open(file, "wx", 0o600);
|
|
249
|
-
try {
|
|
250
|
-
await handle.writeFile(`${JSON.stringify(owner)}\n`);
|
|
251
|
-
await handle.sync();
|
|
252
|
-
} finally {
|
|
253
|
-
await handle.close();
|
|
254
|
-
}
|
|
255
|
-
return;
|
|
256
|
-
} catch (error) {
|
|
257
|
-
if (error?.code !== "EEXIST") throw error;
|
|
258
|
-
let existing;
|
|
259
|
-
try {
|
|
260
|
-
existing = JSON.parse(await readFile(file, "utf8"));
|
|
261
|
-
} catch (readError) {
|
|
262
|
-
if (attempt < 3) {
|
|
263
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
throw new HitchError("daemon lock exists but its owner record is unreadable", { code: "daemon_lock_invalid", exitCode: 12, cause: readError });
|
|
267
|
-
}
|
|
268
|
-
if (processIsAlive(existing.pid)) {
|
|
269
|
-
throw new HitchError(`another daemon owns this root (pid ${existing.pid})`, { code: "already_running", exitCode: 2 });
|
|
270
|
-
}
|
|
271
|
-
const reclaimed = await reclaimStaleLock(file, async (candidate) => {
|
|
272
|
-
let current;
|
|
273
|
-
try {
|
|
274
|
-
current = JSON.parse(await readFile(candidate, "utf8"));
|
|
275
|
-
} catch (readError) {
|
|
276
|
-
if (readError?.code === "ENOENT") return false;
|
|
277
|
-
throw new HitchError("daemon lock exists but its owner record is unreadable", {
|
|
278
|
-
code: "daemon_lock_invalid",
|
|
279
|
-
exitCode: 12,
|
|
280
|
-
cause: readError,
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
return !processIsAlive(current.pid);
|
|
284
|
-
});
|
|
285
|
-
if (!reclaimed) await new Promise((resolve) => setTimeout(resolve, 25));
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
throw new HitchError("could not acquire daemon root lock", { code: "daemon_lock_failed", exitCode: 12 });
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
async function releaseInstanceLock(file, instanceId) {
|
|
292
|
-
let owner;
|
|
293
|
-
try {
|
|
294
|
-
owner = JSON.parse(await readFile(file, "utf8"));
|
|
295
|
-
} catch (error) {
|
|
296
|
-
if (error?.code === "ENOENT") return;
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
if (owner.instance_id === instanceId) await rm(file, { force: true });
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function processIsAlive(pid) {
|
|
303
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
304
|
-
try {
|
|
305
|
-
process.kill(pid, 0);
|
|
306
|
-
return true;
|
|
307
|
-
} catch (error) {
|
|
308
|
-
return error?.code === "EPERM";
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function httpExitCode(status) {
|
|
313
|
-
if (status === 400) return 2;
|
|
314
|
-
if (status === 404) return 3;
|
|
315
|
-
return 12;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function errorStatus(error) {
|
|
319
|
-
if (error?.exitCode === 2) return 400;
|
|
320
|
-
if (error?.exitCode === 3) return 404;
|
|
321
|
-
if (error?.exitCode === 11) return 403;
|
|
322
|
-
if ([4, 5, 10].includes(error?.exitCode)) return 422;
|
|
323
|
-
return 500;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function httpErrorCode(status) {
|
|
327
|
-
if (status === 400) return "invalid_input";
|
|
328
|
-
if (status === 404) return "not_found";
|
|
329
|
-
return "daemon_request_failed";
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function streamFileRange(file, start, end, response) {
|
|
333
|
-
return new Promise((resolve, reject) => {
|
|
334
|
-
const stream = createReadStream(file, { start, end });
|
|
335
|
-
stream.once("error", reject);
|
|
336
|
-
response.once("error", reject);
|
|
337
|
-
response.once("finish", resolve);
|
|
338
|
-
stream.pipe(response);
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
async function completeLineSize(file, size) {
|
|
343
|
-
if (size === 0) return 0;
|
|
344
|
-
const handle = await open(file, "r");
|
|
345
|
-
try {
|
|
346
|
-
let end = size;
|
|
347
|
-
while (end > 0) {
|
|
348
|
-
const start = Math.max(0, end - 64 * 1024);
|
|
349
|
-
const buffer = Buffer.allocUnsafe(end - start);
|
|
350
|
-
const { bytesRead } = await handle.read(buffer, 0, buffer.length, start);
|
|
351
|
-
const newline = buffer.subarray(0, bytesRead).lastIndexOf(0x0a);
|
|
352
|
-
if (newline >= 0) return start + newline + 1;
|
|
353
|
-
end = start;
|
|
354
|
-
}
|
|
355
|
-
return 0;
|
|
356
|
-
} finally {
|
|
357
|
-
await handle.close();
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
async function isLineBoundary(file, offset) {
|
|
362
|
-
if (offset === 0) return true;
|
|
363
|
-
const handle = await open(file, "r");
|
|
364
|
-
try {
|
|
365
|
-
const buffer = Buffer.allocUnsafe(1);
|
|
366
|
-
const { bytesRead } = await handle.read(buffer, 0, 1, offset - 1);
|
|
367
|
-
return bytesRead === 1 && buffer[0] === 0x0a;
|
|
368
|
-
} finally {
|
|
369
|
-
await handle.close();
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
async function ensureToken(file) {
|
|
374
|
-
try {
|
|
375
|
-
const existing = (await readFile(file, "utf8")).trim();
|
|
376
|
-
if (existing.length >= 32) return existing;
|
|
377
|
-
} catch (error) {
|
|
378
|
-
if (error?.code !== "ENOENT") throw error;
|
|
379
|
-
}
|
|
380
|
-
const token = randomBytes(32).toString("hex");
|
|
381
|
-
await writePrivateFile(file, `${token}\n`);
|
|
382
|
-
return token;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function authorized(request, token) {
|
|
386
|
-
const value = request.headers.authorization || "";
|
|
387
|
-
if (!value.startsWith("Bearer ")) return false;
|
|
388
|
-
const supplied = Buffer.from(value.slice(7));
|
|
389
|
-
const expected = Buffer.from(token);
|
|
390
|
-
return supplied.length === expected.length && timingSafeEqual(supplied, expected);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async function readBodyJSON(request, limit = 1_048_576) {
|
|
394
|
-
const chunks = [];
|
|
395
|
-
let size = 0;
|
|
396
|
-
for await (const chunk of request) {
|
|
397
|
-
size += chunk.length;
|
|
398
|
-
if (size > limit) throw invalidInput("request body exceeds 1 MiB");
|
|
399
|
-
chunks.push(chunk);
|
|
400
|
-
}
|
|
401
|
-
try {
|
|
402
|
-
return JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
403
|
-
} catch {
|
|
404
|
-
throw invalidInput("invalid JSON request body");
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function json(response, status, value) {
|
|
409
|
-
if (response.headersSent) return;
|
|
410
|
-
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
411
|
-
response.end(`${JSON.stringify(value)}\n`);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function defaultLogger(type, fields) {
|
|
415
|
-
process.stdout.write(`${new Date().toISOString()} ${type} ${JSON.stringify(fields)}\n`);
|
|
416
|
-
}
|