@jabezpauls/agentbox 0.1.10 → 0.1.11
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/agentbox.mjs +2811 -418
- package/package.json +1 -1
package/dist/agentbox.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// agentbox-cli v0.1.
|
|
2
|
+
// agentbox-cli v0.1.11
|
|
3
3
|
import { createRequire as __agentboxCreateRequire } from "node:module";
|
|
4
4
|
const require = __agentboxCreateRequire(import.meta.url);
|
|
5
5
|
var __create = Object.create;
|
|
@@ -2272,12 +2272,12 @@ var require_extension = __commonJS({
|
|
|
2272
2272
|
var require_websocket = __commonJS({
|
|
2273
2273
|
"../node_modules/ws/lib/websocket.js"(exports, module) {
|
|
2274
2274
|
"use strict";
|
|
2275
|
-
var
|
|
2275
|
+
var EventEmitter5 = __require("events");
|
|
2276
2276
|
var https2 = __require("https");
|
|
2277
2277
|
var http4 = __require("http");
|
|
2278
|
-
var
|
|
2278
|
+
var net6 = __require("net");
|
|
2279
2279
|
var tls = __require("tls");
|
|
2280
|
-
var { randomBytes:
|
|
2280
|
+
var { randomBytes: randomBytes13, createHash: createHash6 } = __require("crypto");
|
|
2281
2281
|
var { Duplex: Duplex3, Readable: Readable2 } = __require("stream");
|
|
2282
2282
|
var { URL: URL2 } = __require("url");
|
|
2283
2283
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -2304,7 +2304,7 @@ var require_websocket = __commonJS({
|
|
|
2304
2304
|
var protocolVersions = [8, 13];
|
|
2305
2305
|
var readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
|
|
2306
2306
|
var subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/;
|
|
2307
|
-
var WebSocket2 = class _WebSocket extends
|
|
2307
|
+
var WebSocket2 = class _WebSocket extends EventEmitter5 {
|
|
2308
2308
|
/**
|
|
2309
2309
|
* Create a new `WebSocket`.
|
|
2310
2310
|
*
|
|
@@ -2815,7 +2815,7 @@ var require_websocket = __commonJS({
|
|
|
2815
2815
|
}
|
|
2816
2816
|
}
|
|
2817
2817
|
const defaultPort = isSecure ? 443 : 80;
|
|
2818
|
-
const key =
|
|
2818
|
+
const key = randomBytes13(16).toString("base64");
|
|
2819
2819
|
const request2 = isSecure ? https2.request : http4.request;
|
|
2820
2820
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
2821
2821
|
let perMessageDeflate;
|
|
@@ -2945,7 +2945,7 @@ var require_websocket = __commonJS({
|
|
|
2945
2945
|
abortHandshake(websocket, socket2, "Invalid Upgrade header");
|
|
2946
2946
|
return;
|
|
2947
2947
|
}
|
|
2948
|
-
const digest =
|
|
2948
|
+
const digest = createHash6("sha1").update(key + GUID).digest("base64");
|
|
2949
2949
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
2950
2950
|
abortHandshake(websocket, socket2, "Invalid Sec-WebSocket-Accept header");
|
|
2951
2951
|
return;
|
|
@@ -3019,12 +3019,12 @@ var require_websocket = __commonJS({
|
|
|
3019
3019
|
}
|
|
3020
3020
|
function netConnect(options) {
|
|
3021
3021
|
options.path = options.socketPath;
|
|
3022
|
-
return
|
|
3022
|
+
return net6.connect(options);
|
|
3023
3023
|
}
|
|
3024
3024
|
function tlsConnect(options) {
|
|
3025
3025
|
options.path = void 0;
|
|
3026
3026
|
if (!options.servername && options.servername !== "") {
|
|
3027
|
-
options.servername =
|
|
3027
|
+
options.servername = net6.isIP(options.host) ? "" : options.host;
|
|
3028
3028
|
}
|
|
3029
3029
|
return tls.connect(options);
|
|
3030
3030
|
}
|
|
@@ -3311,10 +3311,10 @@ var require_subprotocol = __commonJS({
|
|
|
3311
3311
|
var require_websocket_server = __commonJS({
|
|
3312
3312
|
"../node_modules/ws/lib/websocket-server.js"(exports, module) {
|
|
3313
3313
|
"use strict";
|
|
3314
|
-
var
|
|
3314
|
+
var EventEmitter5 = __require("events");
|
|
3315
3315
|
var http4 = __require("http");
|
|
3316
3316
|
var { Duplex: Duplex3 } = __require("stream");
|
|
3317
|
-
var { createHash:
|
|
3317
|
+
var { createHash: createHash6 } = __require("crypto");
|
|
3318
3318
|
var extension2 = require_extension();
|
|
3319
3319
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
3320
3320
|
var subprotocol2 = require_subprotocol();
|
|
@@ -3324,7 +3324,7 @@ var require_websocket_server = __commonJS({
|
|
|
3324
3324
|
var RUNNING = 0;
|
|
3325
3325
|
var CLOSING = 1;
|
|
3326
3326
|
var CLOSED = 2;
|
|
3327
|
-
var WebSocketServer2 = class extends
|
|
3327
|
+
var WebSocketServer2 = class extends EventEmitter5 {
|
|
3328
3328
|
/**
|
|
3329
3329
|
* Create a `WebSocketServer` instance.
|
|
3330
3330
|
*
|
|
@@ -3621,7 +3621,7 @@ var require_websocket_server = __commonJS({
|
|
|
3621
3621
|
);
|
|
3622
3622
|
}
|
|
3623
3623
|
if (this._state > RUNNING) return abortHandshake(socket2, 503);
|
|
3624
|
-
const digest =
|
|
3624
|
+
const digest = createHash6("sha1").update(key + GUID).digest("base64");
|
|
3625
3625
|
const headers = [
|
|
3626
3626
|
"HTTP/1.1 101 Switching Protocols",
|
|
3627
3627
|
"Upgrade: websocket",
|
|
@@ -3860,15 +3860,15 @@ function int(options, name, min, max) {
|
|
|
3860
3860
|
if (!Number.isInteger(n) || n < min || n > max) throw new UsageError(`--${name} must be a whole number from ${min} to ${max}`);
|
|
3861
3861
|
return n;
|
|
3862
3862
|
}
|
|
3863
|
-
function parseSize(
|
|
3864
|
-
const m = /^(\d+(?:\.\d+)?)\s*([kmg]?)(i?b)?$/i.exec(
|
|
3865
|
-
if (!m) throw new UsageError(`"${
|
|
3863
|
+
function parseSize(raw2) {
|
|
3864
|
+
const m = /^(\d+(?:\.\d+)?)\s*([kmg]?)(i?b)?$/i.exec(raw2.trim());
|
|
3865
|
+
if (!m) throw new UsageError(`"${raw2}" is not a size (e.g. 8M, 512K)`);
|
|
3866
3866
|
const unit = { "": 1, k: 1024, m: 1024 ** 2, g: 1024 ** 3 }[(m[2] ?? "").toLowerCase()];
|
|
3867
3867
|
return Math.floor(Number(m[1]) * unit);
|
|
3868
3868
|
}
|
|
3869
3869
|
|
|
3870
3870
|
// src/version.ts
|
|
3871
|
-
var VERSION = true ? "v0.1.
|
|
3871
|
+
var VERSION = true ? "v0.1.11" : "0.0.0-dev";
|
|
3872
3872
|
var STANDALONE = typeof __AGENTBOX_STANDALONE__ === "boolean" && __AGENTBOX_STANDALONE__;
|
|
3873
3873
|
var MIN_NODE_MAJOR = 20;
|
|
3874
3874
|
function nodeMajor(version = process.versions.node) {
|
|
@@ -4345,12 +4345,12 @@ function openTunnel(client, target, opts = {}) {
|
|
|
4345
4345
|
ws.once("error", (err) => fail(new CliError(`could not open a tunnel to ${client.origin}: ${err.message}`, EXIT.UNREACHABLE)));
|
|
4346
4346
|
});
|
|
4347
4347
|
}
|
|
4348
|
-
function parseForwardSpec(
|
|
4349
|
-
const m = /^(\d{1,5})(?::(\d{1,5}))?$/.exec(
|
|
4348
|
+
function parseForwardSpec(raw2) {
|
|
4349
|
+
const m = /^(\d{1,5})(?::(\d{1,5}))?$/.exec(raw2);
|
|
4350
4350
|
const remote = Number(m?.[1]);
|
|
4351
4351
|
const local = m?.[2] !== void 0 ? Number(m[2]) : remote;
|
|
4352
4352
|
if (!m || remote < 1 || remote > 65535 || local < 1 || local > 65535) {
|
|
4353
|
-
throw new UsageError(`"${
|
|
4353
|
+
throw new UsageError(`"${raw2}" is not a port to forward: give <port> or <remote port>:<local port>`);
|
|
4354
4354
|
}
|
|
4355
4355
|
return { remote, local };
|
|
4356
4356
|
}
|
|
@@ -4442,10 +4442,10 @@ var forward = command({
|
|
|
4442
4442
|
var FORWARD_COMMANDS = [forward];
|
|
4443
4443
|
|
|
4444
4444
|
// src/commands/apps.ts
|
|
4445
|
-
function parseExpiry(
|
|
4446
|
-
if (
|
|
4447
|
-
const m = /^(\d+)([smhdw])$/.exec(
|
|
4448
|
-
if (!m) throw new UsageError(`"${
|
|
4445
|
+
function parseExpiry(raw2) {
|
|
4446
|
+
if (raw2 === "never") return null;
|
|
4447
|
+
const m = /^(\d+)([smhdw])$/.exec(raw2.trim());
|
|
4448
|
+
if (!m) throw new UsageError(`"${raw2}" is not an expiry: give e.g. 30m, 12h, 7d, 2w or never`);
|
|
4449
4449
|
const unit = { s: 1, m: 60, h: 3600, d: 86400, w: 604800 }[m[2]];
|
|
4450
4450
|
return Number(m[1]) * unit;
|
|
4451
4451
|
}
|
|
@@ -4690,15 +4690,15 @@ function checkBoxName(name) {
|
|
|
4690
4690
|
}
|
|
4691
4691
|
return name;
|
|
4692
4692
|
}
|
|
4693
|
-
function normalizeBoxUrl(
|
|
4694
|
-
let s =
|
|
4693
|
+
function normalizeBoxUrl(raw2) {
|
|
4694
|
+
let s = raw2.trim();
|
|
4695
4695
|
if (s === "") throw new UsageError("give the box's address, e.g. https://work.example.com");
|
|
4696
4696
|
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) s = `https://${s}`;
|
|
4697
4697
|
let url;
|
|
4698
4698
|
try {
|
|
4699
4699
|
url = new URL(s);
|
|
4700
4700
|
} catch {
|
|
4701
|
-
throw new UsageError(`"${
|
|
4701
|
+
throw new UsageError(`"${raw2}" is not a URL`);
|
|
4702
4702
|
}
|
|
4703
4703
|
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
4704
4704
|
throw new UsageError(`a box is reached over https (or http on your own machine), not ${url.protocol}`);
|
|
@@ -4734,9 +4734,9 @@ var ConfigStore = class {
|
|
|
4734
4734
|
lockTimeoutMs;
|
|
4735
4735
|
/** The configuration as it is on disk now; empty when there is none yet. */
|
|
4736
4736
|
load() {
|
|
4737
|
-
let
|
|
4737
|
+
let raw2;
|
|
4738
4738
|
try {
|
|
4739
|
-
|
|
4739
|
+
raw2 = fs2.readFileSync(this.file, "utf8");
|
|
4740
4740
|
} catch (err) {
|
|
4741
4741
|
if (err.code === "ENOENT") return emptyConfig();
|
|
4742
4742
|
throw new CliError(`cannot read ${this.file}: ${err.message}`);
|
|
@@ -4744,7 +4744,7 @@ var ConfigStore = class {
|
|
|
4744
4744
|
this.tighten();
|
|
4745
4745
|
let data;
|
|
4746
4746
|
try {
|
|
4747
|
-
data = JSON.parse(
|
|
4747
|
+
data = JSON.parse(raw2);
|
|
4748
4748
|
} catch {
|
|
4749
4749
|
throw new CliError(`${this.file} is not valid JSON; fix it or remove it and sign in again`);
|
|
4750
4750
|
}
|
|
@@ -4991,9 +4991,9 @@ import os3 from "node:os";
|
|
|
4991
4991
|
import path5 from "node:path";
|
|
4992
4992
|
|
|
4993
4993
|
// ../shared/src/index.ts
|
|
4994
|
-
function encodePathParam(
|
|
4994
|
+
function encodePathParam(path33) {
|
|
4995
4995
|
let out = "";
|
|
4996
|
-
for (const ch of
|
|
4996
|
+
for (const ch of path33) {
|
|
4997
4997
|
const cp = ch.codePointAt(0) ?? 0;
|
|
4998
4998
|
out += cp >= 56448 && cp <= 56575 ? `%${(cp - 56320).toString(16).toUpperCase()}` : encodeURIComponent(ch);
|
|
4999
4999
|
}
|
|
@@ -5605,11 +5605,11 @@ var sshSetup = command({
|
|
|
5605
5605
|
`);
|
|
5606
5606
|
}
|
|
5607
5607
|
});
|
|
5608
|
-
function parseTarget(
|
|
5609
|
-
if (
|
|
5610
|
-
const m = /^tcp:(\d{1,5})$/.exec(
|
|
5608
|
+
function parseTarget(raw2) {
|
|
5609
|
+
if (raw2 === "herdr") return { kind: "herdr" };
|
|
5610
|
+
const m = /^tcp:(\d{1,5})$/.exec(raw2);
|
|
5611
5611
|
const port = Number(m?.[1]);
|
|
5612
|
-
if (!m || port < 1 || port > 65535) throw new UsageError(`"${
|
|
5612
|
+
if (!m || port < 1 || port > 65535) throw new UsageError(`"${raw2}" is not a tunnel target: give tcp:<port> or herdr`);
|
|
5613
5613
|
return { kind: "tcp", port };
|
|
5614
5614
|
}
|
|
5615
5615
|
var proxy = command({
|
|
@@ -6573,13 +6573,13 @@ var CMD_SAFE = /^[A-Za-z0-9 _.:\\()~-]+$/;
|
|
|
6573
6573
|
function editorCommand(editor, file, platform, env = process.env) {
|
|
6574
6574
|
if (platform !== "win32") return { command: "/bin/sh", args: ["-c", `trap : INT QUIT; ${editor} "$@"`, "sh", file], shell: false };
|
|
6575
6575
|
const [first = "notepad", ...rest] = splitCommand(editor);
|
|
6576
|
-
const
|
|
6577
|
-
if (!/\.(cmd|bat)$/i.test(
|
|
6576
|
+
const resolved2 = /[\\/]/.test(first) ? first : findOnPath(first, env, "win32") ?? first;
|
|
6577
|
+
if (!/\.(cmd|bat)$/i.test(resolved2)) return { command: resolved2, args: [...rest, file], shell: false };
|
|
6578
6578
|
if (!CMD_SAFE.test(file)) {
|
|
6579
6579
|
throw new CliError(`the temporary file's path (${file}) has characters cmd.exe would act on; set TEMP to a plain folder, or use an editor that is an .exe`);
|
|
6580
6580
|
}
|
|
6581
6581
|
const quote2 = (w) => `"${w.replace(/"/g, "")}"`;
|
|
6582
|
-
return { command: [
|
|
6582
|
+
return { command: [resolved2, ...rest, file].map(quote2).join(" "), args: [], shell: true };
|
|
6583
6583
|
}
|
|
6584
6584
|
function runEditor(editor, file, platform = process.platform, env = process.env) {
|
|
6585
6585
|
return new Promise((resolve, reject) => {
|
|
@@ -6807,10 +6807,10 @@ var add = command({
|
|
|
6807
6807
|
var HERDR_COMMANDS = [add, call, socket];
|
|
6808
6808
|
|
|
6809
6809
|
// src/commands/node.ts
|
|
6810
|
-
import { spawn as
|
|
6811
|
-
import
|
|
6810
|
+
import { spawn as spawn9 } from "node:child_process";
|
|
6811
|
+
import fs22 from "node:fs";
|
|
6812
6812
|
import os11 from "node:os";
|
|
6813
|
-
import
|
|
6813
|
+
import path29 from "node:path";
|
|
6814
6814
|
|
|
6815
6815
|
// src/node/control.ts
|
|
6816
6816
|
import { randomBytes as randomBytes4, timingSafeEqual } from "node:crypto";
|
|
@@ -6870,7 +6870,7 @@ function writePrivate2(file, contents, mode = 384) {
|
|
|
6870
6870
|
}
|
|
6871
6871
|
|
|
6872
6872
|
// src/node/control.ts
|
|
6873
|
-
var COMMANDS = /* @__PURE__ */ new Set(["status", "pause", "resume", "leave"]);
|
|
6873
|
+
var COMMANDS = /* @__PURE__ */ new Set(["status", "pause", "resume", "leave", "preview", "preview-list", "preview-stop"]);
|
|
6874
6874
|
async function startControl(socketPath, secretFile, handle) {
|
|
6875
6875
|
const secret = randomBytes4(32).toString("base64url");
|
|
6876
6876
|
writePrivate2(secretFile, secret);
|
|
@@ -6898,7 +6898,8 @@ async function startControl(socketPath, secretFile, handle) {
|
|
|
6898
6898
|
const got = Buffer.from(typeof req.secret === "string" ? req.secret : "");
|
|
6899
6899
|
if (got.length !== want.length || !timingSafeEqual(got, want)) return reply({ ok: false, error: "refused" });
|
|
6900
6900
|
if (typeof req.command !== "string" || !COMMANDS.has(req.command)) return reply({ ok: false, error: "unknown command" });
|
|
6901
|
-
|
|
6901
|
+
const args = typeof req.args === "object" && req.args !== null && !Array.isArray(req.args) ? req.args : {};
|
|
6902
|
+
handle(req.command, args).then(
|
|
6902
6903
|
(out) => reply({ ok: true, ...out }),
|
|
6903
6904
|
(err) => reply({ ok: false, error: err.message })
|
|
6904
6905
|
);
|
|
@@ -6919,7 +6920,7 @@ async function startControl(socketPath, secretFile, handle) {
|
|
|
6919
6920
|
}
|
|
6920
6921
|
};
|
|
6921
6922
|
}
|
|
6922
|
-
function callControl(socketPath, secretFile, command2, timeoutMs = 1e4) {
|
|
6923
|
+
function callControl(socketPath, secretFile, command2, timeoutMs = 1e4, args = {}) {
|
|
6923
6924
|
let secret;
|
|
6924
6925
|
try {
|
|
6925
6926
|
secret = fs9.readFileSync(secretFile, "utf8").trim();
|
|
@@ -6933,7 +6934,7 @@ function callControl(socketPath, secretFile, command2, timeoutMs = 1e4) {
|
|
|
6933
6934
|
sock.destroy();
|
|
6934
6935
|
resolve(null);
|
|
6935
6936
|
}, timeoutMs);
|
|
6936
|
-
sock.on("connect", () => sock.write(`${JSON.stringify({ secret, command: command2 })}
|
|
6937
|
+
sock.on("connect", () => sock.write(`${JSON.stringify({ secret, command: command2, args })}
|
|
6937
6938
|
`));
|
|
6938
6939
|
sock.on("data", (c) => buf += c.toString("utf8"));
|
|
6939
6940
|
sock.on("end", () => {
|
|
@@ -7044,7 +7045,9 @@ function decodeOpen(payload) {
|
|
|
7044
7045
|
} catch {
|
|
7045
7046
|
}
|
|
7046
7047
|
const id = (s) => typeof s === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(s) ? s : null;
|
|
7047
|
-
|
|
7048
|
+
const out = { kind: v.kind === "upgrade" ? "upgrade" : "http", session: id(v.session), grant: id(v.grant) };
|
|
7049
|
+
if (typeof v.app === "number" && Number.isInteger(v.app) && v.app >= 1 && v.app <= 65535) out.app = v.app;
|
|
7050
|
+
return out;
|
|
7048
7051
|
}
|
|
7049
7052
|
function windowPayload(n) {
|
|
7050
7053
|
const b = Buffer.allocUnsafe(4);
|
|
@@ -7243,16 +7246,16 @@ var StreamTable = class {
|
|
|
7243
7246
|
|
|
7244
7247
|
// src/node/credentials.ts
|
|
7245
7248
|
function readCredentials(file) {
|
|
7246
|
-
let
|
|
7249
|
+
let raw2;
|
|
7247
7250
|
try {
|
|
7248
|
-
|
|
7251
|
+
raw2 = fs10.readFileSync(file, "utf8");
|
|
7249
7252
|
} catch (err) {
|
|
7250
7253
|
if (err.code === "ENOENT") return null;
|
|
7251
7254
|
throw err;
|
|
7252
7255
|
}
|
|
7253
7256
|
let c;
|
|
7254
7257
|
try {
|
|
7255
|
-
c = JSON.parse(
|
|
7258
|
+
c = JSON.parse(raw2);
|
|
7256
7259
|
} catch {
|
|
7257
7260
|
throw new CliError(`${file} is not valid JSON; this machine cannot link until it is fixed or re-joined`);
|
|
7258
7261
|
}
|
|
@@ -7272,7 +7275,7 @@ import fs12 from "node:fs";
|
|
|
7272
7275
|
import path11 from "node:path";
|
|
7273
7276
|
|
|
7274
7277
|
// ../../images/machine/agents.md
|
|
7275
|
-
var agents_default = "# You are working on {{machine}}, through agentbox\n\n{{machine}} is one of the owner's own computers, joined to their agentbox. You\nrun here as the owner, with their files, their tools and their network. The\nowner watches your terminal from their browser, on agentbox's machines page.\n\n- **To show the owner an app** you started (a dev server, say),
|
|
7278
|
+
var agents_default = "# You are working on {{machine}}, through agentbox\n\n{{machine}} is one of the owner's own computers, joined to their agentbox. You\nrun here as the owner, with their files, their tools and their network. The\nowner watches your terminal from their browser, on agentbox's machines page.\n\n- **To show the owner an app** you started (a dev server, say), run\n `agentbox-preview <port>` once it listens (on 127.0.0.1 or [::1]): it appears\n in the owner's Preview on the machines page. `agentbox-preview list` and\n `agentbox-preview stop <port>` manage them.\n- **Never make anything public.** Do not use tunnels (ngrok, cloudflared,\n localtunnel), external hosting, claude.ai Artifacts or similar to show a web\n app unless the owner explicitly asks for that.\n- **Docker and ports are this machine's own.** Containers you start and ports\n you listen on are on {{machine}}, not in an agentbox sandbox; clean up what\n you start.\n";
|
|
7276
7279
|
|
|
7277
7280
|
// src/node/update.ts
|
|
7278
7281
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
@@ -7353,16 +7356,16 @@ function asset(v, where, versioned) {
|
|
|
7353
7356
|
return entry;
|
|
7354
7357
|
}
|
|
7355
7358
|
function parseManifest(bytes) {
|
|
7356
|
-
let
|
|
7359
|
+
let raw2;
|
|
7357
7360
|
try {
|
|
7358
|
-
|
|
7361
|
+
raw2 = JSON.parse(bytes.toString("utf8"));
|
|
7359
7362
|
} catch {
|
|
7360
7363
|
throw new UpdateRefused("the release manifest is not JSON");
|
|
7361
7364
|
}
|
|
7362
|
-
if (!
|
|
7363
|
-
if (typeof
|
|
7365
|
+
if (!raw2 || typeof raw2 !== "object" || raw2.schema !== 1) throw new UpdateRefused("the release manifest is of a kind this node does not know; update it by hand");
|
|
7366
|
+
if (typeof raw2.version !== "string" || !parseVersion(raw2.version)) throw new UpdateRefused("the release manifest has no version");
|
|
7364
7367
|
const platforms = {};
|
|
7365
|
-
const given =
|
|
7368
|
+
const given = raw2.platforms ?? {};
|
|
7366
7369
|
for (const p of MACHINE_PLATFORMS) {
|
|
7367
7370
|
const e = given[p];
|
|
7368
7371
|
if (!e) continue;
|
|
@@ -7374,7 +7377,7 @@ function parseManifest(bytes) {
|
|
|
7374
7377
|
if (e.nodeGui !== void 0) entry.nodeGui = asset(e.nodeGui, `${p} windowless node`, false);
|
|
7375
7378
|
platforms[p] = entry;
|
|
7376
7379
|
}
|
|
7377
|
-
return { schema: 1, version:
|
|
7380
|
+
return { schema: 1, version: raw2.version, platforms };
|
|
7378
7381
|
}
|
|
7379
7382
|
|
|
7380
7383
|
// src/node/update.ts
|
|
@@ -7684,14 +7687,14 @@ function expand(p, home2) {
|
|
|
7684
7687
|
return p;
|
|
7685
7688
|
}
|
|
7686
7689
|
function parsePolicy(text, home2) {
|
|
7687
|
-
let
|
|
7690
|
+
let raw2;
|
|
7688
7691
|
try {
|
|
7689
|
-
|
|
7692
|
+
raw2 = JSON.parse(text);
|
|
7690
7693
|
} catch {
|
|
7691
7694
|
return broken("policy.json is not valid JSON");
|
|
7692
7695
|
}
|
|
7693
|
-
if (typeof
|
|
7694
|
-
const o =
|
|
7696
|
+
if (typeof raw2 !== "object" || raw2 === null || Array.isArray(raw2)) return broken("policy.json is not an object");
|
|
7697
|
+
const o = raw2;
|
|
7695
7698
|
for (const k of Object.keys(o)) if (!KEYS.has(k)) return broken(`policy.json has "${k}", which this node does not know`);
|
|
7696
7699
|
const policy = { ...DENY_ALL, files: [] };
|
|
7697
7700
|
for (const k of ["terminals", "ports", "share", "paused"]) {
|
|
@@ -7962,7 +7965,13 @@ var NodeLink = class extends EventEmitter {
|
|
|
7962
7965
|
return this.paused;
|
|
7963
7966
|
}
|
|
7964
7967
|
sendPolicy(policy) {
|
|
7965
|
-
|
|
7968
|
+
this.sendControl({ type: "policy", policy });
|
|
7969
|
+
}
|
|
7970
|
+
/** Say `message` to the box while online; false when not online. */
|
|
7971
|
+
sendControl(message) {
|
|
7972
|
+
if (this.stateNow !== "online" || this.ws?.readyState !== import_websocket.default.OPEN) return false;
|
|
7973
|
+
this.ws.send(encodeControl(message));
|
|
7974
|
+
return true;
|
|
7966
7975
|
}
|
|
7967
7976
|
/** Tell the box this machine is leaving; resolves whether it was told. */
|
|
7968
7977
|
leave(timeoutMs = 5e3) {
|
|
@@ -8340,8 +8349,8 @@ var Prompter = class {
|
|
|
8340
8349
|
};
|
|
8341
8350
|
|
|
8342
8351
|
// src/node/run.ts
|
|
8343
|
-
import
|
|
8344
|
-
import
|
|
8352
|
+
import fs21 from "node:fs";
|
|
8353
|
+
import path28 from "node:path";
|
|
8345
8354
|
|
|
8346
8355
|
// ../hostcore/src/herdr/socket.ts
|
|
8347
8356
|
import net4 from "node:net";
|
|
@@ -8993,9 +9002,9 @@ function control(a, msg, size) {
|
|
|
8993
9002
|
break;
|
|
8994
9003
|
}
|
|
8995
9004
|
}
|
|
8996
|
-
function parse(
|
|
9005
|
+
function parse(raw2) {
|
|
8997
9006
|
try {
|
|
8998
|
-
return JSON.parse(
|
|
9007
|
+
return JSON.parse(raw2.toString());
|
|
8999
9008
|
} catch {
|
|
9000
9009
|
return null;
|
|
9001
9010
|
}
|
|
@@ -9015,8 +9024,8 @@ function single(socket2, streams, pane, cols, rows) {
|
|
|
9015
9024
|
}
|
|
9016
9025
|
};
|
|
9017
9026
|
const attachment = streams.attach(pane, viewer, cols, rows);
|
|
9018
|
-
socket2.on("message", (
|
|
9019
|
-
const msg = parse(
|
|
9027
|
+
socket2.on("message", (raw2) => {
|
|
9028
|
+
const msg = parse(raw2);
|
|
9020
9029
|
if (msg) control(attachment, msg, { cols, rows });
|
|
9021
9030
|
});
|
|
9022
9031
|
socket2.on("close", () => attachment.detach());
|
|
@@ -9047,8 +9056,8 @@ function shared(socket2, streams) {
|
|
|
9047
9056
|
const attachment = streams.attach(pane, viewer, size.cols, size.rows);
|
|
9048
9057
|
if (live) channels.set(ch, { attachment, size });
|
|
9049
9058
|
};
|
|
9050
|
-
socket2.on("message", (
|
|
9051
|
-
const msg = parse(
|
|
9059
|
+
socket2.on("message", (raw2) => {
|
|
9060
|
+
const msg = parse(raw2);
|
|
9052
9061
|
if (!msg) return;
|
|
9053
9062
|
const ch = Number(msg.ch);
|
|
9054
9063
|
if (!Number.isInteger(ch) || ch < 0 || ch > 65535) return;
|
|
@@ -9299,218 +9308,2370 @@ var HerdrSupervisor = class extends EventEmitter2 {
|
|
|
9299
9308
|
|
|
9300
9309
|
// src/node/server.ts
|
|
9301
9310
|
import http from "node:http";
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9311
|
+
|
|
9312
|
+
// ../hostcore/src/files/entries.ts
|
|
9313
|
+
import fsp2 from "node:fs/promises";
|
|
9314
|
+
import path16 from "node:path";
|
|
9315
|
+
|
|
9316
|
+
// ../hostcore/src/files/names.ts
|
|
9317
|
+
var strict = new TextDecoder("utf-8", { fatal: true });
|
|
9318
|
+
function decodeName(bytes) {
|
|
9319
|
+
try {
|
|
9320
|
+
return strict.decode(bytes);
|
|
9321
|
+
} catch {
|
|
9322
|
+
return decodeLossless(bytes);
|
|
9323
|
+
}
|
|
9307
9324
|
}
|
|
9308
|
-
function
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
});
|
|
9329
|
-
req.on("error", () => resolve(null));
|
|
9330
|
-
});
|
|
9325
|
+
function decodeLossless(b) {
|
|
9326
|
+
let out = "";
|
|
9327
|
+
let i = 0;
|
|
9328
|
+
while (i < b.length) {
|
|
9329
|
+
const c = b[i];
|
|
9330
|
+
if (c < 128) {
|
|
9331
|
+
out += String.fromCharCode(c);
|
|
9332
|
+
i += 1;
|
|
9333
|
+
continue;
|
|
9334
|
+
}
|
|
9335
|
+
const len = sequenceLength(b, i);
|
|
9336
|
+
if (len === 0) {
|
|
9337
|
+
out += String.fromCharCode(56320 + c);
|
|
9338
|
+
i += 1;
|
|
9339
|
+
continue;
|
|
9340
|
+
}
|
|
9341
|
+
out += strict.decode(b.subarray(i, i + len));
|
|
9342
|
+
i += len;
|
|
9343
|
+
}
|
|
9344
|
+
return out;
|
|
9331
9345
|
}
|
|
9332
|
-
function
|
|
9333
|
-
const
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
);
|
|
9346
|
+
function sequenceLength(b, i) {
|
|
9347
|
+
const c = b[i];
|
|
9348
|
+
const cont = (k, lo = 128, hi = 191) => {
|
|
9349
|
+
const v = b[i + k];
|
|
9350
|
+
return v !== void 0 && v >= lo && v <= hi;
|
|
9351
|
+
};
|
|
9352
|
+
if (c >= 194 && c <= 223) return cont(1) ? 2 : 0;
|
|
9353
|
+
if (c === 224) return cont(1, 160) && cont(2) ? 3 : 0;
|
|
9354
|
+
if (c >= 225 && c <= 236 || c === 238 || c === 239) return cont(1) && cont(2) ? 3 : 0;
|
|
9355
|
+
if (c === 237) return cont(1, 128, 159) && cont(2) ? 3 : 0;
|
|
9356
|
+
if (c === 240) return cont(1, 144) && cont(2) && cont(3) ? 4 : 0;
|
|
9357
|
+
if (c >= 241 && c <= 243) return cont(1) && cont(2) && cont(3) ? 4 : 0;
|
|
9358
|
+
if (c === 244) return cont(1, 128, 143) && cont(2) && cont(3) ? 4 : 0;
|
|
9359
|
+
return 0;
|
|
9360
|
+
}
|
|
9361
|
+
function hasEscapes(s) {
|
|
9362
|
+
return new RegExp("\\p{Cs}", "u").test(s);
|
|
9363
|
+
}
|
|
9364
|
+
var NameError = class extends Error {
|
|
9365
|
+
};
|
|
9366
|
+
function encodeName(s) {
|
|
9367
|
+
if (!hasEscapes(s)) return Buffer.from(s, "utf8");
|
|
9368
|
+
const parts = [];
|
|
9369
|
+
let run4 = "";
|
|
9370
|
+
for (const ch of s) {
|
|
9371
|
+
const cp = ch.codePointAt(0);
|
|
9372
|
+
if (cp >= 56448 && cp <= 56575) {
|
|
9373
|
+
if (run4) parts.push(Buffer.from(run4, "utf8"));
|
|
9374
|
+
run4 = "";
|
|
9375
|
+
parts.push(Buffer.from([cp - 56320]));
|
|
9376
|
+
} else if (cp >= 55296 && cp <= 57343) {
|
|
9377
|
+
throw new NameError("path contains an unpaired surrogate");
|
|
9378
|
+
} else {
|
|
9379
|
+
run4 += ch;
|
|
9380
|
+
}
|
|
9381
|
+
}
|
|
9382
|
+
if (run4) parts.push(Buffer.from(run4, "utf8"));
|
|
9383
|
+
return Buffer.concat(parts);
|
|
9342
9384
|
}
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9385
|
+
function fsPath(s) {
|
|
9386
|
+
return hasEscapes(s) ? encodeName(s) : s;
|
|
9387
|
+
}
|
|
9388
|
+
function parseQuery(rawUrl) {
|
|
9389
|
+
const out = /* @__PURE__ */ new Map();
|
|
9390
|
+
const q = rawUrl.indexOf("?");
|
|
9391
|
+
if (q === -1) return out;
|
|
9392
|
+
for (const pair of rawUrl.slice(q + 1).split("&")) {
|
|
9393
|
+
if (pair === "") continue;
|
|
9394
|
+
const eq = pair.indexOf("=");
|
|
9395
|
+
const key = decodeComponent(eq === -1 ? pair : pair.slice(0, eq));
|
|
9396
|
+
const value = eq === -1 ? "" : decodeComponent(pair.slice(eq + 1));
|
|
9397
|
+
const list = out.get(key);
|
|
9398
|
+
if (list) list.push(value);
|
|
9399
|
+
else out.set(key, [value]);
|
|
9355
9400
|
}
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
|
|
9401
|
+
return out;
|
|
9402
|
+
}
|
|
9403
|
+
function decodeComponent(s, plusIsSpace = true) {
|
|
9404
|
+
const bytes = [];
|
|
9405
|
+
for (let i = 0; i < s.length; i++) {
|
|
9406
|
+
const ch = s.charCodeAt(i);
|
|
9407
|
+
if (ch === 37 && /^[0-9a-fA-F]{2}$/.test(s.slice(i + 1, i + 3))) {
|
|
9408
|
+
bytes.push(parseInt(s.slice(i + 1, i + 3), 16));
|
|
9409
|
+
i += 2;
|
|
9410
|
+
} else if (ch === 43 && plusIsSpace) {
|
|
9411
|
+
bytes.push(32);
|
|
9412
|
+
} else if (ch < 128) {
|
|
9413
|
+
bytes.push(ch);
|
|
9414
|
+
} else {
|
|
9415
|
+
const cp = s.codePointAt(i);
|
|
9416
|
+
for (const b of Buffer.from(String.fromCodePoint(cp), "utf8")) bytes.push(b);
|
|
9417
|
+
if (cp > 65535) i += 1;
|
|
9418
|
+
}
|
|
9365
9419
|
}
|
|
9366
|
-
|
|
9367
|
-
|
|
9420
|
+
return decodeName(Uint8Array.from(bytes));
|
|
9421
|
+
}
|
|
9422
|
+
|
|
9423
|
+
// ../hostcore/src/files/roots.ts
|
|
9424
|
+
import fsp from "node:fs/promises";
|
|
9425
|
+
import path15 from "node:path";
|
|
9426
|
+
var FilesError = class extends Error {
|
|
9427
|
+
constructor(status3, message, code2) {
|
|
9428
|
+
super(message);
|
|
9429
|
+
this.status = status3;
|
|
9430
|
+
this.code = code2;
|
|
9431
|
+
this.name = "FilesError";
|
|
9368
9432
|
}
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9433
|
+
status;
|
|
9434
|
+
code;
|
|
9435
|
+
};
|
|
9436
|
+
var MAX_PATH = 4096;
|
|
9437
|
+
var RESERVED = ["trash", "uploads", "clones"];
|
|
9438
|
+
function within2(child, parent) {
|
|
9439
|
+
if (child === parent) return true;
|
|
9440
|
+
const base = parent.endsWith(path15.sep) ? parent : parent + path15.sep;
|
|
9441
|
+
return child.startsWith(base);
|
|
9442
|
+
}
|
|
9443
|
+
var Roots = class {
|
|
9444
|
+
/** Where a relative path hangs: the first root configured. */
|
|
9445
|
+
primary;
|
|
9446
|
+
home;
|
|
9447
|
+
roots;
|
|
9448
|
+
all;
|
|
9449
|
+
realCache = /* @__PURE__ */ new Map();
|
|
9450
|
+
caseInsensitive;
|
|
9451
|
+
isHidden;
|
|
9452
|
+
isWriteDenied;
|
|
9453
|
+
refusePath;
|
|
9454
|
+
constructor(opts) {
|
|
9455
|
+
this.roots = opts.roots.map((r) => ({ id: r.id, path: path15.resolve(r.path), mode: r.mode, stateDir: path15.resolve(r.stateDir) }));
|
|
9456
|
+
const first = this.roots[0];
|
|
9457
|
+
if (!first) throw new Error("at least one root is required");
|
|
9458
|
+
this.primary = first;
|
|
9459
|
+
this.home = this.roots.find((r) => r.id === "home");
|
|
9460
|
+
this.all = [...this.roots].sort((a, b) => b.path.length - a.path.length);
|
|
9461
|
+
this.caseInsensitive = opts.caseInsensitive ?? false;
|
|
9462
|
+
this.isHidden = opts.isHidden;
|
|
9463
|
+
this.isWriteDenied = opts.isWriteDenied;
|
|
9464
|
+
this.refusePath = opts.refusePath;
|
|
9465
|
+
}
|
|
9466
|
+
byId(id) {
|
|
9467
|
+
const root = this.roots.find((r) => r.id === id);
|
|
9468
|
+
if (!root) throw new FilesError(404, `no root named ${id}`, "not-found");
|
|
9469
|
+
return root;
|
|
9470
|
+
}
|
|
9471
|
+
list() {
|
|
9472
|
+
return [...this.roots];
|
|
9473
|
+
}
|
|
9474
|
+
/** {@link within}, ignoring case when the roots were configured to. */
|
|
9475
|
+
inside(child, parent) {
|
|
9476
|
+
return this.caseInsensitive ? within2(child.toLowerCase(), parent.toLowerCase()) : within2(child, parent);
|
|
9477
|
+
}
|
|
9478
|
+
/** True when a resolved path is hidden by the configured predicate. */
|
|
9479
|
+
hidden(realPath) {
|
|
9480
|
+
return this.isHidden?.(realPath) ?? false;
|
|
9481
|
+
}
|
|
9482
|
+
/** A resolved path that is hidden reads as one that does not exist. */
|
|
9483
|
+
assertVisible(realPath, where) {
|
|
9484
|
+
if (this.hidden(realPath)) throw new FilesError(404, `${where} does not exist`, "not-found");
|
|
9485
|
+
}
|
|
9486
|
+
/** Refuse a change under a read-only root, or to a path the predicate denies. */
|
|
9487
|
+
assertWritable(root, realPath, where) {
|
|
9488
|
+
if (root.mode === "read") throw new FilesError(403, `${root.path} is read-only`, "read-only");
|
|
9489
|
+
if (this.isWriteDenied?.(realPath)) throw new FilesError(403, `permission denied: ${where}`, "denied");
|
|
9490
|
+
}
|
|
9491
|
+
/** The root's own real path, resolved once. */
|
|
9492
|
+
async realRoot(root) {
|
|
9493
|
+
const cached = this.realCache.get(root.path);
|
|
9494
|
+
if (cached) return cached;
|
|
9495
|
+
let real;
|
|
9496
|
+
try {
|
|
9497
|
+
real = await fsp.realpath(root.path);
|
|
9498
|
+
} catch {
|
|
9499
|
+
throw new FilesError(404, `${root.path} does not exist`);
|
|
9500
|
+
}
|
|
9501
|
+
this.realCache.set(root.path, real);
|
|
9502
|
+
return real;
|
|
9503
|
+
}
|
|
9504
|
+
/** The state directory of a root, e.g. `/workspace/.agentbox`. */
|
|
9505
|
+
stateDir(root) {
|
|
9506
|
+
return root.stateDir;
|
|
9507
|
+
}
|
|
9508
|
+
/** The state directory's name when it sits directly in its root, e.g. `.agentbox`. */
|
|
9509
|
+
stateName(root) {
|
|
9510
|
+
return path15.dirname(root.stateDir) === root.path ? path15.basename(root.stateDir) : null;
|
|
9511
|
+
}
|
|
9512
|
+
/** The state directory as resolved on disk, or null when it cannot be. */
|
|
9513
|
+
async realStateDir(root) {
|
|
9514
|
+
if (within2(root.stateDir, root.path)) {
|
|
9515
|
+
const real = await this.realRoot(root).catch(() => null);
|
|
9516
|
+
return real === null ? null : path15.join(real, path15.relative(root.path, root.stateDir));
|
|
9517
|
+
}
|
|
9518
|
+
return fsp.realpath(root.stateDir).catch(() => null);
|
|
9519
|
+
}
|
|
9520
|
+
/** Place a client path lexically. Throws for anything outside every root. */
|
|
9521
|
+
locate(input) {
|
|
9522
|
+
if (typeof input !== "string") throw new FilesError(400, "path required");
|
|
9523
|
+
if (input.length > MAX_PATH) throw new FilesError(400, "path too long");
|
|
9524
|
+
if (input.includes("\0")) throw new FilesError(400, "path contains a NUL byte");
|
|
9525
|
+
const refused = this.refusePath?.(input) ?? null;
|
|
9526
|
+
if (refused !== null) throw new FilesError(400, refused, "refused");
|
|
9527
|
+
if (hasEscapes(input)) {
|
|
9528
|
+
try {
|
|
9529
|
+
encodeName(input);
|
|
9530
|
+
} catch (err) {
|
|
9531
|
+
if (err instanceof NameError) throw new FilesError(400, err.message);
|
|
9532
|
+
throw err;
|
|
9533
|
+
}
|
|
9534
|
+
}
|
|
9535
|
+
let abs;
|
|
9536
|
+
if (input === "~" || input.startsWith("~/")) {
|
|
9537
|
+
if (!this.home) throw new FilesError(403, `${input} is outside every root`, "outside");
|
|
9538
|
+
abs = path15.resolve(this.home.path, "." + input.slice(1));
|
|
9539
|
+
} else abs = path15.resolve(this.primary.path, input === "" ? "." : input);
|
|
9540
|
+
const root = this.all.find((r) => this.inside(abs, r.path));
|
|
9541
|
+
if (!root) throw new FilesError(403, `${input} is outside ${this.outsideWhat()}`, "outside");
|
|
9542
|
+
return { root, abs };
|
|
9543
|
+
}
|
|
9544
|
+
/** What a path outside every root is said to be outside of. */
|
|
9545
|
+
outsideWhat() {
|
|
9546
|
+
const ids = this.roots.map((r) => r.id);
|
|
9547
|
+
if (ids.length === 2 && ids[0] === "workspace" && ids[1] === "home") return "the workspace and home";
|
|
9548
|
+
return "every root";
|
|
9549
|
+
}
|
|
9550
|
+
/** True when `abs` is the root itself or inside its reserved state dirs. */
|
|
9551
|
+
isReserved(loc) {
|
|
9552
|
+
if (loc.abs === loc.root.path) return true;
|
|
9553
|
+
const state = this.stateDir(loc.root);
|
|
9554
|
+
if (this.inside(loc.abs, state) && this.inside(state, loc.abs)) return true;
|
|
9555
|
+
return RESERVED.some((d) => this.inside(loc.abs, path15.join(state, d)));
|
|
9556
|
+
}
|
|
9557
|
+
/**
|
|
9558
|
+
* Refuse to change the root itself or the API's own state, anything under a
|
|
9559
|
+
* read-only root, and a path the write-denied predicate names (checked on
|
|
9560
|
+
* the resolved path when the reference carries one).
|
|
9561
|
+
*/
|
|
9562
|
+
assertMutable(loc) {
|
|
9563
|
+
if (loc.abs === loc.root.path) throw new FilesError(403, "the root itself cannot be changed", "reserved");
|
|
9564
|
+
if (this.isReserved(loc)) {
|
|
9565
|
+
throw new FilesError(403, `${loc.abs} holds agentbox's own state`, "reserved");
|
|
9380
9566
|
}
|
|
9381
|
-
|
|
9567
|
+
this.assertWritable(loc.root, "fs" in loc ? loc.fs : loc.abs, loc.abs);
|
|
9382
9568
|
}
|
|
9383
|
-
/**
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9569
|
+
/**
|
|
9570
|
+
* Refuse to copy or move anything that is, holds, or sits inside a root's
|
|
9571
|
+
* `.agentbox`. Carrying the trash and upload scratch along is never what a
|
|
9572
|
+
* person meant — and a copy is built inside that very scratch space, so a
|
|
9573
|
+
* copy of it would copy itself until the disk filled. Checked on the path
|
|
9574
|
+
* as named and as resolved, since a link can lead there.
|
|
9575
|
+
*/
|
|
9576
|
+
async assertMovable(ref) {
|
|
9577
|
+
for (const root of this.all) {
|
|
9578
|
+
const lexical = this.stateDir(root);
|
|
9579
|
+
const real = await this.realStateDir(root);
|
|
9580
|
+
for (const [p, state] of [
|
|
9581
|
+
[ref.abs, lexical],
|
|
9582
|
+
[ref.fs, real]
|
|
9583
|
+
]) {
|
|
9584
|
+
if (state && (this.inside(p, state) || this.inside(state, p))) {
|
|
9585
|
+
throw new FilesError(403, `${ref.abs} holds agentbox's own state`, "reserved");
|
|
9586
|
+
}
|
|
9587
|
+
}
|
|
9391
9588
|
}
|
|
9392
|
-
return n;
|
|
9393
9589
|
}
|
|
9394
|
-
/**
|
|
9395
|
-
|
|
9396
|
-
const
|
|
9397
|
-
this.
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
|
|
9403
|
-
}
|
|
9590
|
+
/** Resolve through every symlink; the destination must stay in the root. */
|
|
9591
|
+
async target(input) {
|
|
9592
|
+
const loc = this.locate(input);
|
|
9593
|
+
const rootReal = await this.realRoot(loc.root);
|
|
9594
|
+
let real;
|
|
9595
|
+
try {
|
|
9596
|
+
real = decodeName(await fsp.realpath(fsPath(loc.abs), { encoding: "buffer" }));
|
|
9597
|
+
} catch (err) {
|
|
9598
|
+
throw fsError(err, loc.abs);
|
|
9599
|
+
}
|
|
9600
|
+
if (!this.inside(real, rootReal)) {
|
|
9601
|
+
throw new FilesError(403, `${loc.abs} leads outside its root`, "outside");
|
|
9602
|
+
}
|
|
9603
|
+
this.assertVisible(real, loc.abs);
|
|
9604
|
+
return { ...loc, real };
|
|
9404
9605
|
}
|
|
9405
|
-
|
|
9406
|
-
|
|
9606
|
+
/**
|
|
9607
|
+
* Address an existing entry itself (a symlink, not what it points at). The
|
|
9608
|
+
* parent is resolved and confined; the entry must exist.
|
|
9609
|
+
*/
|
|
9610
|
+
async entry(input) {
|
|
9611
|
+
const loc = this.locate(input);
|
|
9612
|
+
if (loc.abs === loc.root.path) {
|
|
9613
|
+
return { ...loc, fs: await this.realRoot(loc.root) };
|
|
9614
|
+
}
|
|
9615
|
+
const parent = await this.realParent(loc);
|
|
9616
|
+
const fsp_ = path15.join(parent, path15.basename(loc.abs));
|
|
9617
|
+
this.assertVisible(fsp_, loc.abs);
|
|
9618
|
+
try {
|
|
9619
|
+
await fsp.lstat(fsPath(fsp_));
|
|
9620
|
+
} catch (err) {
|
|
9621
|
+
throw fsError(err, loc.abs);
|
|
9622
|
+
}
|
|
9623
|
+
return { ...loc, fs: fsp_ };
|
|
9407
9624
|
}
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9415
|
-
|
|
9416
|
-
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
});
|
|
9425
|
-
|
|
9426
|
-
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
if (
|
|
9431
|
-
|
|
9625
|
+
/**
|
|
9626
|
+
* Address a path to create. The nearest existing ancestor is resolved and
|
|
9627
|
+
* confined; missing directories in between are not created here (see
|
|
9628
|
+
* {@link ensureParent}). Whether the final name exists is the caller's call.
|
|
9629
|
+
*/
|
|
9630
|
+
async creatable(input) {
|
|
9631
|
+
const loc = this.locate(input);
|
|
9632
|
+
if (loc.abs === loc.root.path) throw new FilesError(409, "the root already exists", "exists");
|
|
9633
|
+
const rootReal = await this.realRoot(loc.root);
|
|
9634
|
+
const missing = [path15.basename(loc.abs)];
|
|
9635
|
+
let dir = path15.dirname(loc.abs);
|
|
9636
|
+
for (; ; ) {
|
|
9637
|
+
try {
|
|
9638
|
+
const real = decodeName(await fsp.realpath(fsPath(dir), { encoding: "buffer" }));
|
|
9639
|
+
if (!this.inside(real, rootReal)) throw new FilesError(403, `${loc.abs} leads outside its root`, "outside");
|
|
9640
|
+
const st = await fsp.stat(fsPath(real));
|
|
9641
|
+
if (!st.isDirectory()) throw new FilesError(409, `${dir} is not a directory`, "not-a-directory");
|
|
9642
|
+
const created = path15.join(real, ...missing.reverse());
|
|
9643
|
+
this.assertVisible(created, loc.abs);
|
|
9644
|
+
this.assertWritable(loc.root, created, loc.abs);
|
|
9645
|
+
return { ...loc, fs: created };
|
|
9646
|
+
} catch (err) {
|
|
9647
|
+
if (err instanceof FilesError) throw err;
|
|
9648
|
+
const code2 = err.code;
|
|
9649
|
+
if (code2 !== "ENOENT" || dir === loc.root.path) throw fsError(err, dir);
|
|
9650
|
+
missing.push(path15.basename(dir));
|
|
9651
|
+
dir = path15.dirname(dir);
|
|
9432
9652
|
}
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9653
|
+
}
|
|
9654
|
+
}
|
|
9655
|
+
/**
|
|
9656
|
+
* Create the missing directories between a {@link creatable} path's real
|
|
9657
|
+
* ancestor and its parent. Each is created (never followed) in turn, so a
|
|
9658
|
+
* link planted on the way cannot divert the write.
|
|
9659
|
+
*/
|
|
9660
|
+
async ensureParent(ref) {
|
|
9661
|
+
const parent = path15.dirname(ref.fs);
|
|
9662
|
+
const rootReal = await this.realRoot(ref.root);
|
|
9663
|
+
const parts = [];
|
|
9664
|
+
let dir = parent;
|
|
9665
|
+
for (; ; ) {
|
|
9666
|
+
try {
|
|
9667
|
+
const st = await fsp.lstat(fsPath(dir));
|
|
9668
|
+
if (st.isDirectory()) break;
|
|
9669
|
+
throw new FilesError(409, `${dir} is not a directory`, "not-a-directory");
|
|
9670
|
+
} catch (err) {
|
|
9671
|
+
if (err instanceof FilesError) throw err;
|
|
9672
|
+
if (err.code !== "ENOENT") throw fsError(err, dir);
|
|
9673
|
+
parts.push(dir);
|
|
9674
|
+
dir = path15.dirname(dir);
|
|
9440
9675
|
}
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
() => res.destroy(),
|
|
9451
|
-
(cb) => res.once("close", cb)
|
|
9452
|
-
);
|
|
9453
|
-
if (route === "GET /api/session") {
|
|
9454
|
-
const hub = this.o.hub();
|
|
9455
|
-
if (!hub) return json(res, 503, { error: "herdr", message: "herdr is starting on this machine" });
|
|
9456
|
-
try {
|
|
9457
|
-
return json(res, 200, await hub.snapshot());
|
|
9458
|
-
} catch (err) {
|
|
9459
|
-
return json(res, 502, { error: "herdr", message: err.message });
|
|
9460
|
-
}
|
|
9461
|
-
}
|
|
9462
|
-
const b = await readJson(req);
|
|
9463
|
-
const method = b?.method;
|
|
9464
|
-
if (typeof method !== "string") return json(res, 400, { error: "method required" });
|
|
9465
|
-
if (!isAllowed(method)) return json(res, 403, { error: "method not allowed" });
|
|
9466
|
-
const params = typeof b?.params === "object" && b.params !== null ? b.params : {};
|
|
9467
|
-
try {
|
|
9468
|
-
return json(res, 200, { result: await request(socket2, method, params) });
|
|
9469
|
-
} catch (err) {
|
|
9470
|
-
if (err instanceof HerdrError) return json(res, 502, { error: { code: err.code, message: err.message } });
|
|
9471
|
-
return json(res, 502, { error: { code: "unreachable", message: err.message } });
|
|
9472
|
-
}
|
|
9676
|
+
}
|
|
9677
|
+
if (!this.inside(dir, rootReal)) throw new FilesError(403, `${ref.abs} leads outside its root`, "outside");
|
|
9678
|
+
for (const d of parts.reverse()) {
|
|
9679
|
+
try {
|
|
9680
|
+
await fsp.mkdir(fsPath(d));
|
|
9681
|
+
} catch (err) {
|
|
9682
|
+
if (err.code !== "EEXIST") throw fsError(err, d);
|
|
9683
|
+
const st = await fsp.lstat(fsPath(d));
|
|
9684
|
+
if (!st.isDirectory()) throw new FilesError(409, `${d} is not a directory`, "not-a-directory");
|
|
9473
9685
|
}
|
|
9474
|
-
default:
|
|
9475
|
-
return json(res, 404, { error: "not found" });
|
|
9476
9686
|
}
|
|
9477
9687
|
}
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
const
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
if (!
|
|
9488
|
-
|
|
9489
|
-
this.channel(
|
|
9490
|
-
"terminals",
|
|
9491
|
-
open3,
|
|
9492
|
-
() => ws.terminate(),
|
|
9493
|
-
(cb) => ws.once("close", cb)
|
|
9494
|
-
);
|
|
9495
|
-
if (url.pathname === "/ws/terminal") {
|
|
9496
|
-
serveTerminalSocket(ws, streams, { pane: url.searchParams.get("pane") ?? void 0, cols: url.searchParams.get("cols") ?? void 0, rows: url.searchParams.get("rows") ?? void 0 });
|
|
9497
|
-
return;
|
|
9498
|
-
}
|
|
9499
|
-
this.events(ws, hub);
|
|
9500
|
-
});
|
|
9688
|
+
async realParent(loc) {
|
|
9689
|
+
const rootReal = await this.realRoot(loc.root);
|
|
9690
|
+
const dir = path15.dirname(loc.abs);
|
|
9691
|
+
let real;
|
|
9692
|
+
try {
|
|
9693
|
+
real = decodeName(await fsp.realpath(fsPath(dir), { encoding: "buffer" }));
|
|
9694
|
+
} catch (err) {
|
|
9695
|
+
throw fsError(err, loc.abs);
|
|
9696
|
+
}
|
|
9697
|
+
if (!this.inside(real, rootReal)) throw new FilesError(403, `${loc.abs} leads outside its root`, "outside");
|
|
9698
|
+
return real;
|
|
9501
9699
|
}
|
|
9502
|
-
/**
|
|
9700
|
+
/** The lexical path a real path inside a root is reported as. */
|
|
9701
|
+
present(root, rootReal, real) {
|
|
9702
|
+
if (real === rootReal) return root.path;
|
|
9703
|
+
return path15.join(root.path, real.slice(rootReal.length + 1));
|
|
9704
|
+
}
|
|
9705
|
+
};
|
|
9706
|
+
function fsError(err, where) {
|
|
9707
|
+
if (err instanceof FilesError) return err;
|
|
9708
|
+
const code2 = err?.code;
|
|
9709
|
+
switch (code2) {
|
|
9710
|
+
case "ENOENT":
|
|
9711
|
+
return new FilesError(404, `${where} does not exist`, "not-found");
|
|
9712
|
+
case "EEXIST":
|
|
9713
|
+
case "ENOTEMPTY":
|
|
9714
|
+
return new FilesError(409, `${where} already exists`, "exists");
|
|
9715
|
+
case "ENOTDIR":
|
|
9716
|
+
return new FilesError(409, `a parent of ${where} is not a directory`, "not-a-directory");
|
|
9717
|
+
case "EISDIR":
|
|
9718
|
+
return new FilesError(409, `${where} is a directory`, "is-a-directory");
|
|
9719
|
+
case "ELOOP":
|
|
9720
|
+
return new FilesError(400, `${where} is a symlink loop`, "loop");
|
|
9721
|
+
case "EACCES":
|
|
9722
|
+
case "EPERM":
|
|
9723
|
+
return new FilesError(403, `permission denied: ${where}`, "denied");
|
|
9724
|
+
case "ENAMETOOLONG":
|
|
9725
|
+
return new FilesError(400, `name too long: ${where}`, "too-long");
|
|
9726
|
+
case "ENOSPC":
|
|
9727
|
+
case "EDQUOT":
|
|
9728
|
+
return new FilesError(507, "no space left on the volume", "no-space");
|
|
9729
|
+
case "EINVAL":
|
|
9730
|
+
return new FilesError(400, `invalid operation on ${where}`, "invalid");
|
|
9731
|
+
default:
|
|
9732
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
9733
|
+
}
|
|
9734
|
+
}
|
|
9735
|
+
|
|
9736
|
+
// ../hostcore/src/files/entries.ts
|
|
9737
|
+
var MAX_PAGE = 5e3;
|
|
9738
|
+
function typeOf(st) {
|
|
9739
|
+
if (st.isSymbolicLink()) return "symlink";
|
|
9740
|
+
if (st.isDirectory()) return "dir";
|
|
9741
|
+
if (st.isFile()) return "file";
|
|
9742
|
+
return "other";
|
|
9743
|
+
}
|
|
9744
|
+
async function describe(abs, fs26, rootReal, git) {
|
|
9745
|
+
let st;
|
|
9746
|
+
try {
|
|
9747
|
+
st = await fsp2.lstat(fsPath(fs26));
|
|
9748
|
+
} catch (err) {
|
|
9749
|
+
throw fsError(err, abs);
|
|
9750
|
+
}
|
|
9751
|
+
return describeStat(abs, fs26, st, rootReal, git);
|
|
9752
|
+
}
|
|
9753
|
+
async function describeStat(abs, fs26, st, rootReal, git) {
|
|
9754
|
+
const name = abs === "/" ? "/" : path16.basename(abs);
|
|
9755
|
+
const type = typeOf(st);
|
|
9756
|
+
const entry = {
|
|
9757
|
+
name,
|
|
9758
|
+
path: abs,
|
|
9759
|
+
type,
|
|
9760
|
+
size: type === "dir" ? 0 : st.size,
|
|
9761
|
+
mtime: Math.round(st.mtimeMs)
|
|
9762
|
+
};
|
|
9763
|
+
if (hasEscapes(name)) entry.rawName = true;
|
|
9764
|
+
if (type === "symlink") {
|
|
9765
|
+
try {
|
|
9766
|
+
entry.target = decodeName(await fsp2.readlink(fsPath(fs26), { encoding: "buffer" }));
|
|
9767
|
+
} catch {
|
|
9768
|
+
entry.target = "";
|
|
9769
|
+
}
|
|
9770
|
+
entry.targetType = null;
|
|
9771
|
+
try {
|
|
9772
|
+
const real = decodeName(await fsp2.realpath(fsPath(fs26), { encoding: "buffer" }));
|
|
9773
|
+
if (within2(real, rootReal)) {
|
|
9774
|
+
const t2 = typeOf(await fsp2.stat(fsPath(real)));
|
|
9775
|
+
entry.targetType = t2 === "symlink" ? null : t2;
|
|
9776
|
+
}
|
|
9777
|
+
} catch {
|
|
9778
|
+
}
|
|
9779
|
+
}
|
|
9780
|
+
if (git) entry.git = git(abs, type === "dir" || entry.targetType === "dir");
|
|
9781
|
+
return entry;
|
|
9782
|
+
}
|
|
9783
|
+
var collator = new Intl.Collator(void 0, { numeric: true, sensitivity: "base" });
|
|
9784
|
+
var ListingCache = class {
|
|
9785
|
+
cache = /* @__PURE__ */ new Map();
|
|
9786
|
+
held = 0;
|
|
9787
|
+
ttlMs;
|
|
9788
|
+
maxDirs;
|
|
9789
|
+
maxNames;
|
|
9790
|
+
settleMs;
|
|
9791
|
+
constructor(opts = {}) {
|
|
9792
|
+
this.ttlMs = opts.ttlMs ?? 5e3;
|
|
9793
|
+
this.maxDirs = opts.maxDirs ?? 64;
|
|
9794
|
+
this.maxNames = opts.maxNames ?? 2e5;
|
|
9795
|
+
this.settleMs = opts.settleMs ?? 2e3;
|
|
9796
|
+
}
|
|
9797
|
+
/** What is held now: directories, and names across them. */
|
|
9798
|
+
get size() {
|
|
9799
|
+
return { dirs: this.cache.size, names: this.held };
|
|
9800
|
+
}
|
|
9801
|
+
async names(real, where) {
|
|
9802
|
+
const now = Date.now();
|
|
9803
|
+
this.expire(now);
|
|
9804
|
+
let stamp;
|
|
9805
|
+
let mtimeMs;
|
|
9806
|
+
let dirents;
|
|
9807
|
+
try {
|
|
9808
|
+
const st = await fsp2.stat(fsPath(real), { bigint: true });
|
|
9809
|
+
stamp = `${st.ino}:${st.mtimeNs}:${st.ctimeNs}`;
|
|
9810
|
+
mtimeMs = Number(st.mtimeNs / 1000000n);
|
|
9811
|
+
const hit = this.cache.get(real);
|
|
9812
|
+
if (hit && hit.stamp === stamp) {
|
|
9813
|
+
this.cache.delete(real);
|
|
9814
|
+
this.cache.set(real, hit);
|
|
9815
|
+
return hit.names;
|
|
9816
|
+
}
|
|
9817
|
+
dirents = await fsp2.readdir(fsPath(real), { withFileTypes: true, encoding: "buffer" });
|
|
9818
|
+
} catch (err) {
|
|
9819
|
+
throw fsError(err, where);
|
|
9820
|
+
}
|
|
9821
|
+
const names = dirents.map((d) => ({ name: decodeName(d.name), dir: d.isDirectory() })).sort((a, b) => a.dir === b.dir ? collator.compare(a.name, b.name) : a.dir ? -1 : 1);
|
|
9822
|
+
this.forget(real);
|
|
9823
|
+
const settled = now - mtimeMs >= this.settleMs;
|
|
9824
|
+
if (this.ttlMs > 0 && settled && names.length <= this.maxNames) {
|
|
9825
|
+
this.cache.set(real, { at: now, stamp, names });
|
|
9826
|
+
this.held += names.length;
|
|
9827
|
+
while (this.cache.size > this.maxDirs || this.held > this.maxNames) this.forget(this.cache.keys().next().value);
|
|
9828
|
+
}
|
|
9829
|
+
return names;
|
|
9830
|
+
}
|
|
9831
|
+
forget(real) {
|
|
9832
|
+
const kept = this.cache.get(real);
|
|
9833
|
+
if (!kept) return;
|
|
9834
|
+
this.cache.delete(real);
|
|
9835
|
+
this.held -= kept.names.length;
|
|
9836
|
+
}
|
|
9837
|
+
/** Drop what has outlived its time, used or not, so nothing idles in memory. */
|
|
9838
|
+
expire(now) {
|
|
9839
|
+
for (const [real, kept] of this.cache) if (now - kept.at >= this.ttlMs) this.forget(real);
|
|
9840
|
+
}
|
|
9841
|
+
};
|
|
9842
|
+
async function listDirectory(roots, dir, opts) {
|
|
9843
|
+
const rootReal = await roots.realRoot(dir.root);
|
|
9844
|
+
const all = await (opts.cache ?? new ListingCache({ ttlMs: 0 })).names(dir.real, dir.abs);
|
|
9845
|
+
const shown = all.filter((d) => !roots.hidden(path16.join(dir.real, d.name)));
|
|
9846
|
+
const named = opts.hidden ? shown : shown.filter((d) => !d.name.startsWith("."));
|
|
9847
|
+
const page = named.slice(opts.offset, opts.offset + opts.limit);
|
|
9848
|
+
const entries = await mapLimit(page, 64, async (d) => {
|
|
9849
|
+
const abs = path16.join(dir.abs, d.name);
|
|
9850
|
+
try {
|
|
9851
|
+
return await describe(abs, path16.join(dir.real, d.name), rootReal, opts.git);
|
|
9852
|
+
} catch (err) {
|
|
9853
|
+
if (err instanceof FilesError && err.status === 404) return null;
|
|
9854
|
+
throw err;
|
|
9855
|
+
}
|
|
9856
|
+
});
|
|
9857
|
+
const kept = entries.filter((e) => e !== null);
|
|
9858
|
+
return {
|
|
9859
|
+
path: dir.abs,
|
|
9860
|
+
// The wire type names the box's two roots; a machine's are its own ids.
|
|
9861
|
+
root: dir.root.id,
|
|
9862
|
+
entries: kept,
|
|
9863
|
+
total: named.length,
|
|
9864
|
+
offset: opts.offset,
|
|
9865
|
+
truncated: opts.offset + page.length < named.length
|
|
9866
|
+
};
|
|
9867
|
+
}
|
|
9868
|
+
async function mapLimit(items, limit, fn) {
|
|
9869
|
+
const out = new Array(items.length);
|
|
9870
|
+
let next = 0;
|
|
9871
|
+
const worker = async () => {
|
|
9872
|
+
while (next < items.length) {
|
|
9873
|
+
const i = next++;
|
|
9874
|
+
out[i] = await fn(items[i]);
|
|
9875
|
+
}
|
|
9876
|
+
};
|
|
9877
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
9878
|
+
return out;
|
|
9879
|
+
}
|
|
9880
|
+
|
|
9881
|
+
// ../hostcore/src/files/git.ts
|
|
9882
|
+
import { spawn as spawn7 } from "node:child_process";
|
|
9883
|
+
import fsp3 from "node:fs/promises";
|
|
9884
|
+
import path17 from "node:path";
|
|
9885
|
+
function runGit(args, opts) {
|
|
9886
|
+
return new Promise((resolve, reject) => {
|
|
9887
|
+
const child = spawn7(opts.git ?? "git", args, {
|
|
9888
|
+
cwd: opts.cwd,
|
|
9889
|
+
// Status must never take the index lock: an agent committing in the
|
|
9890
|
+
// same repository would otherwise see "index.lock exists" at random.
|
|
9891
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", LC_ALL: "C", GIT_TERMINAL_PROMPT: "0" },
|
|
9892
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
9893
|
+
});
|
|
9894
|
+
const chunks = [];
|
|
9895
|
+
let size = 0;
|
|
9896
|
+
const max = opts.maxBytes ?? 64 * 1024 * 1024;
|
|
9897
|
+
let stderr = "";
|
|
9898
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), opts.timeoutMs ?? 1e4);
|
|
9899
|
+
child.stdout.on("data", (c) => {
|
|
9900
|
+
size += c.length;
|
|
9901
|
+
if (size > max) child.kill("SIGKILL");
|
|
9902
|
+
else chunks.push(c);
|
|
9903
|
+
});
|
|
9904
|
+
child.stderr.on("data", (c) => {
|
|
9905
|
+
if (stderr.length < 4096) stderr += c.toString();
|
|
9906
|
+
});
|
|
9907
|
+
child.on("error", (err) => {
|
|
9908
|
+
clearTimeout(timer);
|
|
9909
|
+
reject(err);
|
|
9910
|
+
});
|
|
9911
|
+
child.on("close", (code2, signal) => {
|
|
9912
|
+
clearTimeout(timer);
|
|
9913
|
+
if (code2 === 0) resolve(Buffer.concat(chunks));
|
|
9914
|
+
else reject(new Error(`git ${args[0]} failed (${signal ?? code2}): ${stderr.trim().slice(0, 300)}`));
|
|
9915
|
+
});
|
|
9916
|
+
});
|
|
9917
|
+
}
|
|
9918
|
+
function parseStatus(top, out) {
|
|
9919
|
+
const status3 = {
|
|
9920
|
+
top,
|
|
9921
|
+
branch: null,
|
|
9922
|
+
detached: false,
|
|
9923
|
+
upstream: null,
|
|
9924
|
+
ahead: 0,
|
|
9925
|
+
behind: 0,
|
|
9926
|
+
files: /* @__PURE__ */ new Map(),
|
|
9927
|
+
dirty: /* @__PURE__ */ new Set(),
|
|
9928
|
+
changed: 0
|
|
9929
|
+
};
|
|
9930
|
+
const records = [];
|
|
9931
|
+
let start = 0;
|
|
9932
|
+
for (let i = 0; i < out.length; i++) {
|
|
9933
|
+
if (out[i] === 0) {
|
|
9934
|
+
records.push(out.subarray(start, i));
|
|
9935
|
+
start = i + 1;
|
|
9936
|
+
}
|
|
9937
|
+
}
|
|
9938
|
+
if (start < out.length) records.push(out.subarray(start));
|
|
9939
|
+
const add2 = (p, s) => {
|
|
9940
|
+
const rel = p.replace(/\/+$/, "");
|
|
9941
|
+
status3.files.set(rel, s);
|
|
9942
|
+
if (s === "ignored") return;
|
|
9943
|
+
status3.changed += 1;
|
|
9944
|
+
let dir = path17.posix.dirname(rel);
|
|
9945
|
+
for (; ; ) {
|
|
9946
|
+
const key = dir === "." ? "" : dir;
|
|
9947
|
+
if (status3.dirty.has(key)) break;
|
|
9948
|
+
status3.dirty.add(key);
|
|
9949
|
+
if (key === "") break;
|
|
9950
|
+
dir = path17.posix.dirname(dir);
|
|
9951
|
+
}
|
|
9952
|
+
};
|
|
9953
|
+
for (let r = 0; r < records.length; r++) {
|
|
9954
|
+
const rec = decodeName(records[r]);
|
|
9955
|
+
if (rec.startsWith("# ")) {
|
|
9956
|
+
const [key, ...rest] = rec.slice(2).split(" ");
|
|
9957
|
+
const value = rest.join(" ");
|
|
9958
|
+
if (key === "branch.head") {
|
|
9959
|
+
status3.detached = value === "(detached)";
|
|
9960
|
+
status3.branch = status3.detached ? null : value;
|
|
9961
|
+
} else if (key === "branch.upstream") {
|
|
9962
|
+
status3.upstream = value;
|
|
9963
|
+
} else if (key === "branch.ab") {
|
|
9964
|
+
const m = /^\+(\d+) -(\d+)$/.exec(value);
|
|
9965
|
+
if (m) {
|
|
9966
|
+
status3.ahead = Number(m[1]);
|
|
9967
|
+
status3.behind = Number(m[2]);
|
|
9968
|
+
}
|
|
9969
|
+
}
|
|
9970
|
+
continue;
|
|
9971
|
+
}
|
|
9972
|
+
const kind = rec[0];
|
|
9973
|
+
if (kind === "?") add2(rec.slice(2), "untracked");
|
|
9974
|
+
else if (kind === "!") add2(rec.slice(2), "ignored");
|
|
9975
|
+
else if (kind === "1") add2(fieldsAfter(rec, 8), classify(rec.slice(2, 4)));
|
|
9976
|
+
else if (kind === "2") {
|
|
9977
|
+
add2(fieldsAfter(rec, 9), "renamed");
|
|
9978
|
+
r += 1;
|
|
9979
|
+
} else if (kind === "u") add2(fieldsAfter(rec, 10), "conflicted");
|
|
9980
|
+
}
|
|
9981
|
+
return status3;
|
|
9982
|
+
}
|
|
9983
|
+
function fieldsAfter(rec, n) {
|
|
9984
|
+
let i = 0;
|
|
9985
|
+
for (let k = 0; k < n; k++) {
|
|
9986
|
+
i = rec.indexOf(" ", i) + 1;
|
|
9987
|
+
if (i === 0) return "";
|
|
9988
|
+
}
|
|
9989
|
+
return rec.slice(i);
|
|
9990
|
+
}
|
|
9991
|
+
function classify(xy) {
|
|
9992
|
+
if (xy.includes("D")) return "deleted";
|
|
9993
|
+
if (xy.includes("A")) return "added";
|
|
9994
|
+
return "modified";
|
|
9995
|
+
}
|
|
9996
|
+
var GitStatusCache = class {
|
|
9997
|
+
ttlMs;
|
|
9998
|
+
timeoutMs;
|
|
9999
|
+
git;
|
|
10000
|
+
cache = /* @__PURE__ */ new Map();
|
|
10001
|
+
constructor(opts = {}) {
|
|
10002
|
+
this.ttlMs = opts.ttlMs ?? 2e3;
|
|
10003
|
+
this.timeoutMs = opts.timeoutMs ?? 1e4;
|
|
10004
|
+
this.git = opts.git ?? "git";
|
|
10005
|
+
}
|
|
10006
|
+
/**
|
|
10007
|
+
* The top of the repository containing `dir`, looking no higher than
|
|
10008
|
+
* `stop` (a root): the nearest ancestor with a `.git` entry.
|
|
10009
|
+
*/
|
|
10010
|
+
async repoFor(dir, stop) {
|
|
10011
|
+
let d = dir;
|
|
10012
|
+
for (; ; ) {
|
|
10013
|
+
try {
|
|
10014
|
+
await fsp3.lstat(fsPath(path17.join(d, ".git")));
|
|
10015
|
+
return d;
|
|
10016
|
+
} catch {
|
|
10017
|
+
}
|
|
10018
|
+
if (d === stop || !within2(d, stop)) return null;
|
|
10019
|
+
d = path17.dirname(d);
|
|
10020
|
+
}
|
|
10021
|
+
}
|
|
10022
|
+
/** The status of the repository at `top`, or null when git cannot say. */
|
|
10023
|
+
status(top) {
|
|
10024
|
+
const now = Date.now();
|
|
10025
|
+
const hit = this.cache.get(top);
|
|
10026
|
+
if (hit && now - hit.at < this.ttlMs) return hit.value;
|
|
10027
|
+
if (hasEscapes(top)) return Promise.resolve(null);
|
|
10028
|
+
const value = runGit(
|
|
10029
|
+
["status", "--porcelain=v2", "-z", "--branch", "--untracked-files=normal", "--ignored=traditional"],
|
|
10030
|
+
{ cwd: top, timeoutMs: this.timeoutMs, git: this.git }
|
|
10031
|
+
).then(
|
|
10032
|
+
(out) => parseStatus(top, out),
|
|
10033
|
+
() => null
|
|
10034
|
+
);
|
|
10035
|
+
this.cache.set(top, { at: now, value });
|
|
10036
|
+
if (this.cache.size > 256) {
|
|
10037
|
+
for (const [k, v] of this.cache) if (now - v.at >= this.ttlMs) this.cache.delete(k);
|
|
10038
|
+
}
|
|
10039
|
+
return value;
|
|
10040
|
+
}
|
|
10041
|
+
/** Seconds-since-epoch of the last commit, in milliseconds, or null. */
|
|
10042
|
+
async lastCommit(top) {
|
|
10043
|
+
try {
|
|
10044
|
+
const out = await runGit(["log", "-1", "--format=%ct"], { cwd: top, timeoutMs: this.timeoutMs, git: this.git });
|
|
10045
|
+
const n = Number(out.toString().trim());
|
|
10046
|
+
return Number.isFinite(n) && n > 0 ? n * 1e3 : null;
|
|
10047
|
+
} catch {
|
|
10048
|
+
return null;
|
|
10049
|
+
}
|
|
10050
|
+
}
|
|
10051
|
+
/** A per-entry lookup for listing `dir`, or undefined outside any repository. */
|
|
10052
|
+
async lookupFor(dir, stop) {
|
|
10053
|
+
const top = await this.repoFor(dir, stop);
|
|
10054
|
+
if (!top) return void 0;
|
|
10055
|
+
const st = await this.status(top);
|
|
10056
|
+
if (!st) return void 0;
|
|
10057
|
+
return (abs, isDir2) => statusOf(st, path17.relative(top, abs).split(path17.sep).join("/"), isDir2);
|
|
10058
|
+
}
|
|
10059
|
+
};
|
|
10060
|
+
function statusOf(st, rel, isDir2) {
|
|
10061
|
+
if (rel === ".git" || rel.startsWith(".git/")) return null;
|
|
10062
|
+
const own = st.files.get(rel);
|
|
10063
|
+
if (own) return own;
|
|
10064
|
+
if (isDir2 && st.dirty.has(rel)) return "modified";
|
|
10065
|
+
let dir = path17.posix.dirname(rel);
|
|
10066
|
+
while (dir !== "." && dir !== "") {
|
|
10067
|
+
const s = st.files.get(dir);
|
|
10068
|
+
if (s === "untracked" || s === "ignored") return s;
|
|
10069
|
+
dir = path17.posix.dirname(dir);
|
|
10070
|
+
}
|
|
10071
|
+
return null;
|
|
10072
|
+
}
|
|
10073
|
+
|
|
10074
|
+
// ../hostcore/src/files/ops.ts
|
|
10075
|
+
import fsp5 from "node:fs/promises";
|
|
10076
|
+
import path19 from "node:path";
|
|
10077
|
+
|
|
10078
|
+
// ../hostcore/src/files/tree.ts
|
|
10079
|
+
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
10080
|
+
import { constants } from "node:fs";
|
|
10081
|
+
import fsp4 from "node:fs/promises";
|
|
10082
|
+
import path18 from "node:path";
|
|
10083
|
+
async function copyTree(src, dst) {
|
|
10084
|
+
const st = await fsp4.lstat(fsPath(src));
|
|
10085
|
+
if (st.isSymbolicLink()) {
|
|
10086
|
+
const link = await fsp4.readlink(fsPath(src), { encoding: "buffer" });
|
|
10087
|
+
await fsp4.symlink(link, fsPath(dst));
|
|
10088
|
+
} else if (st.isDirectory()) {
|
|
10089
|
+
await fsp4.mkdir(fsPath(dst), { mode: st.mode & 4095 });
|
|
10090
|
+
const names = await fsp4.readdir(fsPath(src), { encoding: "buffer" });
|
|
10091
|
+
for (const raw2 of names) {
|
|
10092
|
+
const name = decodeName(raw2);
|
|
10093
|
+
await copyTree(path18.join(src, name), path18.join(dst, name));
|
|
10094
|
+
}
|
|
10095
|
+
} else if (st.isFile()) {
|
|
10096
|
+
await fsp4.copyFile(fsPath(src), fsPath(dst), constants.COPYFILE_EXCL);
|
|
10097
|
+
}
|
|
10098
|
+
}
|
|
10099
|
+
async function removeTree(p) {
|
|
10100
|
+
await fsp4.rm(fsPath(p), { recursive: true, force: true });
|
|
10101
|
+
}
|
|
10102
|
+
function scratchName(dir, label) {
|
|
10103
|
+
return path18.join(dir, `.${label}-${randomBytes8(8).toString("hex")}`);
|
|
10104
|
+
}
|
|
10105
|
+
async function moveTree(src, dst, scratchDir) {
|
|
10106
|
+
try {
|
|
10107
|
+
await fsp4.rename(fsPath(src), fsPath(dst));
|
|
10108
|
+
return;
|
|
10109
|
+
} catch (err) {
|
|
10110
|
+
if (err.code !== "EXDEV") throw err;
|
|
10111
|
+
}
|
|
10112
|
+
await fsp4.mkdir(fsPath(scratchDir), { recursive: true });
|
|
10113
|
+
const tmp = scratchName(scratchDir, "move");
|
|
10114
|
+
try {
|
|
10115
|
+
await copyTree(src, tmp);
|
|
10116
|
+
await noClobberRename(tmp, dst);
|
|
10117
|
+
} catch (err) {
|
|
10118
|
+
await removeTree(tmp).catch(() => {
|
|
10119
|
+
});
|
|
10120
|
+
throw err;
|
|
10121
|
+
}
|
|
10122
|
+
await removeTree(src);
|
|
10123
|
+
}
|
|
10124
|
+
async function noClobberRename(src, dst) {
|
|
10125
|
+
const st = await fsp4.lstat(fsPath(src));
|
|
10126
|
+
if (st.isFile()) {
|
|
10127
|
+
try {
|
|
10128
|
+
await fsp4.link(fsPath(src), fsPath(dst));
|
|
10129
|
+
await fsp4.unlink(fsPath(src));
|
|
10130
|
+
return;
|
|
10131
|
+
} catch (err) {
|
|
10132
|
+
const code2 = err.code;
|
|
10133
|
+
if (code2 !== "EPERM" && code2 !== "ENOTSUP" && code2 !== "EOPNOTSUPP" && code2 !== "EXDEV") throw err;
|
|
10134
|
+
}
|
|
10135
|
+
}
|
|
10136
|
+
try {
|
|
10137
|
+
await fsp4.lstat(fsPath(dst));
|
|
10138
|
+
const err = new Error(`${dst} exists`);
|
|
10139
|
+
err.code = "EEXIST";
|
|
10140
|
+
throw err;
|
|
10141
|
+
} catch (err) {
|
|
10142
|
+
if (err.code !== "ENOENT") throw err;
|
|
10143
|
+
}
|
|
10144
|
+
await fsp4.rename(fsPath(src), fsPath(dst));
|
|
10145
|
+
}
|
|
10146
|
+
|
|
10147
|
+
// ../hostcore/src/files/ops.ts
|
|
10148
|
+
var MAX_WRITE2 = 1024 * 1024;
|
|
10149
|
+
var FileOps = class {
|
|
10150
|
+
constructor(roots, trash) {
|
|
10151
|
+
this.roots = roots;
|
|
10152
|
+
this.trash = trash;
|
|
10153
|
+
}
|
|
10154
|
+
roots;
|
|
10155
|
+
trash;
|
|
10156
|
+
scratch(ref) {
|
|
10157
|
+
return path19.join(this.roots.stateDir(ref.root), "uploads");
|
|
10158
|
+
}
|
|
10159
|
+
async realRoot(ref) {
|
|
10160
|
+
return this.roots.realRoot(ref.root);
|
|
10161
|
+
}
|
|
10162
|
+
/** Write a small text file: new by default, replaced with `overwrite`. */
|
|
10163
|
+
async write(body) {
|
|
10164
|
+
if (typeof body.content !== "string") throw new FilesError(400, "content must be text");
|
|
10165
|
+
const data = Buffer.from(body.content, "utf8");
|
|
10166
|
+
if (data.length > MAX_WRITE2) throw new FilesError(413, `write takes at most ${MAX_WRITE2} bytes; upload larger files`);
|
|
10167
|
+
const dest = await this.roots.creatable(body.path);
|
|
10168
|
+
this.roots.assertMutable(dest);
|
|
10169
|
+
await this.roots.ensureParent(dest);
|
|
10170
|
+
if (body.overwrite === true) {
|
|
10171
|
+
const existing = await fsp5.lstat(fsPath(dest.fs)).catch(() => null);
|
|
10172
|
+
if (existing?.isDirectory()) throw new FilesError(409, `${dest.abs} is a directory`, "is-a-directory");
|
|
10173
|
+
const dir = this.scratch(dest);
|
|
10174
|
+
await fsp5.mkdir(dir, { recursive: true });
|
|
10175
|
+
const tmp = scratchName(dir, "write");
|
|
10176
|
+
await fsp5.writeFile(tmp, data);
|
|
10177
|
+
if (existing?.isFile()) await fsp5.chmod(tmp, existing.mode & 4095);
|
|
10178
|
+
try {
|
|
10179
|
+
await fsp5.rename(tmp, fsPath(dest.fs));
|
|
10180
|
+
} catch (err) {
|
|
10181
|
+
await fsp5.rm(tmp, { force: true });
|
|
10182
|
+
throw fsError(err, dest.abs);
|
|
10183
|
+
}
|
|
10184
|
+
} else {
|
|
10185
|
+
try {
|
|
10186
|
+
await fsp5.writeFile(fsPath(dest.fs), data, { flag: "wx" });
|
|
10187
|
+
} catch (err) {
|
|
10188
|
+
throw fsError(err, dest.abs);
|
|
10189
|
+
}
|
|
10190
|
+
}
|
|
10191
|
+
return describe(dest.abs, dest.fs, await this.realRoot(dest));
|
|
10192
|
+
}
|
|
10193
|
+
/** Make a directory and any missing parents. An existing directory is fine. */
|
|
10194
|
+
async mkdir(body) {
|
|
10195
|
+
const dest = await this.roots.creatable(body.path);
|
|
10196
|
+
this.roots.assertMutable(dest);
|
|
10197
|
+
await this.roots.ensureParent(dest);
|
|
10198
|
+
try {
|
|
10199
|
+
await fsp5.mkdir(fsPath(dest.fs));
|
|
10200
|
+
} catch (err) {
|
|
10201
|
+
if (err.code !== "EEXIST") throw fsError(err, dest.abs);
|
|
10202
|
+
const st = await fsp5.lstat(fsPath(dest.fs));
|
|
10203
|
+
if (!st.isDirectory()) throw new FilesError(409, `${dest.abs} exists and is not a directory`, "exists");
|
|
10204
|
+
}
|
|
10205
|
+
return describe(dest.abs, dest.fs, await this.realRoot(dest));
|
|
10206
|
+
}
|
|
10207
|
+
/** Rename or move `from` to `to` — the entry itself, not what a link points at. */
|
|
10208
|
+
async move(body) {
|
|
10209
|
+
const src = await this.roots.entry(body.from);
|
|
10210
|
+
this.roots.assertMutable(src);
|
|
10211
|
+
await this.roots.assertMovable(src);
|
|
10212
|
+
const dest = await this.roots.creatable(body.to);
|
|
10213
|
+
this.roots.assertMutable(dest);
|
|
10214
|
+
if (dest.abs === src.abs) return describe(src.abs, src.fs, await this.roots.realRoot(src.root));
|
|
10215
|
+
await this.refuseIntoItself(src.fs, dest.fs, src.abs);
|
|
10216
|
+
await this.clear(dest, body.overwrite === true);
|
|
10217
|
+
await this.roots.ensureParent(dest);
|
|
10218
|
+
try {
|
|
10219
|
+
if (src.root.id === dest.root.id) await noClobberRename(src.fs, dest.fs);
|
|
10220
|
+
else await moveTree(src.fs, dest.fs, this.scratch(dest));
|
|
10221
|
+
} catch (err) {
|
|
10222
|
+
throw fsError(err, dest.abs);
|
|
10223
|
+
}
|
|
10224
|
+
return describe(dest.abs, dest.fs, await this.realRoot(dest));
|
|
10225
|
+
}
|
|
10226
|
+
/**
|
|
10227
|
+
* Copy `from` to `to`. A directory is copied whole, symlinks as symlinks.
|
|
10228
|
+
* The copy is built under a scratch name and renamed into place, so the
|
|
10229
|
+
* destination never shows a half-finished tree.
|
|
10230
|
+
*/
|
|
10231
|
+
async copy(body) {
|
|
10232
|
+
const src = await this.roots.entry(body.from);
|
|
10233
|
+
await this.roots.assertMovable(src);
|
|
10234
|
+
const dest = await this.roots.creatable(body.to);
|
|
10235
|
+
this.roots.assertMutable(dest);
|
|
10236
|
+
if (dest.abs === src.abs) throw new FilesError(409, "a copy needs a different name", "exists");
|
|
10237
|
+
await this.refuseIntoItself(src.fs, dest.fs, src.abs);
|
|
10238
|
+
await this.clear(dest, body.overwrite === true);
|
|
10239
|
+
await this.roots.ensureParent(dest);
|
|
10240
|
+
const dir = this.scratch(dest);
|
|
10241
|
+
await fsp5.mkdir(dir, { recursive: true });
|
|
10242
|
+
const tmp = scratchName(dir, "copy");
|
|
10243
|
+
try {
|
|
10244
|
+
await copyTree(src.fs, tmp);
|
|
10245
|
+
await noClobberRename(tmp, dest.fs);
|
|
10246
|
+
} catch (err) {
|
|
10247
|
+
await removeTree(tmp).catch(() => {
|
|
10248
|
+
});
|
|
10249
|
+
throw fsError(err, dest.abs);
|
|
10250
|
+
}
|
|
10251
|
+
return describe(dest.abs, dest.fs, await this.realRoot(dest));
|
|
10252
|
+
}
|
|
10253
|
+
/** A directory cannot be moved or copied into its own subtree. */
|
|
10254
|
+
async refuseIntoItself(srcFs, destFs, abs) {
|
|
10255
|
+
const st = await fsp5.lstat(fsPath(srcFs));
|
|
10256
|
+
if (st.isDirectory() && within2(destFs, srcFs)) {
|
|
10257
|
+
throw new FilesError(400, `${abs} cannot go inside itself`, "into-itself");
|
|
10258
|
+
}
|
|
10259
|
+
}
|
|
10260
|
+
/** Make way at `dest`: refuse, or with `overwrite` move what is there to the trash. */
|
|
10261
|
+
async clear(dest, overwrite) {
|
|
10262
|
+
const st = await fsp5.lstat(fsPath(dest.fs)).catch(() => null);
|
|
10263
|
+
if (!st) return;
|
|
10264
|
+
if (!overwrite) throw new FilesError(409, `${dest.abs} already exists`, "exists");
|
|
10265
|
+
await this.trash.put(dest.root, dest.abs, dest.fs);
|
|
10266
|
+
}
|
|
10267
|
+
};
|
|
10268
|
+
|
|
10269
|
+
// ../hostcore/src/files/trash.ts
|
|
10270
|
+
import { randomBytes as randomBytes9 } from "node:crypto";
|
|
10271
|
+
import fsp6 from "node:fs/promises";
|
|
10272
|
+
import path20 from "node:path";
|
|
10273
|
+
var TRASH_ID = /^[0-9a-f]{24}$/;
|
|
10274
|
+
var Trash = class {
|
|
10275
|
+
constructor(roots) {
|
|
10276
|
+
this.roots = roots;
|
|
10277
|
+
}
|
|
10278
|
+
roots;
|
|
10279
|
+
dir(root) {
|
|
10280
|
+
return path20.join(this.roots.stateDir(root), "trash");
|
|
10281
|
+
}
|
|
10282
|
+
async trash(paths2) {
|
|
10283
|
+
if (!Array.isArray(paths2) || paths2.length === 0) throw new FilesError(400, "paths required");
|
|
10284
|
+
const result = { trashed: [], missing: [] };
|
|
10285
|
+
for (const p of paths2) {
|
|
10286
|
+
const loc = this.roots.locate(p);
|
|
10287
|
+
this.roots.assertMutable(loc);
|
|
10288
|
+
let ref;
|
|
10289
|
+
try {
|
|
10290
|
+
ref = await this.roots.entry(p);
|
|
10291
|
+
} catch (err) {
|
|
10292
|
+
if (err instanceof FilesError && err.status === 404) {
|
|
10293
|
+
result.missing.push(loc.abs);
|
|
10294
|
+
continue;
|
|
10295
|
+
}
|
|
10296
|
+
throw err;
|
|
10297
|
+
}
|
|
10298
|
+
result.trashed.push(await this.put(ref.root, ref.abs, ref.fs));
|
|
10299
|
+
}
|
|
10300
|
+
return result;
|
|
10301
|
+
}
|
|
10302
|
+
/** Move the entry at real path `fs` (reported as `abs`) into `root`'s trash. */
|
|
10303
|
+
async put(root, abs, fs26) {
|
|
10304
|
+
const st = await fsp6.lstat(fsPath(fs26)).catch((err) => {
|
|
10305
|
+
throw fsError(err, abs);
|
|
10306
|
+
});
|
|
10307
|
+
const id = randomBytes9(12).toString("hex");
|
|
10308
|
+
const home2 = path20.join(this.dir(root), id);
|
|
10309
|
+
const type = typeOf(st);
|
|
10310
|
+
const meta = {
|
|
10311
|
+
v: 1,
|
|
10312
|
+
id,
|
|
10313
|
+
name: path20.basename(abs),
|
|
10314
|
+
originalPath: abs,
|
|
10315
|
+
root: root.id,
|
|
10316
|
+
type,
|
|
10317
|
+
size: type === "file" ? st.size : null,
|
|
10318
|
+
trashedAt: Date.now()
|
|
10319
|
+
};
|
|
10320
|
+
await fsp6.mkdir(home2, { recursive: true });
|
|
10321
|
+
await writeJson(path20.join(home2, "meta.json"), meta);
|
|
10322
|
+
try {
|
|
10323
|
+
await moveTree(fs26, path20.join(home2, "item"), home2);
|
|
10324
|
+
} catch (err) {
|
|
10325
|
+
await removeTree(home2).catch(() => {
|
|
10326
|
+
});
|
|
10327
|
+
throw fsError(err, abs);
|
|
10328
|
+
}
|
|
10329
|
+
return view(meta);
|
|
10330
|
+
}
|
|
10331
|
+
/** Everything in every root's trash, most recent first. */
|
|
10332
|
+
async list() {
|
|
10333
|
+
const items = [];
|
|
10334
|
+
for (const root of this.roots.list()) {
|
|
10335
|
+
let ids;
|
|
10336
|
+
try {
|
|
10337
|
+
ids = await fsp6.readdir(this.dir(root));
|
|
10338
|
+
} catch {
|
|
10339
|
+
continue;
|
|
10340
|
+
}
|
|
10341
|
+
for (const id of ids) {
|
|
10342
|
+
if (!TRASH_ID.test(id)) continue;
|
|
10343
|
+
const meta = await this.read(root, id);
|
|
10344
|
+
if (meta) items.push(view(meta));
|
|
10345
|
+
}
|
|
10346
|
+
}
|
|
10347
|
+
return items.sort((a, b) => b.trashedAt - a.trashedAt);
|
|
10348
|
+
}
|
|
10349
|
+
async read(root, id) {
|
|
10350
|
+
try {
|
|
10351
|
+
const parsed = JSON.parse(await fsp6.readFile(path20.join(this.dir(root), id, "meta.json"), "utf8"));
|
|
10352
|
+
return parsed && parsed.id === id ? parsed : null;
|
|
10353
|
+
} catch {
|
|
10354
|
+
return null;
|
|
10355
|
+
}
|
|
10356
|
+
}
|
|
10357
|
+
async find(id) {
|
|
10358
|
+
if (!TRASH_ID.test(id)) throw new FilesError(400, "invalid trash id");
|
|
10359
|
+
for (const root of this.roots.list()) {
|
|
10360
|
+
const meta = await this.read(root, id);
|
|
10361
|
+
if (meta) return { root, meta };
|
|
10362
|
+
}
|
|
10363
|
+
throw new FilesError(404, "no such item in the trash", "not-found");
|
|
10364
|
+
}
|
|
10365
|
+
/**
|
|
10366
|
+
* Put an item back where it came from, or at `to`. The destination must not
|
|
10367
|
+
* exist; missing parent directories are recreated.
|
|
10368
|
+
*/
|
|
10369
|
+
async restore(id, to) {
|
|
10370
|
+
const { root, meta } = await this.find(id);
|
|
10371
|
+
const home2 = path20.join(this.dir(root), id);
|
|
10372
|
+
const dest = await this.roots.creatable(to === void 0 ? meta.originalPath : to);
|
|
10373
|
+
this.roots.assertMutable(dest);
|
|
10374
|
+
try {
|
|
10375
|
+
await fsp6.lstat(fsPath(path20.join(home2, "item")));
|
|
10376
|
+
} catch {
|
|
10377
|
+
await removeTree(home2);
|
|
10378
|
+
throw new FilesError(410, "the trashed item is gone", "gone");
|
|
10379
|
+
}
|
|
10380
|
+
await this.roots.ensureParent(dest);
|
|
10381
|
+
try {
|
|
10382
|
+
await fsp6.lstat(fsPath(dest.fs));
|
|
10383
|
+
throw new FilesError(409, `${dest.abs} already exists`, "exists");
|
|
10384
|
+
} catch (err) {
|
|
10385
|
+
if (err instanceof FilesError) throw err;
|
|
10386
|
+
}
|
|
10387
|
+
try {
|
|
10388
|
+
if (dest.root.id === root.id) await noClobberRename(path20.join(home2, "item"), dest.fs);
|
|
10389
|
+
else await moveTree(path20.join(home2, "item"), dest.fs, path20.join(this.roots.stateDir(dest.root), "uploads"));
|
|
10390
|
+
} catch (err) {
|
|
10391
|
+
throw fsError(err, dest.abs);
|
|
10392
|
+
}
|
|
10393
|
+
await removeTree(home2);
|
|
10394
|
+
return describe(dest.abs, dest.fs, await this.roots.realRoot(dest.root));
|
|
10395
|
+
}
|
|
10396
|
+
/** Delete an item for good. */
|
|
10397
|
+
async remove(id) {
|
|
10398
|
+
const { root } = await this.find(id);
|
|
10399
|
+
await removeTree(path20.join(this.dir(root), id));
|
|
10400
|
+
}
|
|
10401
|
+
/**
|
|
10402
|
+
* Delete for good what was trashed before `before` (ms), and entries whose
|
|
10403
|
+
* metadata cannot be read; returns how many went. The box keeps its trash
|
|
10404
|
+
* until it is emptied; a machine's node sweeps its own after 7 days.
|
|
10405
|
+
*/
|
|
10406
|
+
async expire(before) {
|
|
10407
|
+
let n = 0;
|
|
10408
|
+
for (const root of this.roots.list()) {
|
|
10409
|
+
let ids;
|
|
10410
|
+
try {
|
|
10411
|
+
ids = await fsp6.readdir(this.dir(root));
|
|
10412
|
+
} catch {
|
|
10413
|
+
continue;
|
|
10414
|
+
}
|
|
10415
|
+
for (const id of ids) {
|
|
10416
|
+
if (!TRASH_ID.test(id)) continue;
|
|
10417
|
+
const meta = await this.read(root, id);
|
|
10418
|
+
if (meta && meta.trashedAt >= before) continue;
|
|
10419
|
+
if (!meta) {
|
|
10420
|
+
const st = await fsp6.stat(path20.join(this.dir(root), id)).catch(() => null);
|
|
10421
|
+
if (st && st.mtimeMs >= before) continue;
|
|
10422
|
+
}
|
|
10423
|
+
await removeTree(path20.join(this.dir(root), id)).catch(() => {
|
|
10424
|
+
});
|
|
10425
|
+
n += 1;
|
|
10426
|
+
}
|
|
10427
|
+
}
|
|
10428
|
+
return n;
|
|
10429
|
+
}
|
|
10430
|
+
/** Delete everything in the trash for good; returns how many items went. */
|
|
10431
|
+
async empty() {
|
|
10432
|
+
const items = await this.list();
|
|
10433
|
+
for (const item of items) await this.remove(item.id).catch(() => {
|
|
10434
|
+
});
|
|
10435
|
+
return items.length;
|
|
10436
|
+
}
|
|
10437
|
+
};
|
|
10438
|
+
function view(meta) {
|
|
10439
|
+
return {
|
|
10440
|
+
id: meta.id,
|
|
10441
|
+
name: meta.name,
|
|
10442
|
+
originalPath: meta.originalPath,
|
|
10443
|
+
root: meta.root,
|
|
10444
|
+
type: meta.type,
|
|
10445
|
+
size: meta.size,
|
|
10446
|
+
trashedAt: meta.trashedAt
|
|
10447
|
+
};
|
|
10448
|
+
}
|
|
10449
|
+
async function writeJson(file, value) {
|
|
10450
|
+
const tmp = `${file}.${randomBytes9(4).toString("hex")}.tmp`;
|
|
10451
|
+
await fsp6.writeFile(tmp, JSON.stringify(value));
|
|
10452
|
+
await fsp6.rename(tmp, file);
|
|
10453
|
+
}
|
|
10454
|
+
|
|
10455
|
+
// ../hostcore/src/files/uploads.ts
|
|
10456
|
+
import { randomBytes as randomBytes10 } from "node:crypto";
|
|
10457
|
+
import { createWriteStream } from "node:fs";
|
|
10458
|
+
import fsp7 from "node:fs/promises";
|
|
10459
|
+
import path21 from "node:path";
|
|
10460
|
+
import { Transform as Transform2 } from "node:stream";
|
|
10461
|
+
import { pipeline } from "node:stream/promises";
|
|
10462
|
+
var MAX_CHUNK2 = 50 * 1024 * 1024;
|
|
10463
|
+
var UPLOAD_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
10464
|
+
var UPLOAD_ID = /^[0-9a-f]{32}$/;
|
|
10465
|
+
var Uploads = class {
|
|
10466
|
+
constructor(roots, trash, opts = {}) {
|
|
10467
|
+
this.roots = roots;
|
|
10468
|
+
this.trash = trash;
|
|
10469
|
+
this.opts = opts;
|
|
10470
|
+
}
|
|
10471
|
+
roots;
|
|
10472
|
+
trash;
|
|
10473
|
+
opts;
|
|
10474
|
+
queues = /* @__PURE__ */ new Map();
|
|
10475
|
+
dir(root) {
|
|
10476
|
+
return path21.join(this.roots.stateDir(root), "uploads");
|
|
10477
|
+
}
|
|
10478
|
+
/** Run `work` after any earlier work on the same upload has settled. */
|
|
10479
|
+
serial(id, work) {
|
|
10480
|
+
const prev = this.queues.get(id) ?? Promise.resolve();
|
|
10481
|
+
const next = prev.then(work, work);
|
|
10482
|
+
const tail = next.then(
|
|
10483
|
+
() => {
|
|
10484
|
+
},
|
|
10485
|
+
() => {
|
|
10486
|
+
}
|
|
10487
|
+
);
|
|
10488
|
+
this.queues.set(id, tail);
|
|
10489
|
+
void tail.then(() => {
|
|
10490
|
+
if (this.queues.get(id) === tail) this.queues.delete(id);
|
|
10491
|
+
});
|
|
10492
|
+
return next;
|
|
10493
|
+
}
|
|
10494
|
+
async start(body) {
|
|
10495
|
+
const size = body.size;
|
|
10496
|
+
if (typeof size !== "number" || !Number.isSafeInteger(size) || size < 0) {
|
|
10497
|
+
throw new FilesError(400, "size must be a byte count");
|
|
10498
|
+
}
|
|
10499
|
+
const overwrite = body.overwrite === true;
|
|
10500
|
+
const dest = await this.roots.creatable(body.path);
|
|
10501
|
+
this.roots.assertMutable(dest);
|
|
10502
|
+
await this.checkDestination(dest.fs, dest.abs, overwrite);
|
|
10503
|
+
const root = dest.root;
|
|
10504
|
+
const dir = this.dir(root);
|
|
10505
|
+
await fsp7.mkdir(dir, { recursive: true });
|
|
10506
|
+
const fsStat = await fsp7.statfs(dir);
|
|
10507
|
+
if (size > fsStat.bavail * fsStat.bsize) {
|
|
10508
|
+
throw new FilesError(507, "not enough space left on the volume for this upload", "no-space");
|
|
10509
|
+
}
|
|
10510
|
+
const id = randomBytes10(16).toString("hex");
|
|
10511
|
+
const now = Date.now();
|
|
10512
|
+
const rec = {
|
|
10513
|
+
v: 1,
|
|
10514
|
+
id,
|
|
10515
|
+
root: root.id,
|
|
10516
|
+
path: dest.abs,
|
|
10517
|
+
size,
|
|
10518
|
+
received: 0,
|
|
10519
|
+
overwrite,
|
|
10520
|
+
created: now,
|
|
10521
|
+
updated: now,
|
|
10522
|
+
done: false
|
|
10523
|
+
};
|
|
10524
|
+
await fsp7.writeFile(path21.join(dir, `${id}.part`), "");
|
|
10525
|
+
await this.save(rec);
|
|
10526
|
+
return view2(rec);
|
|
10527
|
+
}
|
|
10528
|
+
async checkDestination(fs26, abs, overwrite) {
|
|
10529
|
+
try {
|
|
10530
|
+
const st = await fsp7.lstat(fsPath(fs26));
|
|
10531
|
+
if (st.isDirectory()) throw new FilesError(409, `${abs} is a directory`, "is-a-directory");
|
|
10532
|
+
if (!overwrite) throw new FilesError(409, `${abs} already exists`, "exists");
|
|
10533
|
+
} catch (err) {
|
|
10534
|
+
if (err instanceof FilesError) throw err;
|
|
10535
|
+
if (err.code !== "ENOENT") throw fsError(err, abs);
|
|
10536
|
+
}
|
|
10537
|
+
}
|
|
10538
|
+
async save(rec) {
|
|
10539
|
+
const file = path21.join(this.dir(this.roots.byId(rec.root)), `${rec.id}.json`);
|
|
10540
|
+
const tmp = `${file}.${randomBytes10(4).toString("hex")}.tmp`;
|
|
10541
|
+
await fsp7.writeFile(tmp, JSON.stringify(rec));
|
|
10542
|
+
await fsp7.rename(tmp, file);
|
|
10543
|
+
}
|
|
10544
|
+
async load(id) {
|
|
10545
|
+
if (!UPLOAD_ID.test(id)) throw new FilesError(400, "invalid upload id");
|
|
10546
|
+
for (const root of this.roots.list()) {
|
|
10547
|
+
try {
|
|
10548
|
+
const rec = JSON.parse(await fsp7.readFile(path21.join(this.dir(root), `${id}.json`), "utf8"));
|
|
10549
|
+
if (rec.id === id) return rec;
|
|
10550
|
+
} catch {
|
|
10551
|
+
}
|
|
10552
|
+
}
|
|
10553
|
+
throw new FilesError(404, "no such upload", "not-found");
|
|
10554
|
+
}
|
|
10555
|
+
async get(id) {
|
|
10556
|
+
return view2(await this.load(id));
|
|
10557
|
+
}
|
|
10558
|
+
/**
|
|
10559
|
+
* Write one chunk at `offset`. `length` is the declared body length when the
|
|
10560
|
+
* request gave one, checked before a byte is read.
|
|
10561
|
+
*/
|
|
10562
|
+
put(id, offset, body, length) {
|
|
10563
|
+
return this.serial(id, async () => {
|
|
10564
|
+
const rec = await this.load(id);
|
|
10565
|
+
if (rec.done) throw new FilesError(409, "this upload has already finished", "finished");
|
|
10566
|
+
const at = Number(offset);
|
|
10567
|
+
if (!Number.isSafeInteger(at) || at < 0) throw new FilesError(400, "offset must be a byte offset");
|
|
10568
|
+
if (at > rec.received) {
|
|
10569
|
+
throw new FilesError(409, `expected a chunk at offset ${rec.received}`, `offset:${rec.received}`);
|
|
10570
|
+
}
|
|
10571
|
+
const max = this.opts.maxChunk ?? MAX_CHUNK2;
|
|
10572
|
+
if (length !== null && length > max) throw new FilesError(413, `a chunk may be at most ${max} bytes`, "too-large");
|
|
10573
|
+
if (length !== null && at + length > rec.size) {
|
|
10574
|
+
throw new FilesError(400, "the chunk runs past the declared size", "past-end");
|
|
10575
|
+
}
|
|
10576
|
+
let written = 0;
|
|
10577
|
+
const limit = new Transform2({
|
|
10578
|
+
transform(chunk, _enc, cb) {
|
|
10579
|
+
written += chunk.length;
|
|
10580
|
+
if (written > max) cb(new FilesError(413, `a chunk may be at most ${max} bytes`, "too-large"));
|
|
10581
|
+
else if (at + written > rec.size) cb(new FilesError(400, "the chunk runs past the declared size", "past-end"));
|
|
10582
|
+
else cb(null, chunk);
|
|
10583
|
+
}
|
|
10584
|
+
});
|
|
10585
|
+
const part = path21.join(this.dir(this.roots.byId(rec.root)), `${id}.part`);
|
|
10586
|
+
await pipeline(body, limit, createWriteStream(part, { flags: "r+", start: at }));
|
|
10587
|
+
rec.received = Math.max(rec.received, at + written);
|
|
10588
|
+
rec.updated = Date.now();
|
|
10589
|
+
await this.save(rec);
|
|
10590
|
+
return view2(rec);
|
|
10591
|
+
});
|
|
10592
|
+
}
|
|
10593
|
+
/** Move the finished file into place. Repeating it answers the same. */
|
|
10594
|
+
finish(id) {
|
|
10595
|
+
return this.serial(id, async () => {
|
|
10596
|
+
const rec = await this.load(id);
|
|
10597
|
+
if (rec.done && rec.entry) return rec.entry;
|
|
10598
|
+
if (rec.received !== rec.size) {
|
|
10599
|
+
throw new FilesError(409, `only ${rec.received} of ${rec.size} bytes have arrived`, `offset:${rec.received}`);
|
|
10600
|
+
}
|
|
10601
|
+
const root = this.roots.byId(rec.root);
|
|
10602
|
+
const part = path21.join(this.dir(root), `${id}.part`);
|
|
10603
|
+
const fh = await fsp7.open(part, "r+");
|
|
10604
|
+
try {
|
|
10605
|
+
await fh.truncate(rec.size);
|
|
10606
|
+
await fh.sync();
|
|
10607
|
+
} finally {
|
|
10608
|
+
await fh.close();
|
|
10609
|
+
}
|
|
10610
|
+
const dest = await this.roots.creatable(rec.path);
|
|
10611
|
+
this.roots.assertMutable(dest);
|
|
10612
|
+
await this.checkDestination(dest.fs, dest.abs, rec.overwrite);
|
|
10613
|
+
await this.roots.ensureParent(dest);
|
|
10614
|
+
const existing = await fsp7.lstat(fsPath(dest.fs)).catch(() => null);
|
|
10615
|
+
if (existing) {
|
|
10616
|
+
if (existing.isFile()) await fsp7.chmod(part, existing.mode & 4095);
|
|
10617
|
+
await this.trash.put(dest.root, dest.abs, dest.fs);
|
|
10618
|
+
}
|
|
10619
|
+
try {
|
|
10620
|
+
await noClobberRename(part, dest.fs);
|
|
10621
|
+
} catch (err) {
|
|
10622
|
+
throw fsError(err, dest.abs);
|
|
10623
|
+
}
|
|
10624
|
+
const entry = await describe(dest.abs, dest.fs, await this.roots.realRoot(dest.root));
|
|
10625
|
+
rec.done = true;
|
|
10626
|
+
rec.entry = entry;
|
|
10627
|
+
rec.updated = Date.now();
|
|
10628
|
+
await this.save(rec);
|
|
10629
|
+
return entry;
|
|
10630
|
+
});
|
|
10631
|
+
}
|
|
10632
|
+
/** Abandon an upload and its data. */
|
|
10633
|
+
cancel(id) {
|
|
10634
|
+
return this.serial(id, async () => {
|
|
10635
|
+
const rec = await this.load(id);
|
|
10636
|
+
const dir = this.dir(this.roots.byId(rec.root));
|
|
10637
|
+
await fsp7.rm(path21.join(dir, `${id}.part`), { force: true });
|
|
10638
|
+
await fsp7.rm(path21.join(dir, `${id}.json`), { force: true });
|
|
10639
|
+
});
|
|
10640
|
+
}
|
|
10641
|
+
/**
|
|
10642
|
+
* Remove uploads (finished or not) untouched for longer than the TTL, plus
|
|
10643
|
+
* any scratch file left behind by an interrupted copy or move.
|
|
10644
|
+
*/
|
|
10645
|
+
async sweep(now = Date.now()) {
|
|
10646
|
+
const ttl = this.opts.ttlMs ?? UPLOAD_TTL_MS;
|
|
10647
|
+
let removed = 0;
|
|
10648
|
+
for (const root of this.roots.list()) {
|
|
10649
|
+
const dir = this.dir(root);
|
|
10650
|
+
let names;
|
|
10651
|
+
try {
|
|
10652
|
+
names = await fsp7.readdir(dir);
|
|
10653
|
+
} catch {
|
|
10654
|
+
continue;
|
|
10655
|
+
}
|
|
10656
|
+
for (const name of names) {
|
|
10657
|
+
const file = path21.join(dir, name);
|
|
10658
|
+
const st = await fsp7.lstat(file).catch(() => null);
|
|
10659
|
+
if (!st || now - st.mtimeMs < ttl) continue;
|
|
10660
|
+
const m = /^([0-9a-f]{32})\.(json|part)$/.exec(name);
|
|
10661
|
+
if (m && name.endsWith(".json")) {
|
|
10662
|
+
const rec = await this.load(m[1]).catch(() => null);
|
|
10663
|
+
if (rec && now - rec.updated < ttl) continue;
|
|
10664
|
+
}
|
|
10665
|
+
await removeTree(file).catch(() => {
|
|
10666
|
+
});
|
|
10667
|
+
removed += 1;
|
|
10668
|
+
}
|
|
10669
|
+
}
|
|
10670
|
+
return removed;
|
|
10671
|
+
}
|
|
10672
|
+
/** Sweep now and then every hour, until the returned stop is called. */
|
|
10673
|
+
startSweeping(everyMs = 60 * 60 * 1e3) {
|
|
10674
|
+
void this.sweep().catch(() => {
|
|
10675
|
+
});
|
|
10676
|
+
const timer = setInterval(() => void this.sweep().catch(() => {
|
|
10677
|
+
}), everyMs);
|
|
10678
|
+
timer.unref?.();
|
|
10679
|
+
return () => clearInterval(timer);
|
|
10680
|
+
}
|
|
10681
|
+
};
|
|
10682
|
+
function view2(rec) {
|
|
10683
|
+
return {
|
|
10684
|
+
id: rec.id,
|
|
10685
|
+
path: rec.path,
|
|
10686
|
+
size: rec.size,
|
|
10687
|
+
received: rec.received,
|
|
10688
|
+
overwrite: rec.overwrite,
|
|
10689
|
+
created: rec.created,
|
|
10690
|
+
updated: rec.updated,
|
|
10691
|
+
done: rec.done
|
|
10692
|
+
};
|
|
10693
|
+
}
|
|
10694
|
+
|
|
10695
|
+
// ../hostcore/src/files/service.ts
|
|
10696
|
+
var FilesService = class {
|
|
10697
|
+
roots;
|
|
10698
|
+
git;
|
|
10699
|
+
trash;
|
|
10700
|
+
uploads;
|
|
10701
|
+
ops;
|
|
10702
|
+
fd;
|
|
10703
|
+
/** Sorted directory names, shared by the listing and WebDAV. */
|
|
10704
|
+
listings = new ListingCache();
|
|
10705
|
+
constructor(opts) {
|
|
10706
|
+
this.roots = new Roots({
|
|
10707
|
+
roots: opts.roots,
|
|
10708
|
+
...opts.caseInsensitive === void 0 ? {} : { caseInsensitive: opts.caseInsensitive },
|
|
10709
|
+
...opts.isHidden === void 0 ? {} : { isHidden: opts.isHidden },
|
|
10710
|
+
...opts.isWriteDenied === void 0 ? {} : { isWriteDenied: opts.isWriteDenied },
|
|
10711
|
+
...opts.refusePath === void 0 ? {} : { refusePath: opts.refusePath }
|
|
10712
|
+
});
|
|
10713
|
+
this.git = new GitStatusCache(opts.gitTtlMs === void 0 ? {} : { ttlMs: opts.gitTtlMs });
|
|
10714
|
+
this.trash = new Trash(this.roots);
|
|
10715
|
+
this.uploads = new Uploads(this.roots, this.trash, opts.maxChunk === void 0 ? {} : { maxChunk: opts.maxChunk });
|
|
10716
|
+
this.ops = new FileOps(this.roots, this.trash);
|
|
10717
|
+
this.fd = opts.fd;
|
|
10718
|
+
}
|
|
10719
|
+
/** Sweep abandoned uploads now and hourly; returns a stop. */
|
|
10720
|
+
startMaintenance() {
|
|
10721
|
+
return this.uploads.startSweeping();
|
|
10722
|
+
}
|
|
10723
|
+
};
|
|
10724
|
+
|
|
10725
|
+
// ../hostcore/src/files/http.ts
|
|
10726
|
+
import { createReadStream } from "node:fs";
|
|
10727
|
+
import fsp9 from "node:fs/promises";
|
|
10728
|
+
import path23 from "node:path";
|
|
10729
|
+
import { pipeline as pipeline2 } from "node:stream";
|
|
10730
|
+
|
|
10731
|
+
// ../hostcore/src/files/raw.ts
|
|
10732
|
+
var INLINE_TYPES = {
|
|
10733
|
+
".png": "image/png",
|
|
10734
|
+
".jpg": "image/jpeg",
|
|
10735
|
+
".jpeg": "image/jpeg",
|
|
10736
|
+
".gif": "image/gif",
|
|
10737
|
+
".webp": "image/webp",
|
|
10738
|
+
".avif": "image/avif",
|
|
10739
|
+
".bmp": "image/bmp",
|
|
10740
|
+
".ico": "image/x-icon",
|
|
10741
|
+
".pdf": "application/pdf"
|
|
10742
|
+
};
|
|
10743
|
+
var DOWNLOAD_TYPES = {
|
|
10744
|
+
...INLINE_TYPES,
|
|
10745
|
+
".html": "text/html",
|
|
10746
|
+
".htm": "text/html",
|
|
10747
|
+
".svg": "image/svg+xml",
|
|
10748
|
+
".json": "application/json",
|
|
10749
|
+
// Never a script or stylesheet type: with nosniff, a browser then refuses
|
|
10750
|
+
// to run the file as code even if it is asked to (see loadedAsCode).
|
|
10751
|
+
".js": "text/plain",
|
|
10752
|
+
".mjs": "text/plain",
|
|
10753
|
+
".css": "text/plain",
|
|
10754
|
+
".md": "text/markdown",
|
|
10755
|
+
".txt": "text/plain",
|
|
10756
|
+
".csv": "text/csv",
|
|
10757
|
+
".xml": "application/xml",
|
|
10758
|
+
".zip": "application/zip",
|
|
10759
|
+
".gz": "application/gzip",
|
|
10760
|
+
".tar": "application/x-tar",
|
|
10761
|
+
".mp4": "video/mp4",
|
|
10762
|
+
".webm": "video/webm",
|
|
10763
|
+
".mp3": "audio/mpeg",
|
|
10764
|
+
".wav": "audio/wav",
|
|
10765
|
+
".woff2": "font/woff2"
|
|
10766
|
+
};
|
|
10767
|
+
function etagOf(st) {
|
|
10768
|
+
return `"${st.size.toString(16)}-${Math.round(st.mtimeMs).toString(16)}"`;
|
|
10769
|
+
}
|
|
10770
|
+
function looksLikeText(head) {
|
|
10771
|
+
if (head.includes(0)) return false;
|
|
10772
|
+
for (let cut = 0; cut < 4 && cut <= head.length; cut++) {
|
|
10773
|
+
try {
|
|
10774
|
+
new TextDecoder("utf-8", { fatal: true }).decode(head.subarray(0, head.length - cut));
|
|
10775
|
+
return true;
|
|
10776
|
+
} catch {
|
|
10777
|
+
}
|
|
10778
|
+
}
|
|
10779
|
+
return false;
|
|
10780
|
+
}
|
|
10781
|
+
function disposition(kind, name) {
|
|
10782
|
+
const ascii = name.replace(/[^\x20-\x7e]|["\\%;]/g, "_") || "download";
|
|
10783
|
+
const exact = [...encodeName(name)].map((b) => /[A-Za-z0-9!#$&+.^_`|~-]/.test(String.fromCharCode(b)) ? String.fromCharCode(b) : `%${b.toString(16).toUpperCase().padStart(2, "0")}`).join("");
|
|
10784
|
+
return `${kind}; filename="${ascii}"; filename*=UTF-8''${exact}`;
|
|
10785
|
+
}
|
|
10786
|
+
function parseRange(header, size) {
|
|
10787
|
+
if (!header) return null;
|
|
10788
|
+
const m = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
|
|
10789
|
+
if (!m) return null;
|
|
10790
|
+
const [, a, b] = m;
|
|
10791
|
+
if (a === "" && b === "") return null;
|
|
10792
|
+
let start;
|
|
10793
|
+
let end;
|
|
10794
|
+
if (a === "") {
|
|
10795
|
+
const n = Number(b);
|
|
10796
|
+
if (n === 0) return "bad";
|
|
10797
|
+
start = Math.max(0, size - n);
|
|
10798
|
+
end = size - 1;
|
|
10799
|
+
} else {
|
|
10800
|
+
start = Number(a);
|
|
10801
|
+
end = b === "" ? size - 1 : Math.min(Number(b), size - 1);
|
|
10802
|
+
}
|
|
10803
|
+
if (start >= size || start > end) return "bad";
|
|
10804
|
+
return { start, end };
|
|
10805
|
+
}
|
|
10806
|
+
|
|
10807
|
+
// ../hostcore/src/files/search.ts
|
|
10808
|
+
import { spawn as spawn8 } from "node:child_process";
|
|
10809
|
+
import fs17 from "node:fs";
|
|
10810
|
+
import fsp8 from "node:fs/promises";
|
|
10811
|
+
import path22 from "node:path";
|
|
10812
|
+
var SKIP = /* @__PURE__ */ new Set([".git", "node_modules", ".agentbox"]);
|
|
10813
|
+
var fdCache;
|
|
10814
|
+
function findFd(env = process.env) {
|
|
10815
|
+
if (fdCache !== void 0) return fdCache;
|
|
10816
|
+
fdCache = null;
|
|
10817
|
+
for (const dir of (env.PATH ?? "").split(path22.delimiter)) {
|
|
10818
|
+
for (const name of ["fdfind", "fd"]) {
|
|
10819
|
+
const candidate = path22.join(dir, name);
|
|
10820
|
+
try {
|
|
10821
|
+
fs17.accessSync(candidate, fs17.constants.X_OK);
|
|
10822
|
+
fdCache = candidate;
|
|
10823
|
+
return fdCache;
|
|
10824
|
+
} catch {
|
|
10825
|
+
}
|
|
10826
|
+
}
|
|
10827
|
+
}
|
|
10828
|
+
return fdCache;
|
|
10829
|
+
}
|
|
10830
|
+
function score(rel, query) {
|
|
10831
|
+
const q = query.toLowerCase();
|
|
10832
|
+
const name = path22.basename(rel).toLowerCase();
|
|
10833
|
+
let s = name === q ? 0 : name.startsWith(q) ? 1 : name.includes(q) ? 2 : 3;
|
|
10834
|
+
s = s * 1e3 + rel.split("/").length * 10 + Math.min(rel.length, 9);
|
|
10835
|
+
return s;
|
|
10836
|
+
}
|
|
10837
|
+
async function searchNames(opts) {
|
|
10838
|
+
const query = opts.query.trim();
|
|
10839
|
+
if (!query) return [];
|
|
10840
|
+
const fd = opts.fd === void 0 ? findFd() : opts.fd;
|
|
10841
|
+
const gather = Math.min(opts.limit * 10, 2e3);
|
|
10842
|
+
const found = fd ? await runFd(fd, opts.dir, query, gather, opts.timeoutMs ?? 5e3) : await walk(opts.dir, query, gather);
|
|
10843
|
+
const scored = found.filter((p) => within2(p, opts.dir) && p !== opts.dir).map((p) => ({ p, s: score(path22.relative(opts.dir, p), query) })).sort((a, b) => a.s - b.s || a.p.localeCompare(b.p));
|
|
10844
|
+
return scored.slice(0, opts.limit).map((x) => x.p);
|
|
10845
|
+
}
|
|
10846
|
+
function runFd(bin, dir, query, max, timeoutMs) {
|
|
10847
|
+
const args = [
|
|
10848
|
+
"--fixed-strings",
|
|
10849
|
+
"--ignore-case",
|
|
10850
|
+
"--absolute-path",
|
|
10851
|
+
"--print0",
|
|
10852
|
+
"--color=never",
|
|
10853
|
+
"--max-results",
|
|
10854
|
+
String(max),
|
|
10855
|
+
...[...SKIP].flatMap((d) => ["--exclude", d])
|
|
10856
|
+
];
|
|
10857
|
+
if (query.includes("/")) args.push("--full-path");
|
|
10858
|
+
args.push("--", query, dir);
|
|
10859
|
+
return new Promise((resolve) => {
|
|
10860
|
+
const child = spawn8(bin, args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
10861
|
+
const chunks = [];
|
|
10862
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
|
|
10863
|
+
child.stdout.on("data", (c) => chunks.push(c));
|
|
10864
|
+
const done = () => {
|
|
10865
|
+
clearTimeout(timer);
|
|
10866
|
+
const out = Buffer.concat(chunks);
|
|
10867
|
+
const paths2 = [];
|
|
10868
|
+
let start = 0;
|
|
10869
|
+
for (let i = 0; i < out.length; i++) {
|
|
10870
|
+
if (out[i] === 0) {
|
|
10871
|
+
paths2.push(decodeName(out.subarray(start, i)).replace(/\/+$/, ""));
|
|
10872
|
+
start = i + 1;
|
|
10873
|
+
}
|
|
10874
|
+
}
|
|
10875
|
+
resolve(paths2);
|
|
10876
|
+
};
|
|
10877
|
+
child.on("error", done);
|
|
10878
|
+
child.on("close", done);
|
|
10879
|
+
});
|
|
10880
|
+
}
|
|
10881
|
+
async function walk(dir, query, max) {
|
|
10882
|
+
const q = query.toLowerCase();
|
|
10883
|
+
const full = q.includes("/");
|
|
10884
|
+
const out = [];
|
|
10885
|
+
const queue = [dir];
|
|
10886
|
+
let visited = 0;
|
|
10887
|
+
while (queue.length > 0 && out.length < max && visited < 2e4) {
|
|
10888
|
+
const d = queue.shift();
|
|
10889
|
+
visited += 1;
|
|
10890
|
+
let names;
|
|
10891
|
+
try {
|
|
10892
|
+
names = await fsp8.readdir(fsPath(d), { encoding: "buffer" });
|
|
10893
|
+
} catch {
|
|
10894
|
+
continue;
|
|
10895
|
+
}
|
|
10896
|
+
for (const raw2 of names) {
|
|
10897
|
+
const name = decodeName(raw2);
|
|
10898
|
+
if (name.startsWith(".") || SKIP.has(name)) continue;
|
|
10899
|
+
const p = path22.join(d, name);
|
|
10900
|
+
const hay = (full ? path22.relative(dir, p) : name).toLowerCase();
|
|
10901
|
+
if (hay.includes(q)) out.push(p);
|
|
10902
|
+
const st = await fsp8.lstat(fsPath(p)).catch(() => null);
|
|
10903
|
+
if (st?.isDirectory()) queue.push(p);
|
|
10904
|
+
}
|
|
10905
|
+
}
|
|
10906
|
+
return out;
|
|
10907
|
+
}
|
|
10908
|
+
|
|
10909
|
+
// ../hostcore/src/files/http.ts
|
|
10910
|
+
var MAX_JSON = 64 * 1024;
|
|
10911
|
+
var MAX_WRITE_JSON = 6 * 1024 * 1024;
|
|
10912
|
+
function send(res, status3, body) {
|
|
10913
|
+
const text = JSON.stringify(body);
|
|
10914
|
+
res.writeHead(status3, { "content-type": "application/json", "content-length": Buffer.byteLength(text), "cache-control": "no-store" });
|
|
10915
|
+
res.end(text);
|
|
10916
|
+
}
|
|
10917
|
+
function sendError(res, err) {
|
|
10918
|
+
const e = fsError(err, "path");
|
|
10919
|
+
if (!(e instanceof FilesError)) throw e;
|
|
10920
|
+
send(res, e.status, e.code ? { error: e.message, code: e.code } : { error: e.message });
|
|
10921
|
+
}
|
|
10922
|
+
function readBody(req, max) {
|
|
10923
|
+
return new Promise((resolve, reject) => {
|
|
10924
|
+
const chunks = [];
|
|
10925
|
+
let size = 0;
|
|
10926
|
+
req.on("data", (c) => {
|
|
10927
|
+
size += c.length;
|
|
10928
|
+
if (size > max) {
|
|
10929
|
+
reject(new FilesError(413, "request body too large"));
|
|
10930
|
+
req.destroy();
|
|
10931
|
+
return;
|
|
10932
|
+
}
|
|
10933
|
+
chunks.push(c);
|
|
10934
|
+
});
|
|
10935
|
+
req.on("end", () => {
|
|
10936
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
10937
|
+
if (text.trim() === "") return resolve({});
|
|
10938
|
+
try {
|
|
10939
|
+
const v = JSON.parse(text);
|
|
10940
|
+
if (typeof v === "object" && v !== null && !Array.isArray(v)) return resolve(v);
|
|
10941
|
+
} catch {
|
|
10942
|
+
}
|
|
10943
|
+
reject(new FilesError(400, "the body must be a JSON object"));
|
|
10944
|
+
});
|
|
10945
|
+
req.on("error", reject);
|
|
10946
|
+
});
|
|
10947
|
+
}
|
|
10948
|
+
function one(q, key) {
|
|
10949
|
+
return q.get(key)?.[0];
|
|
10950
|
+
}
|
|
10951
|
+
function flag(q, key) {
|
|
10952
|
+
const v = one(q, key);
|
|
10953
|
+
return v !== void 0 && v !== "0" && v !== "false";
|
|
10954
|
+
}
|
|
10955
|
+
function int2(q, key, fallback, min, max) {
|
|
10956
|
+
const v = Number(one(q, key));
|
|
10957
|
+
return Number.isFinite(v) ? Math.min(max, Math.max(min, Math.trunc(v))) : fallback;
|
|
10958
|
+
}
|
|
10959
|
+
var CODE = /* @__PURE__ */ new Set(["script", "worker", "sharedworker", "serviceworker", "style", "audioworklet", "paintworklet", "xslt"]);
|
|
10960
|
+
async function isText(file, size) {
|
|
10961
|
+
if (size === 0) return true;
|
|
10962
|
+
const fh = await fsp9.open(fsPath(file), "r");
|
|
10963
|
+
try {
|
|
10964
|
+
const buf = Buffer.alloc(Math.min(8192, size));
|
|
10965
|
+
const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
|
|
10966
|
+
return looksLikeText(buf.subarray(0, bytesRead));
|
|
10967
|
+
} finally {
|
|
10968
|
+
await fh.close();
|
|
10969
|
+
}
|
|
10970
|
+
}
|
|
10971
|
+
async function raw(files, req, res, q) {
|
|
10972
|
+
const dest = req.headers["sec-fetch-dest"];
|
|
10973
|
+
if (typeof dest === "string" && CODE.has(dest.toLowerCase())) throw new FilesError(403, "a file cannot be loaded as a script or stylesheet", "not-code");
|
|
10974
|
+
const ref = await files.roots.target(one(q, "path"));
|
|
10975
|
+
const st = await fsp9.stat(fsPath(ref.real));
|
|
10976
|
+
if (!st.isFile()) throw new FilesError(409, `${ref.abs} is not a file`, "not-a-file");
|
|
10977
|
+
const name = path23.basename(ref.abs);
|
|
10978
|
+
const inline = flag(q, "inline") && await isText(ref.real, st.size);
|
|
10979
|
+
const headers = {
|
|
10980
|
+
"content-type": inline ? "text/plain; charset=utf-8" : "application/octet-stream",
|
|
10981
|
+
"content-disposition": disposition(inline ? "inline" : "attachment", name),
|
|
10982
|
+
"cache-control": "private, no-cache",
|
|
10983
|
+
"last-modified": st.mtime.toUTCString(),
|
|
10984
|
+
etag: etagOf(st)
|
|
10985
|
+
};
|
|
10986
|
+
const range = parseRange(req.headers.range, st.size);
|
|
10987
|
+
if (range === "bad") {
|
|
10988
|
+
res.writeHead(416, { ...headers, "content-range": `bytes */${st.size}`, "content-length": "0" });
|
|
10989
|
+
res.end();
|
|
10990
|
+
return;
|
|
10991
|
+
}
|
|
10992
|
+
const start = range ? range.start : 0;
|
|
10993
|
+
const end = range ? range.end : st.size - 1;
|
|
10994
|
+
const length = st.size === 0 ? 0 : end - start + 1;
|
|
10995
|
+
if (range) headers["content-range"] = `bytes ${start}-${end}/${st.size}`;
|
|
10996
|
+
headers["content-length"] = String(length);
|
|
10997
|
+
res.writeHead(range ? 206 : 200, headers);
|
|
10998
|
+
if (req.method === "HEAD" || length === 0) {
|
|
10999
|
+
res.end();
|
|
11000
|
+
return;
|
|
11001
|
+
}
|
|
11002
|
+
pipeline2(createReadStream(fsPath(ref.real), { start, end }), res, () => {
|
|
11003
|
+
});
|
|
11004
|
+
}
|
|
11005
|
+
async function search(files, q) {
|
|
11006
|
+
const { roots } = files;
|
|
11007
|
+
const dir = await roots.target(one(q, "path") ?? "");
|
|
11008
|
+
const rootReal = await roots.realRoot(dir.root);
|
|
11009
|
+
const found = await searchNames({
|
|
11010
|
+
dir: dir.real,
|
|
11011
|
+
query: one(q, "q") ?? "",
|
|
11012
|
+
limit: int2(q, "limit", 50, 1, 200),
|
|
11013
|
+
...files.fd === void 0 ? {} : { fd: files.fd }
|
|
11014
|
+
});
|
|
11015
|
+
const out = [];
|
|
11016
|
+
for (const real of found) {
|
|
11017
|
+
if (!roots.inside(real, rootReal) || roots.hidden(real)) continue;
|
|
11018
|
+
try {
|
|
11019
|
+
out.push(await describe(roots.present(dir.root, rootReal, real), real, rootReal));
|
|
11020
|
+
} catch {
|
|
11021
|
+
}
|
|
11022
|
+
}
|
|
11023
|
+
return out;
|
|
11024
|
+
}
|
|
11025
|
+
var TRASH_ITEM = /^\/api\/files\/trash\/([0-9a-f]{24})(\/restore)?$/;
|
|
11026
|
+
var UPLOAD = /^\/api\/files\/uploads\/([0-9a-f]{32})(\/finish)?$/;
|
|
11027
|
+
async function serveFiles(files, req, res) {
|
|
11028
|
+
const url = req.url ?? "/";
|
|
11029
|
+
const pathname = url.split("?", 1)[0] ?? "/";
|
|
11030
|
+
if (pathname !== "/api/files" && !pathname.startsWith("/api/files/")) return false;
|
|
11031
|
+
const method = req.method ?? "GET";
|
|
11032
|
+
const q = parseQuery(url);
|
|
11033
|
+
const { roots, ops, trash, uploads } = files;
|
|
11034
|
+
try {
|
|
11035
|
+
const route = `${method} ${pathname}`;
|
|
11036
|
+
switch (route) {
|
|
11037
|
+
case "GET /api/files/list": {
|
|
11038
|
+
const dir = await roots.target(one(q, "path") ?? "");
|
|
11039
|
+
const st = await fsp9.stat(fsPath(dir.real));
|
|
11040
|
+
if (!st.isDirectory()) throw new FilesError(409, `${dir.abs} is not a directory`, "not-a-directory");
|
|
11041
|
+
send(
|
|
11042
|
+
res,
|
|
11043
|
+
200,
|
|
11044
|
+
await listDirectory(roots, dir, {
|
|
11045
|
+
hidden: flag(q, "hidden"),
|
|
11046
|
+
offset: int2(q, "offset", 0, 0, Number.MAX_SAFE_INTEGER),
|
|
11047
|
+
limit: int2(q, "limit", MAX_PAGE, 1, MAX_PAGE),
|
|
11048
|
+
cache: files.listings
|
|
11049
|
+
})
|
|
11050
|
+
);
|
|
11051
|
+
return true;
|
|
11052
|
+
}
|
|
11053
|
+
case "GET /api/files/stat": {
|
|
11054
|
+
const ref = await roots.entry(one(q, "path") ?? "");
|
|
11055
|
+
send(res, 200, await describe(ref.abs, ref.fs, await roots.realRoot(ref.root)));
|
|
11056
|
+
return true;
|
|
11057
|
+
}
|
|
11058
|
+
case "GET /api/files/raw":
|
|
11059
|
+
case "HEAD /api/files/raw":
|
|
11060
|
+
await raw(files, req, res, q);
|
|
11061
|
+
return true;
|
|
11062
|
+
case "GET /api/files/zip":
|
|
11063
|
+
send(res, 501, { error: "folders cannot be downloaded as a zip from a machine yet; download its files one by one", code: "unsupported" });
|
|
11064
|
+
return true;
|
|
11065
|
+
case "GET /api/files/search":
|
|
11066
|
+
send(res, 200, await search(files, q));
|
|
11067
|
+
return true;
|
|
11068
|
+
case "POST /api/files/write":
|
|
11069
|
+
send(res, 200, await ops.write(await readBody(req, MAX_WRITE_JSON)));
|
|
11070
|
+
return true;
|
|
11071
|
+
case "POST /api/files/mkdir":
|
|
11072
|
+
send(res, 200, await ops.mkdir(await readBody(req, MAX_JSON)));
|
|
11073
|
+
return true;
|
|
11074
|
+
case "POST /api/files/move":
|
|
11075
|
+
send(res, 200, await ops.move(await readBody(req, MAX_JSON)));
|
|
11076
|
+
return true;
|
|
11077
|
+
case "POST /api/files/copy":
|
|
11078
|
+
send(res, 200, await ops.copy(await readBody(req, MAX_JSON)));
|
|
11079
|
+
return true;
|
|
11080
|
+
case "POST /api/files/trash":
|
|
11081
|
+
send(res, 200, await trash.trash((await readBody(req, MAX_JSON)).paths));
|
|
11082
|
+
return true;
|
|
11083
|
+
case "GET /api/files/trash":
|
|
11084
|
+
send(res, 200, await trash.list());
|
|
11085
|
+
return true;
|
|
11086
|
+
case "DELETE /api/files/trash":
|
|
11087
|
+
send(res, 200, { removed: await trash.empty() });
|
|
11088
|
+
return true;
|
|
11089
|
+
case "POST /api/files/uploads": {
|
|
11090
|
+
const session = await uploads.start(await readBody(req, MAX_JSON));
|
|
11091
|
+
send(res, 200, { uploadId: session.id, ...session });
|
|
11092
|
+
return true;
|
|
11093
|
+
}
|
|
11094
|
+
}
|
|
11095
|
+
const t2 = TRASH_ITEM.exec(pathname);
|
|
11096
|
+
if (t2) {
|
|
11097
|
+
const id = t2[1];
|
|
11098
|
+
if (t2[2] && method === "POST") {
|
|
11099
|
+
send(res, 200, await trash.restore(id, (await readBody(req, MAX_JSON)).to));
|
|
11100
|
+
return true;
|
|
11101
|
+
}
|
|
11102
|
+
if (!t2[2] && method === "DELETE") {
|
|
11103
|
+
await trash.remove(id);
|
|
11104
|
+
res.writeHead(204, { "cache-control": "no-store" });
|
|
11105
|
+
res.end();
|
|
11106
|
+
return true;
|
|
11107
|
+
}
|
|
11108
|
+
}
|
|
11109
|
+
const u = UPLOAD.exec(pathname);
|
|
11110
|
+
if (u) {
|
|
11111
|
+
const id = u[1];
|
|
11112
|
+
if (u[2] && method === "POST") {
|
|
11113
|
+
send(res, 200, await uploads.finish(id));
|
|
11114
|
+
return true;
|
|
11115
|
+
}
|
|
11116
|
+
if (!u[2]) {
|
|
11117
|
+
if (method === "GET") {
|
|
11118
|
+
send(res, 200, await uploads.get(id));
|
|
11119
|
+
return true;
|
|
11120
|
+
}
|
|
11121
|
+
if (method === "DELETE") {
|
|
11122
|
+
await uploads.cancel(id);
|
|
11123
|
+
res.writeHead(204, { "cache-control": "no-store" });
|
|
11124
|
+
res.end();
|
|
11125
|
+
return true;
|
|
11126
|
+
}
|
|
11127
|
+
if (method === "PUT") {
|
|
11128
|
+
const declared = req.headers["content-length"];
|
|
11129
|
+
const length = declared === void 0 ? null : Number(declared);
|
|
11130
|
+
const done = await uploads.put(id, one(q, "offset"), req, Number.isFinite(length) ? length : null);
|
|
11131
|
+
if (!req.readableEnded) res.setHeader("connection", "close");
|
|
11132
|
+
send(res, 200, done);
|
|
11133
|
+
return true;
|
|
11134
|
+
}
|
|
11135
|
+
}
|
|
11136
|
+
}
|
|
11137
|
+
send(res, 404, { error: "not found" });
|
|
11138
|
+
} catch (err) {
|
|
11139
|
+
if (!req.readableEnded) res.setHeader("connection", "close");
|
|
11140
|
+
if (res.headersSent) res.destroy();
|
|
11141
|
+
else sendError(res, err);
|
|
11142
|
+
}
|
|
11143
|
+
return true;
|
|
11144
|
+
}
|
|
11145
|
+
|
|
11146
|
+
// src/node/preview.ts
|
|
11147
|
+
import { EventEmitter as EventEmitter3 } from "node:events";
|
|
11148
|
+
import net5 from "node:net";
|
|
11149
|
+
import path25 from "node:path";
|
|
11150
|
+
|
|
11151
|
+
// src/node/service-linux.ts
|
|
11152
|
+
import { execFile } from "node:child_process";
|
|
11153
|
+
import fs18 from "node:fs";
|
|
11154
|
+
import os9 from "node:os";
|
|
11155
|
+
import path24 from "node:path";
|
|
11156
|
+
var UNIT_NAME = "agentbox-node.service";
|
|
11157
|
+
var runCommand = (cmd, args) => new Promise((resolve) => {
|
|
11158
|
+
execFile(cmd, args, { timeout: 3e4 }, (err, stdout, stderr) => {
|
|
11159
|
+
const code2 = err ? typeof err.code === "number" ? err.code : 127 : 0;
|
|
11160
|
+
resolve({ code: code2, stdout: String(stdout), stderr: String(stderr) });
|
|
11161
|
+
});
|
|
11162
|
+
});
|
|
11163
|
+
function unitDir(env = process.env, home2 = os9.homedir()) {
|
|
11164
|
+
const xdg = env.XDG_CONFIG_HOME;
|
|
11165
|
+
return path24.join(xdg && path24.isAbsolute(xdg) ? xdg : path24.join(home2, ".config"), "systemd", "user");
|
|
11166
|
+
}
|
|
11167
|
+
function execStart(execPath = process.execPath, script = process.argv[1]) {
|
|
11168
|
+
const isSea = script === void 0 || path24.resolve(script) === path24.resolve(execPath);
|
|
11169
|
+
return isSea ? [execPath, "node", "run"] : [execPath, path24.resolve(script), "node", "run"];
|
|
11170
|
+
}
|
|
11171
|
+
function quote(arg) {
|
|
11172
|
+
return `"${arg.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%")}"`;
|
|
11173
|
+
}
|
|
11174
|
+
function unitText(command2) {
|
|
11175
|
+
return [
|
|
11176
|
+
"[Unit]",
|
|
11177
|
+
"Description=agentbox node: this machine's link to its box",
|
|
11178
|
+
"After=network-online.target",
|
|
11179
|
+
"Wants=network-online.target",
|
|
11180
|
+
"",
|
|
11181
|
+
"[Service]",
|
|
11182
|
+
"Type=simple",
|
|
11183
|
+
`ExecStart=${command2.map(quote).join(" ")}`,
|
|
11184
|
+
"Restart=on-failure",
|
|
11185
|
+
"RestartSec=5",
|
|
11186
|
+
// herdr, and the agents in it, outlive a restart of the node (an
|
|
11187
|
+
// update, a crash); the next node adopts it.
|
|
11188
|
+
"KillMode=process",
|
|
11189
|
+
"",
|
|
11190
|
+
"[Install]",
|
|
11191
|
+
"WantedBy=default.target",
|
|
11192
|
+
""
|
|
11193
|
+
].join("\n");
|
|
11194
|
+
}
|
|
11195
|
+
async function installService(opts) {
|
|
11196
|
+
const run4 = opts.run ?? runCommand;
|
|
11197
|
+
const dir = unitDir(opts.env);
|
|
11198
|
+
fs18.mkdirSync(dir, { recursive: true });
|
|
11199
|
+
const unitFile = path24.join(dir, UNIT_NAME);
|
|
11200
|
+
fs18.writeFileSync(unitFile, unitText(opts.command), { mode: 420 });
|
|
11201
|
+
for (const args of [["--user", "daemon-reload"], ["--user", "enable", UNIT_NAME], ["--user", "restart", UNIT_NAME]]) {
|
|
11202
|
+
const r = await run4("systemctl", args);
|
|
11203
|
+
if (r.code !== 0) {
|
|
11204
|
+
throw new Error(`systemctl ${args.join(" ")} failed: ${(r.stderr || r.stdout).trim() || `exit ${r.code}`}`);
|
|
11205
|
+
}
|
|
11206
|
+
}
|
|
11207
|
+
const user = opts.user ?? os9.userInfo().username;
|
|
11208
|
+
const linger = await run4("loginctl", ["enable-linger", user]);
|
|
11209
|
+
if (linger.code === 0) return { unitFile, lingering: true, note: null };
|
|
11210
|
+
const cron = `@reboot ${opts.command.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ")}`;
|
|
11211
|
+
return {
|
|
11212
|
+
unitFile,
|
|
11213
|
+
lingering: false,
|
|
11214
|
+
note: `This machine does not let the node keep running after you log out (loginctl enable-linger was refused), so it runs while you are logged in. To start it at boot instead, add this line with \`crontab -e\`:
|
|
11215
|
+
${cron}`
|
|
11216
|
+
};
|
|
11217
|
+
}
|
|
11218
|
+
async function removeService(opts = {}) {
|
|
11219
|
+
const run4 = opts.run ?? runCommand;
|
|
11220
|
+
const unitFile = path24.join(unitDir(opts.env), UNIT_NAME);
|
|
11221
|
+
if (!fs18.existsSync(unitFile)) return false;
|
|
11222
|
+
await run4("systemctl", ["--user", "disable", "--now", UNIT_NAME]);
|
|
11223
|
+
fs18.rmSync(unitFile, { force: true });
|
|
11224
|
+
await run4("systemctl", ["--user", "daemon-reload"]);
|
|
11225
|
+
return true;
|
|
11226
|
+
}
|
|
11227
|
+
function serviceInstalled(env = process.env) {
|
|
11228
|
+
return fs18.existsSync(path24.join(unitDir(env), UNIT_NAME));
|
|
11229
|
+
}
|
|
11230
|
+
|
|
11231
|
+
// src/node/preview.ts
|
|
11232
|
+
var MAX_APPS = 64;
|
|
11233
|
+
var CLOSED_GRACE_MS = 6e4;
|
|
11234
|
+
var CHECK_MS2 = 5e3;
|
|
11235
|
+
function connectLocal(port, timeoutMs = 5e3) {
|
|
11236
|
+
const attempt2 = (host) => new Promise((resolve, reject) => {
|
|
11237
|
+
const s = net5.connect({ host, port });
|
|
11238
|
+
const timer = setTimeout(() => s.destroy(new Error("timed out")), timeoutMs);
|
|
11239
|
+
s.once("connect", () => {
|
|
11240
|
+
clearTimeout(timer);
|
|
11241
|
+
s.off("error", reject);
|
|
11242
|
+
resolve(s);
|
|
11243
|
+
});
|
|
11244
|
+
s.once("error", (err) => {
|
|
11245
|
+
clearTimeout(timer);
|
|
11246
|
+
reject(err);
|
|
11247
|
+
});
|
|
11248
|
+
});
|
|
11249
|
+
return attempt2("127.0.0.1").catch(() => attempt2("::1"));
|
|
11250
|
+
}
|
|
11251
|
+
async function answers(port) {
|
|
11252
|
+
try {
|
|
11253
|
+
const s = await connectLocal(port, 2e3);
|
|
11254
|
+
s.destroy();
|
|
11255
|
+
return true;
|
|
11256
|
+
} catch {
|
|
11257
|
+
return false;
|
|
11258
|
+
}
|
|
11259
|
+
}
|
|
11260
|
+
function cleanName(raw2, port) {
|
|
11261
|
+
const s = typeof raw2 === "string" ? raw2.replace(/[^\w .()+@-]/g, "").trim().slice(0, 64) : "";
|
|
11262
|
+
return s || `port ${port}`;
|
|
11263
|
+
}
|
|
11264
|
+
var Previews = class extends EventEmitter3 {
|
|
11265
|
+
constructor(o = {}) {
|
|
11266
|
+
super();
|
|
11267
|
+
this.o = o;
|
|
11268
|
+
}
|
|
11269
|
+
o;
|
|
11270
|
+
apps = /* @__PURE__ */ new Map();
|
|
11271
|
+
timer = null;
|
|
11272
|
+
get now() {
|
|
11273
|
+
return (this.o.now ?? Date.now)();
|
|
11274
|
+
}
|
|
11275
|
+
list() {
|
|
11276
|
+
return [...this.apps.values()].map(({ port, name, registeredAt }) => ({ port, name, registeredAt }));
|
|
11277
|
+
}
|
|
11278
|
+
has(port) {
|
|
11279
|
+
return this.apps.has(port);
|
|
11280
|
+
}
|
|
11281
|
+
/** Register `port` (it must answer now). */
|
|
11282
|
+
async register(rawPort, rawName) {
|
|
11283
|
+
const port = typeof rawPort === "number" ? rawPort : Number(rawPort);
|
|
11284
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return { ok: false, error: "a port is a number from 1 to 65535" };
|
|
11285
|
+
const existing = this.apps.get(port);
|
|
11286
|
+
if (!existing && this.apps.size >= MAX_APPS) return { ok: false, error: `this machine has ${MAX_APPS} apps registered already; stop one first` };
|
|
11287
|
+
if (!await (this.o.probe ?? answers)(port)) return { ok: false, error: `nothing answers on port ${port} (on 127.0.0.1 or [::1]); start the server first` };
|
|
11288
|
+
const name = rawName === void 0 && existing ? existing.name : cleanName(rawName, port);
|
|
11289
|
+
const app = { port, name, registeredAt: existing?.registeredAt ?? this.now, closedSince: null };
|
|
11290
|
+
this.apps.set(port, app);
|
|
11291
|
+
if (!existing || existing.name !== name) this.emit("change");
|
|
11292
|
+
return { ok: true, app: { port, name, registeredAt: app.registeredAt }, existed: existing !== void 0 };
|
|
11293
|
+
}
|
|
11294
|
+
remove(port) {
|
|
11295
|
+
const had = this.apps.delete(port);
|
|
11296
|
+
if (had) this.emit("change");
|
|
11297
|
+
return had;
|
|
11298
|
+
}
|
|
11299
|
+
clear() {
|
|
11300
|
+
if (this.apps.size === 0) return;
|
|
11301
|
+
this.apps.clear();
|
|
11302
|
+
this.emit("change");
|
|
11303
|
+
}
|
|
11304
|
+
/** Probe every port; drop those closed for the grace period. */
|
|
11305
|
+
async check() {
|
|
11306
|
+
let changed = false;
|
|
11307
|
+
for (const app of [...this.apps.values()]) {
|
|
11308
|
+
const up = await (this.o.probe ?? answers)(app.port);
|
|
11309
|
+
if (this.apps.get(app.port) !== app) continue;
|
|
11310
|
+
if (up) app.closedSince = null;
|
|
11311
|
+
else if (app.closedSince === null) app.closedSince = this.now;
|
|
11312
|
+
else if (this.now - app.closedSince >= (this.o.graceMs ?? CLOSED_GRACE_MS)) {
|
|
11313
|
+
this.apps.delete(app.port);
|
|
11314
|
+
changed = true;
|
|
11315
|
+
}
|
|
11316
|
+
}
|
|
11317
|
+
if (changed) this.emit("change");
|
|
11318
|
+
}
|
|
11319
|
+
start(everyMs = CHECK_MS2) {
|
|
11320
|
+
if (this.timer) return;
|
|
11321
|
+
let busy = false;
|
|
11322
|
+
this.timer = setInterval(() => {
|
|
11323
|
+
if (busy) return;
|
|
11324
|
+
busy = true;
|
|
11325
|
+
void this.check().finally(() => busy = false);
|
|
11326
|
+
}, everyMs);
|
|
11327
|
+
this.timer.unref();
|
|
11328
|
+
}
|
|
11329
|
+
stop() {
|
|
11330
|
+
if (this.timer) clearInterval(this.timer);
|
|
11331
|
+
this.timer = null;
|
|
11332
|
+
}
|
|
11333
|
+
};
|
|
11334
|
+
function shQuote2(s) {
|
|
11335
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
11336
|
+
}
|
|
11337
|
+
function writePreviewShim(shims, command2 = execStart().slice(0, -2)) {
|
|
11338
|
+
const file = path25.join(shims, "agentbox-preview");
|
|
11339
|
+
writePrivate2(
|
|
11340
|
+
file,
|
|
11341
|
+
`#!/bin/sh
|
|
11342
|
+
# agentbox: show an app on this machine in the owner's Preview. Written by the node.
|
|
11343
|
+
exec ${command2.map(shQuote2).join(" ")} node preview "$@"
|
|
11344
|
+
`,
|
|
11345
|
+
448
|
|
11346
|
+
);
|
|
11347
|
+
return file;
|
|
11348
|
+
}
|
|
11349
|
+
|
|
11350
|
+
// src/node/server.ts
|
|
11351
|
+
var HOP = /* @__PURE__ */ new Set(["connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "te", "trailer"]);
|
|
11352
|
+
var MAX_BODY = 64 * 1024;
|
|
11353
|
+
function json(res, status3, body) {
|
|
11354
|
+
const text = JSON.stringify(body);
|
|
11355
|
+
res.writeHead(status3, { "content-type": "application/json", "content-length": Buffer.byteLength(text), "cache-control": "no-store" });
|
|
11356
|
+
res.end(text);
|
|
11357
|
+
}
|
|
11358
|
+
function readJson(req) {
|
|
11359
|
+
return new Promise((resolve) => {
|
|
11360
|
+
const chunks = [];
|
|
11361
|
+
let size = 0;
|
|
11362
|
+
req.on("data", (c) => {
|
|
11363
|
+
size += c.length;
|
|
11364
|
+
if (size > MAX_BODY) {
|
|
11365
|
+
resolve(null);
|
|
11366
|
+
req.destroy();
|
|
11367
|
+
return;
|
|
11368
|
+
}
|
|
11369
|
+
chunks.push(c);
|
|
11370
|
+
});
|
|
11371
|
+
req.on("end", () => {
|
|
11372
|
+
try {
|
|
11373
|
+
const v = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
11374
|
+
resolve(typeof v === "object" && v !== null && !Array.isArray(v) ? v : null);
|
|
11375
|
+
} catch {
|
|
11376
|
+
resolve(null);
|
|
11377
|
+
}
|
|
11378
|
+
});
|
|
11379
|
+
req.on("error", () => resolve(null));
|
|
11380
|
+
});
|
|
11381
|
+
}
|
|
11382
|
+
function refuseSocket(socket2, status3, body) {
|
|
11383
|
+
const text = JSON.stringify(body);
|
|
11384
|
+
socket2.end(
|
|
11385
|
+
`HTTP/1.1 ${status3} ${http.STATUS_CODES[status3] ?? ""}\r
|
|
11386
|
+
Connection: close\r
|
|
11387
|
+
Content-Type: application/json\r
|
|
11388
|
+
Content-Length: ${Buffer.byteLength(text)}\r
|
|
11389
|
+
\r
|
|
11390
|
+
${text}`
|
|
11391
|
+
);
|
|
11392
|
+
}
|
|
11393
|
+
var NodeServer = class {
|
|
11394
|
+
constructor(o) {
|
|
11395
|
+
this.o = o;
|
|
11396
|
+
this.server = http.createServer({ requestTimeout: 0, headersTimeout: 3e4, keepAliveTimeout: 6e4 }, (req, res) => {
|
|
11397
|
+
void this.handle(req, res).catch((err) => {
|
|
11398
|
+
o.log?.(`server: ${err.message}`);
|
|
11399
|
+
if (!res.headersSent) json(res, 500, { error: "internal" });
|
|
11400
|
+
else res.destroy();
|
|
11401
|
+
});
|
|
11402
|
+
});
|
|
11403
|
+
this.server.on("upgrade", (req, socket2, head) => this.upgrade(req, socket2, head));
|
|
11404
|
+
this.server.on("clientError", (_err, socket2) => socket2.destroy());
|
|
11405
|
+
this.appServer = http.createServer({ requestTimeout: 0, headersTimeout: 3e4, keepAliveTimeout: 6e4 }, (req, res) => this.app(req, res));
|
|
11406
|
+
this.appServer.on("upgrade", (req, socket2, head) => this.appUpgrade(req, socket2, head));
|
|
11407
|
+
this.appServer.on("clientError", (_err, socket2) => socket2.destroy());
|
|
11408
|
+
}
|
|
11409
|
+
o;
|
|
11410
|
+
server;
|
|
11411
|
+
/** Streams for an app: every request on one goes to its registered port. */
|
|
11412
|
+
appServer;
|
|
11413
|
+
wss = new import_websocket_server.default({ noServer: true, maxPayload: 1024 * 1024 });
|
|
11414
|
+
opens = /* @__PURE__ */ new WeakMap();
|
|
11415
|
+
channels = /* @__PURE__ */ new Set();
|
|
11416
|
+
/** A stream from the link: a connection to this server (or, for an app, to the app's). */
|
|
11417
|
+
accept(stream, open3) {
|
|
11418
|
+
this.opens.set(stream, open3);
|
|
11419
|
+
(open3.app === void 0 ? this.server : this.appServer).emit("connection", stream);
|
|
11420
|
+
}
|
|
11421
|
+
openOf(req) {
|
|
11422
|
+
return this.opens.get(req.socket) ?? { kind: "http", session: null, grant: null };
|
|
11423
|
+
}
|
|
11424
|
+
/** Why `need` is not allowed for this stream now, or `null`. */
|
|
11425
|
+
refusal(need, open3) {
|
|
11426
|
+
const l = this.o.policy();
|
|
11427
|
+
const denied = (error, what) => ({
|
|
11428
|
+
status: 403,
|
|
11429
|
+
body: { error, message: l.problem ? `this machine's policy denies everything: ${l.problem}` : what }
|
|
11430
|
+
});
|
|
11431
|
+
if (need === "terminals" && !l.policy.terminals) return denied("terminals_off", "terminals are off in this machine's policy.json");
|
|
11432
|
+
if (need === "files" && l.policy.files.length === 0) return denied("files_off", "this machine's policy.json allows no folders");
|
|
11433
|
+
if (need === "ports") {
|
|
11434
|
+
if (!l.policy.ports) return denied("ports_off", "apps are off in this machine's policy.json (ports)");
|
|
11435
|
+
return null;
|
|
11436
|
+
}
|
|
11437
|
+
if (!this.o.presence.check(open3.grant, open3.session)) {
|
|
11438
|
+
return { status: 401, body: { error: "presence", message: "this machine wants proof that you are there: the machines passphrase" } };
|
|
11439
|
+
}
|
|
11440
|
+
return null;
|
|
11441
|
+
}
|
|
11442
|
+
/** Close every channel the policy or presence no longer allows. Returns how many. */
|
|
11443
|
+
enforce() {
|
|
11444
|
+
let n = 0;
|
|
11445
|
+
for (const ch of [...this.channels]) {
|
|
11446
|
+
if (this.refusal(ch.need, ch.open) === null && (ch.still?.() ?? true)) continue;
|
|
11447
|
+
this.channels.delete(ch);
|
|
11448
|
+
ch.close();
|
|
11449
|
+
n += 1;
|
|
11450
|
+
}
|
|
11451
|
+
return n;
|
|
11452
|
+
}
|
|
11453
|
+
/** Track a channel for as long as `until` lasts; holds its grant from idling out. */
|
|
11454
|
+
channel(need, open3, close, onEnd, still) {
|
|
11455
|
+
const ch = still ? { need, open: open3, close, still } : { need, open: open3, close };
|
|
11456
|
+
this.channels.add(ch);
|
|
11457
|
+
const release = open3.grant ? this.o.presence.hold(open3.grant) : () => {
|
|
11458
|
+
};
|
|
11459
|
+
onEnd(() => {
|
|
11460
|
+
this.channels.delete(ch);
|
|
11461
|
+
release();
|
|
11462
|
+
});
|
|
11463
|
+
}
|
|
11464
|
+
get openChannels() {
|
|
11465
|
+
return this.channels.size;
|
|
11466
|
+
}
|
|
11467
|
+
async handle(req, res) {
|
|
11468
|
+
const url = new URL(req.url ?? "/", "http://machine");
|
|
11469
|
+
const open3 = this.openOf(req);
|
|
11470
|
+
const route = `${req.method} ${url.pathname}`;
|
|
11471
|
+
switch (route) {
|
|
11472
|
+
case "GET /api/health": {
|
|
11473
|
+
const hub = this.o.hub();
|
|
11474
|
+
const l = this.o.policy();
|
|
11475
|
+
return json(res, 200, {
|
|
11476
|
+
ok: true,
|
|
11477
|
+
herdr: { connected: hub?.connected ?? false, version: hub?.version ?? null, protocol: hub?.protocol ?? null },
|
|
11478
|
+
workspaceRoot: this.o.home,
|
|
11479
|
+
homeRoot: this.o.home,
|
|
11480
|
+
sharing: false,
|
|
11481
|
+
publicUrl: null,
|
|
11482
|
+
machine: {
|
|
11483
|
+
terminals: l.policy.terminals,
|
|
11484
|
+
files: l.policy.files.map((f) => ({ path: f.path, mode: f.mode })),
|
|
11485
|
+
ports: l.policy.ports,
|
|
11486
|
+
problem: l.problem,
|
|
11487
|
+
presenceLocked: this.o.presence.locked()
|
|
11488
|
+
}
|
|
11489
|
+
});
|
|
11490
|
+
}
|
|
11491
|
+
case "POST /presence/challenge": {
|
|
11492
|
+
await readJson(req);
|
|
11493
|
+
const c = this.o.presence.challenge(open3.session);
|
|
11494
|
+
if (c === null) return json(res, 400, { error: "no_session", message: "no machines session" });
|
|
11495
|
+
if ("locked" in c) return json(res, 429, { error: "locked", until: c.until, message: lockedMessage(c.until) });
|
|
11496
|
+
return json(res, 200, c);
|
|
11497
|
+
}
|
|
11498
|
+
case "POST /presence/answer": {
|
|
11499
|
+
const b = await readJson(req);
|
|
11500
|
+
const r = this.o.presence.answer(open3.session, typeof b?.challenge === "string" ? b.challenge : "", typeof b?.answer === "string" ? b.answer : "");
|
|
11501
|
+
if (r.ok) return json(res, 200, { grant: r.grant, expiresAt: r.expiresAt });
|
|
11502
|
+
if (r.error === "locked") return json(res, 429, { error: "locked", until: r.until, message: lockedMessage(r.until) });
|
|
11503
|
+
if (r.error === "wrong") return json(res, 401, { error: "wrong", message: "That is not this machine's passphrase.", failures: r.failures });
|
|
11504
|
+
return json(res, 400, { error: r.error, message: "That challenge has expired; try again." });
|
|
11505
|
+
}
|
|
11506
|
+
case "GET /api/session":
|
|
11507
|
+
case "POST /api/rpc": {
|
|
11508
|
+
const no = this.refusal("terminals", open3);
|
|
11509
|
+
if (no) return json(res, no.status, no.body);
|
|
11510
|
+
const socket2 = this.o.herdrSocket();
|
|
11511
|
+
if (!socket2) return json(res, 503, { error: "herdr", message: "herdr is starting on this machine" });
|
|
11512
|
+
this.channel(
|
|
11513
|
+
"terminals",
|
|
11514
|
+
open3,
|
|
11515
|
+
() => res.destroy(),
|
|
11516
|
+
(cb) => res.once("close", cb)
|
|
11517
|
+
);
|
|
11518
|
+
if (route === "GET /api/session") {
|
|
11519
|
+
const hub = this.o.hub();
|
|
11520
|
+
if (!hub) return json(res, 503, { error: "herdr", message: "herdr is starting on this machine" });
|
|
11521
|
+
try {
|
|
11522
|
+
return json(res, 200, await hub.snapshot());
|
|
11523
|
+
} catch (err) {
|
|
11524
|
+
return json(res, 502, { error: "herdr", message: err.message });
|
|
11525
|
+
}
|
|
11526
|
+
}
|
|
11527
|
+
const b = await readJson(req);
|
|
11528
|
+
const method = b?.method;
|
|
11529
|
+
if (typeof method !== "string") return json(res, 400, { error: "method required" });
|
|
11530
|
+
if (!isAllowed(method)) return json(res, 403, { error: "method not allowed" });
|
|
11531
|
+
const params = typeof b?.params === "object" && b.params !== null ? b.params : {};
|
|
11532
|
+
try {
|
|
11533
|
+
return json(res, 200, { result: await request(socket2, method, params) });
|
|
11534
|
+
} catch (err) {
|
|
11535
|
+
if (err instanceof HerdrError) return json(res, 502, { error: { code: err.code, message: err.message } });
|
|
11536
|
+
return json(res, 502, { error: { code: "unreachable", message: err.message } });
|
|
11537
|
+
}
|
|
11538
|
+
}
|
|
11539
|
+
default: {
|
|
11540
|
+
if (url.pathname === "/api/files" || url.pathname.startsWith("/api/files/")) return this.files(req, res, open3);
|
|
11541
|
+
return json(res, 404, { error: "not found" });
|
|
11542
|
+
}
|
|
11543
|
+
}
|
|
11544
|
+
}
|
|
11545
|
+
async files(req, res, open3) {
|
|
11546
|
+
const no = this.refusal("files", open3);
|
|
11547
|
+
if (no) {
|
|
11548
|
+
req.resume();
|
|
11549
|
+
return json(res, no.status, no.body);
|
|
11550
|
+
}
|
|
11551
|
+
const svc = this.o.files?.() ?? null;
|
|
11552
|
+
if (!svc) return json(res, 403, { error: "files_off", message: "this machine's policy.json allows no folders" });
|
|
11553
|
+
this.channel(
|
|
11554
|
+
"files",
|
|
11555
|
+
open3,
|
|
11556
|
+
() => res.destroy(),
|
|
11557
|
+
(cb) => res.once("close", cb),
|
|
11558
|
+
() => this.o.files?.() === svc
|
|
11559
|
+
);
|
|
11560
|
+
await serveFiles(svc, req, res);
|
|
11561
|
+
}
|
|
11562
|
+
/** The port an app stream is for, if it may be served now; else the refusal to send. */
|
|
11563
|
+
appPort(open3) {
|
|
11564
|
+
const no = this.refusal("ports", open3);
|
|
11565
|
+
if (no) return no;
|
|
11566
|
+
const port = open3.app;
|
|
11567
|
+
if (port === void 0 || !this.o.previews?.has(port)) return { status: 404, body: { error: "not_found", message: "no app is registered on that port here" } };
|
|
11568
|
+
return { port };
|
|
11569
|
+
}
|
|
11570
|
+
/** A request for an app: passed on to its port as it came, its answer back as it comes (the gate applies the app policy). */
|
|
11571
|
+
app(req, res) {
|
|
11572
|
+
const open3 = this.openOf(req);
|
|
11573
|
+
const p = this.appPort(open3);
|
|
11574
|
+
if (!("port" in p)) {
|
|
11575
|
+
req.resume();
|
|
11576
|
+
return json(res, p.status, p.body);
|
|
11577
|
+
}
|
|
11578
|
+
const { port } = p;
|
|
11579
|
+
const headers = {};
|
|
11580
|
+
for (const [k, v] of Object.entries(req.headers)) if (v !== void 0 && !HOP.has(k)) headers[k] = v;
|
|
11581
|
+
if (req.headers["transfer-encoding"] !== void 0 && req.headers["content-length"] === void 0) headers["transfer-encoding"] = "chunked";
|
|
11582
|
+
connectLocal(port).then(
|
|
11583
|
+
(sock) => {
|
|
11584
|
+
if (res.destroyed) return void sock.destroy();
|
|
11585
|
+
const up = http.request({ method: req.method, path: req.url ?? "/", headers, createConnection: () => sock }, (upRes) => {
|
|
11586
|
+
const raw2 = [];
|
|
11587
|
+
for (let i = 0; i + 1 < upRes.rawHeaders.length; i += 2) {
|
|
11588
|
+
if (HOP.has(upRes.rawHeaders[i].toLowerCase())) continue;
|
|
11589
|
+
raw2.push(upRes.rawHeaders[i], upRes.rawHeaders[i + 1]);
|
|
11590
|
+
}
|
|
11591
|
+
res.writeHead(upRes.statusCode ?? 502, upRes.statusMessage, raw2);
|
|
11592
|
+
upRes.pipe(res);
|
|
11593
|
+
upRes.on("error", () => res.destroy());
|
|
11594
|
+
});
|
|
11595
|
+
up.on("error", () => res.headersSent ? res.destroy() : json(res, 502, { error: "app", message: `the app on port ${port} did not answer` }));
|
|
11596
|
+
res.once("close", () => {
|
|
11597
|
+
if (!res.writableFinished) up.destroy();
|
|
11598
|
+
});
|
|
11599
|
+
this.channel("ports", open3, () => res.destroy(), (cb) => res.once("close", cb), () => this.o.previews?.has(port) ?? false);
|
|
11600
|
+
req.pipe(up);
|
|
11601
|
+
},
|
|
11602
|
+
() => {
|
|
11603
|
+
req.resume();
|
|
11604
|
+
json(res, 502, { error: "app", message: `nothing answers on port ${port} here` });
|
|
11605
|
+
}
|
|
11606
|
+
);
|
|
11607
|
+
}
|
|
11608
|
+
/** An app's WebSocket (or any upgrade): its request head to the port, then bytes both ways. */
|
|
11609
|
+
appUpgrade(req, socket2, head) {
|
|
11610
|
+
socket2.on("error", () => socket2.destroy());
|
|
11611
|
+
const open3 = this.openOf(req);
|
|
11612
|
+
const p = this.appPort(open3);
|
|
11613
|
+
if (!("port" in p)) return refuseSocket(socket2, p.status, p.body);
|
|
11614
|
+
const { port } = p;
|
|
11615
|
+
connectLocal(port).then(
|
|
11616
|
+
(up) => {
|
|
11617
|
+
if (socket2.destroyed) return void up.destroy();
|
|
11618
|
+
let block = `${req.method ?? "GET"} ${req.url ?? "/"} HTTP/1.1\r
|
|
11619
|
+
`;
|
|
11620
|
+
for (let i = 0; i + 1 < req.rawHeaders.length; i += 2) block += `${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}\r
|
|
11621
|
+
`;
|
|
11622
|
+
up.write(`${block}\r
|
|
11623
|
+
`);
|
|
11624
|
+
if (head.length) up.write(head);
|
|
11625
|
+
const end = () => {
|
|
11626
|
+
up.destroy();
|
|
11627
|
+
socket2.destroy();
|
|
11628
|
+
};
|
|
11629
|
+
up.on("error", end);
|
|
11630
|
+
up.on("close", end);
|
|
11631
|
+
socket2.on("close", end);
|
|
11632
|
+
this.channel("ports", open3, end, (cb) => socket2.once("close", cb), () => this.o.previews?.has(port) ?? false);
|
|
11633
|
+
up.pipe(socket2);
|
|
11634
|
+
socket2.pipe(up);
|
|
11635
|
+
},
|
|
11636
|
+
() => refuseSocket(socket2, 502, { error: "app", message: `nothing answers on port ${port} here` })
|
|
11637
|
+
);
|
|
11638
|
+
}
|
|
11639
|
+
upgrade(req, socket2, head) {
|
|
11640
|
+
socket2.on("error", () => socket2.destroy());
|
|
11641
|
+
const url = new URL(req.url ?? "/", "http://machine");
|
|
11642
|
+
const open3 = this.openOf(req);
|
|
11643
|
+
if (url.pathname !== "/ws/terminal" && url.pathname !== "/ws/events") return refuseSocket(socket2, 404, { error: "not found" });
|
|
11644
|
+
const no = this.refusal("terminals", open3);
|
|
11645
|
+
if (no) return refuseSocket(socket2, no.status, no.body);
|
|
11646
|
+
const streams = this.o.streams();
|
|
11647
|
+
const hub = this.o.hub();
|
|
11648
|
+
if (!streams || !hub) return refuseSocket(socket2, 503, { error: "herdr", message: "herdr is starting on this machine" });
|
|
11649
|
+
this.wss.handleUpgrade(req, socket2, head, (ws) => {
|
|
11650
|
+
this.channel(
|
|
11651
|
+
"terminals",
|
|
11652
|
+
open3,
|
|
11653
|
+
() => ws.terminate(),
|
|
11654
|
+
(cb) => ws.once("close", cb)
|
|
11655
|
+
);
|
|
11656
|
+
if (url.pathname === "/ws/terminal") {
|
|
11657
|
+
serveTerminalSocket(ws, streams, { pane: url.searchParams.get("pane") ?? void 0, cols: url.searchParams.get("cols") ?? void 0, rows: url.searchParams.get("rows") ?? void 0 });
|
|
11658
|
+
return;
|
|
11659
|
+
}
|
|
11660
|
+
this.events(ws, hub);
|
|
11661
|
+
});
|
|
11662
|
+
}
|
|
11663
|
+
/** `/ws/events`: a fresh snapshot, then every hub message as it comes. */
|
|
9503
11664
|
events(ws, hub) {
|
|
9504
11665
|
heartbeat(ws);
|
|
9505
11666
|
let off = null;
|
|
9506
|
-
const
|
|
11667
|
+
const send2 = (m) => {
|
|
9507
11668
|
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(m));
|
|
9508
11669
|
};
|
|
9509
11670
|
hub.snapshot().then(
|
|
9510
11671
|
(snapshot) => {
|
|
9511
11672
|
if (ws.readyState !== ws.OPEN) return;
|
|
9512
|
-
|
|
9513
|
-
off = hub.on(
|
|
11673
|
+
send2({ kind: "snapshot", snapshot });
|
|
11674
|
+
off = hub.on(send2);
|
|
9514
11675
|
},
|
|
9515
11676
|
() => ws.close()
|
|
9516
11677
|
);
|
|
@@ -9521,6 +11682,7 @@ var NodeServer = class {
|
|
|
9521
11682
|
this.channels.clear();
|
|
9522
11683
|
this.wss.close();
|
|
9523
11684
|
this.server.close();
|
|
11685
|
+
this.appServer.close();
|
|
9524
11686
|
}
|
|
9525
11687
|
};
|
|
9526
11688
|
function lockedMessage(until) {
|
|
@@ -9528,91 +11690,11 @@ function lockedMessage(until) {
|
|
|
9528
11690
|
return `Too many wrong passphrases: this machine has locked presence for ${mins} more minute${mins === 1 ? "" : "s"}.`;
|
|
9529
11691
|
}
|
|
9530
11692
|
|
|
9531
|
-
// src/node/service-linux.ts
|
|
9532
|
-
import { execFile } from "node:child_process";
|
|
9533
|
-
import fs17 from "node:fs";
|
|
9534
|
-
import os9 from "node:os";
|
|
9535
|
-
import path15 from "node:path";
|
|
9536
|
-
var UNIT_NAME = "agentbox-node.service";
|
|
9537
|
-
var runCommand = (cmd, args) => new Promise((resolve) => {
|
|
9538
|
-
execFile(cmd, args, { timeout: 3e4 }, (err, stdout, stderr) => {
|
|
9539
|
-
const code2 = err ? typeof err.code === "number" ? err.code : 127 : 0;
|
|
9540
|
-
resolve({ code: code2, stdout: String(stdout), stderr: String(stderr) });
|
|
9541
|
-
});
|
|
9542
|
-
});
|
|
9543
|
-
function unitDir(env = process.env, home2 = os9.homedir()) {
|
|
9544
|
-
const xdg = env.XDG_CONFIG_HOME;
|
|
9545
|
-
return path15.join(xdg && path15.isAbsolute(xdg) ? xdg : path15.join(home2, ".config"), "systemd", "user");
|
|
9546
|
-
}
|
|
9547
|
-
function execStart(execPath = process.execPath, script = process.argv[1]) {
|
|
9548
|
-
const isSea = script === void 0 || path15.resolve(script) === path15.resolve(execPath);
|
|
9549
|
-
return isSea ? [execPath, "node", "run"] : [execPath, path15.resolve(script), "node", "run"];
|
|
9550
|
-
}
|
|
9551
|
-
function quote(arg) {
|
|
9552
|
-
return `"${arg.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%")}"`;
|
|
9553
|
-
}
|
|
9554
|
-
function unitText(command2) {
|
|
9555
|
-
return [
|
|
9556
|
-
"[Unit]",
|
|
9557
|
-
"Description=agentbox node: this machine's link to its box",
|
|
9558
|
-
"After=network-online.target",
|
|
9559
|
-
"Wants=network-online.target",
|
|
9560
|
-
"",
|
|
9561
|
-
"[Service]",
|
|
9562
|
-
"Type=simple",
|
|
9563
|
-
`ExecStart=${command2.map(quote).join(" ")}`,
|
|
9564
|
-
"Restart=on-failure",
|
|
9565
|
-
"RestartSec=5",
|
|
9566
|
-
// herdr, and the agents in it, outlive a restart of the node (an
|
|
9567
|
-
// update, a crash); the next node adopts it.
|
|
9568
|
-
"KillMode=process",
|
|
9569
|
-
"",
|
|
9570
|
-
"[Install]",
|
|
9571
|
-
"WantedBy=default.target",
|
|
9572
|
-
""
|
|
9573
|
-
].join("\n");
|
|
9574
|
-
}
|
|
9575
|
-
async function installService(opts) {
|
|
9576
|
-
const run4 = opts.run ?? runCommand;
|
|
9577
|
-
const dir = unitDir(opts.env);
|
|
9578
|
-
fs17.mkdirSync(dir, { recursive: true });
|
|
9579
|
-
const unitFile = path15.join(dir, UNIT_NAME);
|
|
9580
|
-
fs17.writeFileSync(unitFile, unitText(opts.command), { mode: 420 });
|
|
9581
|
-
for (const args of [["--user", "daemon-reload"], ["--user", "enable", UNIT_NAME], ["--user", "restart", UNIT_NAME]]) {
|
|
9582
|
-
const r = await run4("systemctl", args);
|
|
9583
|
-
if (r.code !== 0) {
|
|
9584
|
-
throw new Error(`systemctl ${args.join(" ")} failed: ${(r.stderr || r.stdout).trim() || `exit ${r.code}`}`);
|
|
9585
|
-
}
|
|
9586
|
-
}
|
|
9587
|
-
const user = opts.user ?? os9.userInfo().username;
|
|
9588
|
-
const linger = await run4("loginctl", ["enable-linger", user]);
|
|
9589
|
-
if (linger.code === 0) return { unitFile, lingering: true, note: null };
|
|
9590
|
-
const cron = `@reboot ${opts.command.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ")}`;
|
|
9591
|
-
return {
|
|
9592
|
-
unitFile,
|
|
9593
|
-
lingering: false,
|
|
9594
|
-
note: `This machine does not let the node keep running after you log out (loginctl enable-linger was refused), so it runs while you are logged in. To start it at boot instead, add this line with \`crontab -e\`:
|
|
9595
|
-
${cron}`
|
|
9596
|
-
};
|
|
9597
|
-
}
|
|
9598
|
-
async function removeService(opts = {}) {
|
|
9599
|
-
const run4 = opts.run ?? runCommand;
|
|
9600
|
-
const unitFile = path15.join(unitDir(opts.env), UNIT_NAME);
|
|
9601
|
-
if (!fs17.existsSync(unitFile)) return false;
|
|
9602
|
-
await run4("systemctl", ["--user", "disable", "--now", UNIT_NAME]);
|
|
9603
|
-
fs17.rmSync(unitFile, { force: true });
|
|
9604
|
-
await run4("systemctl", ["--user", "daemon-reload"]);
|
|
9605
|
-
return true;
|
|
9606
|
-
}
|
|
9607
|
-
function serviceInstalled(env = process.env) {
|
|
9608
|
-
return fs17.existsSync(path15.join(unitDir(env), UNIT_NAME));
|
|
9609
|
-
}
|
|
9610
|
-
|
|
9611
11693
|
// src/node/specs.ts
|
|
9612
11694
|
import { execFile as execFile2 } from "node:child_process";
|
|
9613
|
-
import
|
|
11695
|
+
import fs19 from "node:fs";
|
|
9614
11696
|
import os10 from "node:os";
|
|
9615
|
-
import
|
|
11697
|
+
import path26 from "node:path";
|
|
9616
11698
|
function run(cmd, args, timeoutMs = 3e3) {
|
|
9617
11699
|
return new Promise((resolve) => {
|
|
9618
11700
|
execFile2(cmd, args, { timeout: timeoutMs, windowsHide: true, maxBuffer: 256 * 1024 }, (err, stdout) => resolve(err ? null : stdout));
|
|
@@ -9620,10 +11702,10 @@ function run(cmd, args, timeoutMs = 3e3) {
|
|
|
9620
11702
|
}
|
|
9621
11703
|
function onPath(cmd, env) {
|
|
9622
11704
|
const exts = process.platform === "win32" ? [".exe", ".cmd", ""] : [""];
|
|
9623
|
-
for (const dir of (env.PATH ?? "").split(
|
|
11705
|
+
for (const dir of (env.PATH ?? "").split(path26.delimiter)) {
|
|
9624
11706
|
for (const ext of exts) {
|
|
9625
11707
|
try {
|
|
9626
|
-
|
|
11708
|
+
fs19.accessSync(path26.join(dir, cmd + ext), fs19.constants.X_OK);
|
|
9627
11709
|
return true;
|
|
9628
11710
|
} catch {
|
|
9629
11711
|
}
|
|
@@ -9648,15 +11730,15 @@ async function gpus(env) {
|
|
|
9648
11730
|
}
|
|
9649
11731
|
function freeBytes(dir) {
|
|
9650
11732
|
try {
|
|
9651
|
-
const s =
|
|
11733
|
+
const s = fs19.statfsSync(dir);
|
|
9652
11734
|
return s.bavail * s.bsize;
|
|
9653
11735
|
} catch {
|
|
9654
11736
|
return null;
|
|
9655
11737
|
}
|
|
9656
11738
|
}
|
|
9657
11739
|
async function claudeVersion(binDir, env) {
|
|
9658
|
-
const own =
|
|
9659
|
-
const cmd =
|
|
11740
|
+
const own = path26.join(binDir, process.platform === "win32" ? "claude.exe" : "claude");
|
|
11741
|
+
const cmd = fs19.existsSync(own) ? own : onPath("claude", env) ? "claude" : null;
|
|
9660
11742
|
if (!cmd) return null;
|
|
9661
11743
|
const out = await run(cmd, ["--version"], 5e3);
|
|
9662
11744
|
return out ? /\d+\.\d+\.\d+/.exec(out)?.[0] ?? null : null;
|
|
@@ -9682,6 +11764,204 @@ async function gatherSpecs(opts) {
|
|
|
9682
11764
|
};
|
|
9683
11765
|
}
|
|
9684
11766
|
|
|
11767
|
+
// src/node/files.ts
|
|
11768
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
11769
|
+
import fs20 from "node:fs";
|
|
11770
|
+
import path27 from "node:path";
|
|
11771
|
+
function pathOf(platform) {
|
|
11772
|
+
return platform === "win32" ? path27.win32 : path27.posix;
|
|
11773
|
+
}
|
|
11774
|
+
function pathLists(o) {
|
|
11775
|
+
const platform = o.platform ?? process.platform;
|
|
11776
|
+
const env = o.env ?? process.env;
|
|
11777
|
+
const P = pathOf(platform);
|
|
11778
|
+
const H = o.home;
|
|
11779
|
+
const N = o.nodeDir;
|
|
11780
|
+
const j = (...parts) => P.join(...parts);
|
|
11781
|
+
const nodeSecrets = ["credentials.json", "secrets", "claude", "control.secret", "control.sock", "state"].map((f) => j(N, f));
|
|
11782
|
+
const ci = platform === "win32" || platform === "darwin";
|
|
11783
|
+
const fold = (p) => ci ? P.normalize(p).toLowerCase() : P.normalize(p);
|
|
11784
|
+
const home2 = fold(H);
|
|
11785
|
+
const onPath2 = (env.PATH ?? env.Path ?? "").split(P.delimiter).filter((d) => d !== "" && P.isAbsolute(d) && fold(d) !== home2 && fold(d).startsWith(home2 + P.sep));
|
|
11786
|
+
if (platform === "win32") {
|
|
11787
|
+
const A = env.APPDATA && P.isAbsolute(env.APPDATA) ? env.APPDATA : j(H, "AppData", "Roaming");
|
|
11788
|
+
const L = env.LOCALAPPDATA && P.isAbsolute(env.LOCALAPPDATA) ? env.LOCALAPPDATA : j(H, "AppData", "Local");
|
|
11789
|
+
return {
|
|
11790
|
+
hidden: [
|
|
11791
|
+
j(H, ".ssh"),
|
|
11792
|
+
j(H, ".gnupg"),
|
|
11793
|
+
j(A, "gnupg"),
|
|
11794
|
+
j(A, "agentbox"),
|
|
11795
|
+
j(H, ".config", "agentbox"),
|
|
11796
|
+
...nodeSecrets,
|
|
11797
|
+
j(H, ".claude", ".credentials.json"),
|
|
11798
|
+
j(A, "Microsoft", "Credentials"),
|
|
11799
|
+
j(L, "Microsoft", "Credentials"),
|
|
11800
|
+
j(A, "Microsoft", "Protect"),
|
|
11801
|
+
j(L, "Google", "Chrome", "User Data"),
|
|
11802
|
+
j(L, "Microsoft", "Edge", "User Data"),
|
|
11803
|
+
j(L, "BraveSoftware"),
|
|
11804
|
+
j(L, "Chromium"),
|
|
11805
|
+
j(L, "Vivaldi"),
|
|
11806
|
+
j(A, "Opera Software"),
|
|
11807
|
+
j(A, "Mozilla", "Firefox"),
|
|
11808
|
+
j(L, "Mozilla", "Firefox")
|
|
11809
|
+
],
|
|
11810
|
+
writeDenied: [
|
|
11811
|
+
N,
|
|
11812
|
+
...onPath2,
|
|
11813
|
+
j(H, "Documents", "PowerShell"),
|
|
11814
|
+
j(H, "Documents", "WindowsPowerShell"),
|
|
11815
|
+
j(A, "Microsoft", "Windows", "Start Menu", "Programs", "Startup"),
|
|
11816
|
+
j(H, ".gitconfig"),
|
|
11817
|
+
j(H, ".config", "git"),
|
|
11818
|
+
// Git Bash reads these.
|
|
11819
|
+
j(H, ".bashrc"),
|
|
11820
|
+
j(H, ".bash_profile"),
|
|
11821
|
+
j(H, ".profile"),
|
|
11822
|
+
j(H, ".ssh", "authorized_keys")
|
|
11823
|
+
]
|
|
11824
|
+
};
|
|
11825
|
+
}
|
|
11826
|
+
const configs = [.../* @__PURE__ */ new Set([env.XDG_CONFIG_HOME && P.isAbsolute(env.XDG_CONFIG_HOME) ? env.XDG_CONFIG_HOME : j(H, ".config"), j(H, ".config")])];
|
|
11827
|
+
const inConfig = (...parts) => configs.map((c) => j(c, ...parts));
|
|
11828
|
+
const mac = platform === "darwin" ? j(H, "Library") : null;
|
|
11829
|
+
return {
|
|
11830
|
+
hidden: [
|
|
11831
|
+
j(H, ".ssh"),
|
|
11832
|
+
j(H, ".gnupg"),
|
|
11833
|
+
...inConfig("agentbox"),
|
|
11834
|
+
...nodeSecrets,
|
|
11835
|
+
j(H, ".claude", ".credentials.json"),
|
|
11836
|
+
j(H, ".mozilla"),
|
|
11837
|
+
j(H, ".librewolf"),
|
|
11838
|
+
j(H, "snap", "firefox"),
|
|
11839
|
+
j(H, "snap", "chromium"),
|
|
11840
|
+
j(H, ".var", "app", "org.mozilla.firefox"),
|
|
11841
|
+
j(H, ".var", "app", "com.google.Chrome"),
|
|
11842
|
+
j(H, ".var", "app", "org.chromium.Chromium"),
|
|
11843
|
+
j(H, ".var", "app", "com.brave.Browser"),
|
|
11844
|
+
...["google-chrome", "google-chrome-beta", "google-chrome-unstable", "chromium", "BraveSoftware", "microsoft-edge", "vivaldi", "opera"].flatMap((b) => inConfig(b)),
|
|
11845
|
+
...mac ? [
|
|
11846
|
+
j(mac, "Keychains"),
|
|
11847
|
+
j(mac, "Cookies"),
|
|
11848
|
+
j(mac, "Safari"),
|
|
11849
|
+
j(mac, "Application Support", "Google", "Chrome"),
|
|
11850
|
+
j(mac, "Application Support", "Firefox"),
|
|
11851
|
+
j(mac, "Application Support", "BraveSoftware"),
|
|
11852
|
+
j(mac, "Application Support", "Microsoft Edge"),
|
|
11853
|
+
j(mac, "Application Support", "Chromium")
|
|
11854
|
+
] : []
|
|
11855
|
+
],
|
|
11856
|
+
writeDenied: [
|
|
11857
|
+
N,
|
|
11858
|
+
...onPath2,
|
|
11859
|
+
j(H, ".local", "bin"),
|
|
11860
|
+
j(H, "bin"),
|
|
11861
|
+
j(H, ".local", "share", "applications"),
|
|
11862
|
+
j(H, ".pam_environment"),
|
|
11863
|
+
...[".bashrc", ".bash_aliases", ".bash_profile", ".bash_login", ".bash_logout", ".profile", ".zshrc", ".zshenv", ".zprofile", ".zlogin", ".zlogout", ".xprofile", ".xsession", ".xinitrc"].map((f) => j(H, f)),
|
|
11864
|
+
...inConfig("fish"),
|
|
11865
|
+
...inConfig("powershell"),
|
|
11866
|
+
...inConfig("systemd", "user"),
|
|
11867
|
+
...inConfig("environment.d"),
|
|
11868
|
+
...inConfig("autostart"),
|
|
11869
|
+
...inConfig("git"),
|
|
11870
|
+
j(H, ".ssh", "authorized_keys"),
|
|
11871
|
+
j(H, ".gitconfig"),
|
|
11872
|
+
...mac ? [j(mac, "LaunchAgents")] : []
|
|
11873
|
+
]
|
|
11874
|
+
};
|
|
11875
|
+
}
|
|
11876
|
+
function resolved(p) {
|
|
11877
|
+
try {
|
|
11878
|
+
return fs20.realpathSync.native(p);
|
|
11879
|
+
} catch {
|
|
11880
|
+
try {
|
|
11881
|
+
return path27.join(fs20.realpathSync.native(path27.dirname(p)), path27.basename(p));
|
|
11882
|
+
} catch {
|
|
11883
|
+
return null;
|
|
11884
|
+
}
|
|
11885
|
+
}
|
|
11886
|
+
}
|
|
11887
|
+
function refuseWindowsPath(input) {
|
|
11888
|
+
const s = input.replace(/\//g, "\\");
|
|
11889
|
+
if (s.startsWith("\\\\")) {
|
|
11890
|
+
return /^\\\\[.?]\\/.test(s) || s === "\\\\." || s === "\\\\?" ? "device paths (\\\\.\\, \\\\?\\) are refused" : "network (UNC) paths are refused";
|
|
11891
|
+
}
|
|
11892
|
+
const rest = s.replace(/^\\?[A-Za-z]:/, "");
|
|
11893
|
+
if (rest.includes(":")) return "alternate data streams (a `:` in a name) are refused";
|
|
11894
|
+
for (const part of rest.split("\\")) {
|
|
11895
|
+
if (part === "" || part === "." || part === "..") continue;
|
|
11896
|
+
if (/[. ]$/.test(part)) return "a name ending in a dot or a space is refused";
|
|
11897
|
+
if (/^(con|prn|aux|nul|com[0-9¹²³]|lpt[0-9¹²³]|conin\$|conout\$)(\..*)?$/i.test(part)) return "DOS device names are refused";
|
|
11898
|
+
}
|
|
11899
|
+
return null;
|
|
11900
|
+
}
|
|
11901
|
+
function pathRules(o) {
|
|
11902
|
+
const platform = o.platform ?? process.platform;
|
|
11903
|
+
const P = pathOf(platform);
|
|
11904
|
+
const caseInsensitive = platform === "win32" || platform === "darwin";
|
|
11905
|
+
const fold = (p) => {
|
|
11906
|
+
const n = P.normalize(p);
|
|
11907
|
+
const t2 = n.length > 1 && n.endsWith(P.sep) && !/^[A-Za-z]:\\$/.test(n) ? n.slice(0, -1) : n;
|
|
11908
|
+
return caseInsensitive ? t2.toLowerCase() : t2;
|
|
11909
|
+
};
|
|
11910
|
+
const lists = pathLists(o);
|
|
11911
|
+
const expand2 = (list) => {
|
|
11912
|
+
const out = /* @__PURE__ */ new Set();
|
|
11913
|
+
for (const p of list) {
|
|
11914
|
+
out.add(p);
|
|
11915
|
+
if (o.resolve ?? platform === process.platform) {
|
|
11916
|
+
const r = resolved(p);
|
|
11917
|
+
if (r) out.add(r);
|
|
11918
|
+
}
|
|
11919
|
+
}
|
|
11920
|
+
return [...out];
|
|
11921
|
+
};
|
|
11922
|
+
const hidden = expand2(lists.hidden);
|
|
11923
|
+
const writeDenied = expand2(lists.writeDenied);
|
|
11924
|
+
const hiddenF = hidden.map(fold);
|
|
11925
|
+
const deniedF = writeDenied.map(fold);
|
|
11926
|
+
const under = (p, list) => {
|
|
11927
|
+
const f = fold(p);
|
|
11928
|
+
return list.some((e) => f === e || f.startsWith(e.endsWith(P.sep) ? e : e + P.sep));
|
|
11929
|
+
};
|
|
11930
|
+
const gitRuns = (p) => {
|
|
11931
|
+
const parts = fold(p).split(P.sep);
|
|
11932
|
+
for (let i = 0; i + 1 < parts.length; i++) {
|
|
11933
|
+
if (parts[i] === ".git" && (parts[i + 1] === "hooks" || parts[i + 1] === "config")) return true;
|
|
11934
|
+
}
|
|
11935
|
+
return false;
|
|
11936
|
+
};
|
|
11937
|
+
return {
|
|
11938
|
+
caseInsensitive,
|
|
11939
|
+
hidden,
|
|
11940
|
+
writeDenied,
|
|
11941
|
+
isHidden: (p) => under(p, hiddenF),
|
|
11942
|
+
isWriteDenied: (p) => under(p, deniedF) || gitRuns(p),
|
|
11943
|
+
refusePath: (input) => platform === "win32" ? refuseWindowsPath(input) : null
|
|
11944
|
+
};
|
|
11945
|
+
}
|
|
11946
|
+
function rootId(folder) {
|
|
11947
|
+
return `r${createHash5("sha256").update(folder).digest("hex").slice(0, 12)}`;
|
|
11948
|
+
}
|
|
11949
|
+
function filesFor(policy, paths2, rules) {
|
|
11950
|
+
if (policy.files.length === 0) return null;
|
|
11951
|
+
const roots = policy.files.map((f) => {
|
|
11952
|
+
const id = rootId(f.path);
|
|
11953
|
+
return { id, path: f.path, mode: f.mode, stateDir: path27.join(paths2.state, "files", id) };
|
|
11954
|
+
});
|
|
11955
|
+
return new FilesService({
|
|
11956
|
+
roots,
|
|
11957
|
+
caseInsensitive: rules.caseInsensitive,
|
|
11958
|
+
isHidden: rules.isHidden,
|
|
11959
|
+
isWriteDenied: rules.isWriteDenied,
|
|
11960
|
+
refusePath: rules.refusePath
|
|
11961
|
+
});
|
|
11962
|
+
}
|
|
11963
|
+
var TRASH_DAYS = 7;
|
|
11964
|
+
|
|
9685
11965
|
// src/node/run.ts
|
|
9686
11966
|
var RESTART_EXIT = 75;
|
|
9687
11967
|
var HEADLESS = { cols: 200, rows: 50, ms: 3e4 };
|
|
@@ -9699,13 +11979,41 @@ async function runNode(o) {
|
|
|
9699
11979
|
let policy = loadPolicy(o.paths.policy, o.home);
|
|
9700
11980
|
const claude = claudePaths(o.paths);
|
|
9701
11981
|
prepareClaude(claude, { home: o.home, machine: creds.name });
|
|
9702
|
-
const herdrEnv = { ...process.env, PATH: [claude.shims, o.paths.bin, process.env.PATH ?? ""].join(
|
|
11982
|
+
const herdrEnv = { ...process.env, PATH: [claude.shims, o.paths.bin, process.env.PATH ?? ""].join(path28.delimiter) };
|
|
9703
11983
|
delete herdrEnv.CLAUDE_CODE_OAUTH_TOKEN;
|
|
11984
|
+
const rules = pathRules({ home: o.home, nodeDir: o.paths.dir });
|
|
11985
|
+
let files = null;
|
|
11986
|
+
let filesKey = "";
|
|
11987
|
+
let stopUploads = () => {
|
|
11988
|
+
};
|
|
11989
|
+
const setFiles = (l) => {
|
|
11990
|
+
const key = JSON.stringify(l.policy.files);
|
|
11991
|
+
if (key === filesKey) return;
|
|
11992
|
+
filesKey = key;
|
|
11993
|
+
stopUploads();
|
|
11994
|
+
files = filesFor(l.policy, o.paths, rules);
|
|
11995
|
+
stopUploads = files ? files.startMaintenance() : () => {
|
|
11996
|
+
};
|
|
11997
|
+
};
|
|
11998
|
+
setFiles(policy);
|
|
11999
|
+
const sweepTrash = () => {
|
|
12000
|
+
void files?.trash.expire(now() - TRASH_DAYS * 24 * 60 * 6e4).then(
|
|
12001
|
+
(n) => n && o.log(`files: ${n} item(s) older than ${TRASH_DAYS} days removed from the trash`),
|
|
12002
|
+
() => {
|
|
12003
|
+
}
|
|
12004
|
+
);
|
|
12005
|
+
};
|
|
12006
|
+
sweepTrash();
|
|
12007
|
+
const trashSweeper = setInterval(sweepTrash, 60 * 6e4);
|
|
12008
|
+
trashSweeper.unref();
|
|
12009
|
+
const previews = new Previews({ now });
|
|
12010
|
+
previews.start();
|
|
12011
|
+
writePreviewShim(claude.shims);
|
|
9704
12012
|
const presence = new PresenceGate({ presence: creds.presence, machineId: creds.machineId, hours: () => policy.policy.presenceHours, now, log: o.log });
|
|
9705
12013
|
let hub = null;
|
|
9706
12014
|
let streams = null;
|
|
9707
|
-
const herdrBin =
|
|
9708
|
-
const herdr =
|
|
12015
|
+
const herdrBin = path28.join(o.paths.bin, process.platform === "win32" ? "herdr.exe" : "herdr");
|
|
12016
|
+
const herdr = fs21.existsSync(herdrBin) ? new HerdrSupervisor({ bin: herdrBin, dir: o.paths.dir, env: herdrEnv }) : null;
|
|
9709
12017
|
if (!herdr) o.log(`herdr is not installed (${herdrBin}); terminals and agents are unavailable until \`agentbox node join\` installs it`);
|
|
9710
12018
|
const server = new NodeServer({
|
|
9711
12019
|
herdrSocket: () => herdr?.socketPath ?? null,
|
|
@@ -9714,6 +12022,8 @@ async function runNode(o) {
|
|
|
9714
12022
|
policy: () => policy,
|
|
9715
12023
|
presence,
|
|
9716
12024
|
home: o.home,
|
|
12025
|
+
files: () => files,
|
|
12026
|
+
previews,
|
|
9717
12027
|
log: o.log
|
|
9718
12028
|
});
|
|
9719
12029
|
herdr?.on("log", (line) => o.log(line));
|
|
@@ -9767,6 +12077,14 @@ async function runNode(o) {
|
|
|
9767
12077
|
...o.link ?? {}
|
|
9768
12078
|
});
|
|
9769
12079
|
link.on("log", (line) => o.log(line));
|
|
12080
|
+
const sendApps = () => {
|
|
12081
|
+
link.sendControl({ type: "apps", apps: previews.list().map((a) => ({ port: a.port, name: a.name })) });
|
|
12082
|
+
};
|
|
12083
|
+
previews.on("change", () => {
|
|
12084
|
+
sendApps();
|
|
12085
|
+
server.enforce();
|
|
12086
|
+
});
|
|
12087
|
+
link.on("welcome", sendApps);
|
|
9770
12088
|
link.on("stream", (stream, open3) => server.accept(stream, open3));
|
|
9771
12089
|
link.on("control", (m) => {
|
|
9772
12090
|
if (m.type !== "claude-token") return;
|
|
@@ -9779,6 +12097,8 @@ async function runNode(o) {
|
|
|
9779
12097
|
const stopWatching = watchPolicy(o.paths.policy, o.home, (l) => {
|
|
9780
12098
|
policy = l;
|
|
9781
12099
|
sayPolicy(l);
|
|
12100
|
+
setFiles(l);
|
|
12101
|
+
if (!l.policy.ports) previews.clear();
|
|
9782
12102
|
const closed = server.enforce();
|
|
9783
12103
|
if (closed) o.log(`policy: closed ${closed} channel(s) it no longer allows`);
|
|
9784
12104
|
if (l.policy.paused && !link.isPaused) link.pause();
|
|
@@ -9788,8 +12108,23 @@ async function runNode(o) {
|
|
|
9788
12108
|
let finish = () => {
|
|
9789
12109
|
};
|
|
9790
12110
|
const done = new Promise((r) => finish = r);
|
|
9791
|
-
const control2 = await startControl(o.paths.control, o.paths.controlSecret, async (command2) => {
|
|
12111
|
+
const control2 = await startControl(o.paths.control, o.paths.controlSecret, async (command2, args) => {
|
|
9792
12112
|
switch (command2) {
|
|
12113
|
+
case "preview": {
|
|
12114
|
+
if (!policy.policy.ports) throw new Error(`apps are off in this machine's policy.json ("ports": false)`);
|
|
12115
|
+
if (args.share === true) throw new Error("apps on a machine cannot be shared publicly yet");
|
|
12116
|
+
const r = await previews.register(args.port, args.name);
|
|
12117
|
+
if (!r.ok) throw new Error(r.error);
|
|
12118
|
+
o.log(`preview: port ${r.app.port} registered as "${r.app.name}"`);
|
|
12119
|
+
return { app: r.app, existed: r.existed, online: link.state === "online" };
|
|
12120
|
+
}
|
|
12121
|
+
case "preview-list":
|
|
12122
|
+
return { apps: previews.list() };
|
|
12123
|
+
case "preview-stop": {
|
|
12124
|
+
const port = Number(args.port);
|
|
12125
|
+
if (!previews.remove(port)) throw new Error(`no app is registered on port ${args.port}`);
|
|
12126
|
+
return { removed: port };
|
|
12127
|
+
}
|
|
9793
12128
|
case "status":
|
|
9794
12129
|
return {
|
|
9795
12130
|
state: link.state,
|
|
@@ -9819,7 +12154,7 @@ async function runNode(o) {
|
|
|
9819
12154
|
}
|
|
9820
12155
|
});
|
|
9821
12156
|
link.on("revoked", () => {
|
|
9822
|
-
|
|
12157
|
+
fs21.rmSync(o.paths.secrets, { recursive: true, force: true });
|
|
9823
12158
|
writeCredentials(o.paths.credentials, { ...creds, revokedAt: now() });
|
|
9824
12159
|
o.log(`${creds.box} revoked this machine. Its shared secrets are wiped; everything else is kept. Re-join it to use it again.`);
|
|
9825
12160
|
finish(EXIT.OK);
|
|
@@ -9839,7 +12174,7 @@ async function runNode(o) {
|
|
|
9839
12174
|
running: o.version,
|
|
9840
12175
|
// Only the standalone executable replaces itself; agentbox.mjs under Node does not.
|
|
9841
12176
|
nodePath: execStart().length === 3 ? process.execPath : null,
|
|
9842
|
-
...
|
|
12177
|
+
...fs21.existsSync(herdrPath) ? { herdr: { path: herdrPath, version: HERDR_VERSION } } : {},
|
|
9843
12178
|
...installedClaude(claude) ? { claude: { path: claude.bin, version: installedClaude(claude), versionFile: claude.versionFile } } : {},
|
|
9844
12179
|
// Agents live in herdr: herdr and Claude Code are replaced only while none runs.
|
|
9845
12180
|
idle: () => herdr ? herdr.idle() : true
|
|
@@ -9869,6 +12204,9 @@ async function runNode(o) {
|
|
|
9869
12204
|
link.start();
|
|
9870
12205
|
const code2 = await done;
|
|
9871
12206
|
clearInterval(enforcer);
|
|
12207
|
+
clearInterval(trashSweeper);
|
|
12208
|
+
stopUploads();
|
|
12209
|
+
previews.stop();
|
|
9872
12210
|
server.close();
|
|
9873
12211
|
herdr?.stop();
|
|
9874
12212
|
hub?.stop();
|
|
@@ -9888,22 +12226,22 @@ function paths(ctx) {
|
|
|
9888
12226
|
function home(ctx) {
|
|
9889
12227
|
return ctx.env.HOME || os11.homedir();
|
|
9890
12228
|
}
|
|
9891
|
-
function parseFolders(
|
|
12229
|
+
function parseFolders(raw2, homeDir2) {
|
|
9892
12230
|
const out = [];
|
|
9893
|
-
for (const part of
|
|
12231
|
+
for (const part of raw2.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
9894
12232
|
const m = /^(.*?)(?::(read|write))?$/.exec(part);
|
|
9895
12233
|
let p = m[1];
|
|
9896
12234
|
if (p === "~") p = homeDir2;
|
|
9897
|
-
else if (p.startsWith("~/")) p =
|
|
9898
|
-
p =
|
|
9899
|
-
if (p ===
|
|
12235
|
+
else if (p.startsWith("~/")) p = path29.join(homeDir2, p.slice(2));
|
|
12236
|
+
p = path29.resolve(p);
|
|
12237
|
+
if (p === path29.resolve(homeDir2)) throw new UsageError("not the whole home folder: name the project folders the box may use");
|
|
9900
12238
|
out.push({ path: p, mode: m[2] ?? "write" });
|
|
9901
12239
|
}
|
|
9902
12240
|
return out;
|
|
9903
12241
|
}
|
|
9904
12242
|
function printPolicy(ctx, p) {
|
|
9905
12243
|
try {
|
|
9906
|
-
ctx.err(`${
|
|
12244
|
+
ctx.err(`${fs22.readFileSync(p.policy, "utf8")}`);
|
|
9907
12245
|
} catch {
|
|
9908
12246
|
ctx.err("(no policy.json)\n");
|
|
9909
12247
|
}
|
|
@@ -10078,9 +12416,9 @@ var leave = command({
|
|
|
10078
12416
|
link.stop();
|
|
10079
12417
|
}
|
|
10080
12418
|
if (ctx.platform === "linux") await removeService({ env: ctx.env });
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
12419
|
+
fs22.rmSync(np.secrets, { recursive: true, force: true });
|
|
12420
|
+
fs22.rmSync(np.credentials, { force: true });
|
|
12421
|
+
fs22.rmSync(np.controlSecret, { force: true });
|
|
10084
12422
|
ctx.err(told ? `Left ${creds.box}.
|
|
10085
12423
|
` : `Left here; ${creds.box} could not be told, so it lists this machine until the owner removes it.
|
|
10086
12424
|
`);
|
|
@@ -10097,7 +12435,7 @@ var logs = command({
|
|
|
10097
12435
|
async run(ctx, p) {
|
|
10098
12436
|
const args = ["--user", "-u", UNIT_NAME, "-n", "200", "--no-pager", ...bool(p.options, "follow") ? ["-f"] : []];
|
|
10099
12437
|
return new Promise((resolve) => {
|
|
10100
|
-
const child =
|
|
12438
|
+
const child = spawn9("journalctl", args, { stdio: "inherit" });
|
|
10101
12439
|
child.on("error", () => {
|
|
10102
12440
|
ctx.err("journalctl is not available here; run `agentbox node run` in a terminal to see the log.\n");
|
|
10103
12441
|
resolve(EXIT.FAILURE);
|
|
@@ -10125,26 +12463,81 @@ var passphrase = command({
|
|
|
10125
12463
|
ctx.err("This machine now proves presence with the new passphrase.\n");
|
|
10126
12464
|
}
|
|
10127
12465
|
});
|
|
10128
|
-
var
|
|
12466
|
+
var preview = command({
|
|
12467
|
+
path: ["node", "preview"],
|
|
12468
|
+
summary: "show an app on this machine in the owner's Preview (agentbox-preview)",
|
|
12469
|
+
usage: "<port> | open <port> | list | stop <port>",
|
|
12470
|
+
details: [
|
|
12471
|
+
"agentbox-preview <port> [--name n] register a server already listening on <port>",
|
|
12472
|
+
"agentbox-preview list what is registered",
|
|
12473
|
+
"agentbox-preview stop <port> take it off the Preview",
|
|
12474
|
+
"",
|
|
12475
|
+
"Only registered ports are ever reached, on 127.0.0.1 or [::1]. A registration ends when",
|
|
12476
|
+
"the port has not answered for 60 seconds, or when the node restarts. Apps on a machine are",
|
|
12477
|
+
"private: the owner opens them from the machines page. `start -- <command>` is not",
|
|
12478
|
+
"available on a machine yet: start the server yourself, then register its port."
|
|
12479
|
+
].join("\n"),
|
|
12480
|
+
operands: { min: 1, max: 2 },
|
|
12481
|
+
json: true,
|
|
12482
|
+
options: [{ name: "name", type: "string", value: "name", description: "what to call it (default: the port)" }],
|
|
12483
|
+
async run(ctx, p) {
|
|
12484
|
+
const np = paths(ctx);
|
|
12485
|
+
const [verb, arg] = p.operands;
|
|
12486
|
+
const ask2 = async (command2, args = {}) => {
|
|
12487
|
+
const r2 = await callControl(np.control, np.controlSecret, command2, 15e3, args);
|
|
12488
|
+
if (r2 === null) throw new CliError("the node is not running on this machine (`agentbox node status`)", EXIT.UNREACHABLE);
|
|
12489
|
+
if (r2.ok !== true) throw new CliError(String(r2.error ?? "the node refused"));
|
|
12490
|
+
return r2;
|
|
12491
|
+
};
|
|
12492
|
+
if (verb === "list") {
|
|
12493
|
+
const r2 = await ask2("preview-list");
|
|
12494
|
+
const apps = Array.isArray(r2.apps) ? r2.apps : [];
|
|
12495
|
+
if (ctx.json) return ctx.printJson({ apps });
|
|
12496
|
+
if (apps.length === 0) ctx.out("No apps registered on this machine.\n");
|
|
12497
|
+
for (const a of apps) ctx.out(`${a.port} ${safeText(a.name)}
|
|
12498
|
+
`);
|
|
12499
|
+
return;
|
|
12500
|
+
}
|
|
12501
|
+
if (verb === "start") throw new UsageError("`start` is not available on a machine yet: start the server yourself, then `agentbox-preview <port>`");
|
|
12502
|
+
if (verb === "stop") {
|
|
12503
|
+
if (!arg) throw new UsageError("stop needs a port");
|
|
12504
|
+
await ask2("preview-stop", { port: Number(arg) });
|
|
12505
|
+
ctx.err(`Port ${arg} is off the Preview.
|
|
12506
|
+
`);
|
|
12507
|
+
return;
|
|
12508
|
+
}
|
|
12509
|
+
const port = verb === "open" ? arg : verb;
|
|
12510
|
+
if (!port || !/^\d{1,5}$/.test(port)) throw new UsageError("name a port: agentbox-preview <port>");
|
|
12511
|
+
const name = str(p.options, "name");
|
|
12512
|
+
const r = await ask2("preview", { port: Number(port), ...name === void 0 ? {} : { name } });
|
|
12513
|
+
const app = r.app;
|
|
12514
|
+
if (ctx.json) return ctx.printJson({ app, online: r.online === true });
|
|
12515
|
+
ctx.err(
|
|
12516
|
+
`Port ${app.port} is in the owner's Preview as "${safeText(app.name)}" (the machines page, this machine, Preview).${r.online === true ? "" : " The machine is not linked to its box right now; it shows there once it is."}
|
|
12517
|
+
`
|
|
12518
|
+
);
|
|
12519
|
+
}
|
|
12520
|
+
});
|
|
12521
|
+
var NODE_COMMANDS = [join, run2, status2, pause, resume, leave, logs, passphrase, preview];
|
|
10129
12522
|
|
|
10130
12523
|
// src/commands/mount.ts
|
|
10131
|
-
import
|
|
12524
|
+
import fs23 from "node:fs";
|
|
10132
12525
|
import os12 from "node:os";
|
|
10133
|
-
import
|
|
12526
|
+
import path30 from "node:path";
|
|
10134
12527
|
|
|
10135
12528
|
// src/dav.ts
|
|
10136
|
-
import { randomBytes as
|
|
12529
|
+
import { randomBytes as randomBytes11, timingSafeEqual as timingSafeEqual4 } from "node:crypto";
|
|
10137
12530
|
import http2 from "node:http";
|
|
10138
12531
|
var REMOTE_PREFIX = "/api/dav";
|
|
10139
12532
|
var BadPath = class extends Error {
|
|
10140
12533
|
};
|
|
10141
12534
|
function percentDecode(s) {
|
|
10142
12535
|
const out = [];
|
|
10143
|
-
const
|
|
10144
|
-
for (let i = 0; i <
|
|
10145
|
-
const b =
|
|
10146
|
-
if (b === 37 && i + 2 <
|
|
10147
|
-
const hex = String.fromCharCode(
|
|
12536
|
+
const raw2 = Buffer.from(s, "utf8");
|
|
12537
|
+
for (let i = 0; i < raw2.length; i++) {
|
|
12538
|
+
const b = raw2[i];
|
|
12539
|
+
if (b === 37 && i + 2 < raw2.length) {
|
|
12540
|
+
const hex = String.fromCharCode(raw2[i + 1], raw2[i + 2]);
|
|
10148
12541
|
if (/^[0-9A-Fa-f]{2}$/.test(hex)) {
|
|
10149
12542
|
out.push(parseInt(hex, 16));
|
|
10150
12543
|
i += 2;
|
|
@@ -10242,7 +12635,7 @@ var DROP_REQUEST = /* @__PURE__ */ new Set(["host", "authorization", "proxy-auth
|
|
|
10242
12635
|
var DROP_RESPONSE = /* @__PURE__ */ new Set(["set-cookie", "www-authenticate", "strict-transport-security", "alt-svc"]);
|
|
10243
12636
|
var MAX_REWRITE = 64 * 1024 * 1024;
|
|
10244
12637
|
function newCredentials() {
|
|
10245
|
-
return { user: "agentbox", password:
|
|
12638
|
+
return { user: "agentbox", password: randomBytes11(24).toString("base64url") };
|
|
10246
12639
|
}
|
|
10247
12640
|
function basicMatches(header, want) {
|
|
10248
12641
|
const m = /^Basic\s+([A-Za-z0-9+/=]+)\s*$/i.exec(header ?? "");
|
|
@@ -10255,7 +12648,7 @@ var DavFront = class {
|
|
|
10255
12648
|
constructor(client, opts = {}) {
|
|
10256
12649
|
this.client = client;
|
|
10257
12650
|
this.opts = opts;
|
|
10258
|
-
this.secret = opts.secret ??
|
|
12651
|
+
this.secret = opts.secret ?? randomBytes11(16).toString("base64url");
|
|
10259
12652
|
this.credentials = opts.credentials === void 0 ? newCredentials() : opts.credentials;
|
|
10260
12653
|
}
|
|
10261
12654
|
client;
|
|
@@ -10433,7 +12826,7 @@ function encodeQuery(query, rawQuery) {
|
|
|
10433
12826
|
for (const [k, v] of Object.entries(query ?? {})) {
|
|
10434
12827
|
if (v === void 0) continue;
|
|
10435
12828
|
const values = Array.isArray(v) ? v : [String(v)];
|
|
10436
|
-
for (const
|
|
12829
|
+
for (const one2 of values) parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(one2)}`);
|
|
10437
12830
|
}
|
|
10438
12831
|
if (rawQuery) parts.push(rawQuery);
|
|
10439
12832
|
return parts.length ? `?${parts.join("&")}` : "";
|
|
@@ -10487,7 +12880,7 @@ var BoxClient = class {
|
|
|
10487
12880
|
get hasToken() {
|
|
10488
12881
|
return this.token !== null;
|
|
10489
12882
|
}
|
|
10490
|
-
request(method,
|
|
12883
|
+
request(method, path33, opts = {}) {
|
|
10491
12884
|
const headers = { ...this.baseHeaders(opts.auth !== false), ...opts.headers ?? {} };
|
|
10492
12885
|
let payload = null;
|
|
10493
12886
|
const body = opts.body;
|
|
@@ -10502,7 +12895,7 @@ var BoxClient = class {
|
|
|
10502
12895
|
}
|
|
10503
12896
|
if (Buffer.isBuffer(payload)) headers["content-length"] = String(payload.length);
|
|
10504
12897
|
}
|
|
10505
|
-
const target = `${
|
|
12898
|
+
const target = `${path33}${encodeQuery(opts.query, opts.rawQuery)}`;
|
|
10506
12899
|
const lib = this.url.protocol === "https:" ? https : http3;
|
|
10507
12900
|
return new Promise((resolve, reject) => {
|
|
10508
12901
|
const req = lib.request(
|
|
@@ -10548,21 +12941,21 @@ var BoxClient = class {
|
|
|
10548
12941
|
* A JSON call: the parsed answer on 2xx, else an {@link ApiError} with what
|
|
10549
12942
|
* the box said. `what` names the action for the message ("listing /x").
|
|
10550
12943
|
*/
|
|
10551
|
-
async json(method,
|
|
10552
|
-
const res = await this.request(method,
|
|
12944
|
+
async json(method, path33, opts = {}) {
|
|
12945
|
+
const res = await this.request(method, path33, { ...opts, headers: { accept: "application/json", ...opts.headers ?? {} } });
|
|
10553
12946
|
const text = await res.text();
|
|
10554
|
-
if (res.status < 200 || res.status >= 300) throw apiErrorFrom(res.status, text, opts.what ?? `${method} ${
|
|
12947
|
+
if (res.status < 200 || res.status >= 300) throw apiErrorFrom(res.status, text, opts.what ?? `${method} ${path33}`);
|
|
10555
12948
|
if (res.status === 204 || text === "") return void 0;
|
|
10556
12949
|
try {
|
|
10557
12950
|
return JSON.parse(text);
|
|
10558
12951
|
} catch {
|
|
10559
|
-
throw new CliError(`${opts.what ??
|
|
12952
|
+
throw new CliError(`${opts.what ?? path33}: the box answered with something that is not JSON (is ${this.origin} an agentbox?)`);
|
|
10560
12953
|
}
|
|
10561
12954
|
}
|
|
10562
12955
|
/** A request whose answer must be 2xx; the body is left to the caller. */
|
|
10563
|
-
async ok(method,
|
|
10564
|
-
const res = await this.request(method,
|
|
10565
|
-
if (res.status < 200 || res.status >= 300) throw apiErrorFrom(res.status, await res.text(), opts.what ?? `${method} ${
|
|
12956
|
+
async ok(method, path33, opts = {}) {
|
|
12957
|
+
const res = await this.request(method, path33, opts);
|
|
12958
|
+
if (res.status < 200 || res.status >= 300) throw apiErrorFrom(res.status, await res.text(), opts.what ?? `${method} ${path33}`);
|
|
10566
12959
|
return res;
|
|
10567
12960
|
}
|
|
10568
12961
|
close() {
|
|
@@ -10576,11 +12969,11 @@ async function gvfsFolder(root, port, secret, ms) {
|
|
|
10576
12969
|
for (; ; ) {
|
|
10577
12970
|
let names = [];
|
|
10578
12971
|
try {
|
|
10579
|
-
names =
|
|
12972
|
+
names = fs23.readdirSync(root);
|
|
10580
12973
|
} catch {
|
|
10581
12974
|
}
|
|
10582
12975
|
const name = gvfsMountName(names, port, secret);
|
|
10583
|
-
if (name) return
|
|
12976
|
+
if (name) return path30.join(root, name);
|
|
10584
12977
|
if (Date.now() > deadline) return null;
|
|
10585
12978
|
await new Promise((r) => setTimeout(r, 100));
|
|
10586
12979
|
}
|
|
@@ -10649,14 +13042,14 @@ password ${credentials.password}
|
|
|
10649
13042
|
throw new CliError(`${plan.helper} was not found, so the box cannot be mounted here: ${plan.install}. Or serve it with \`agentbox mount --no-mount\` and use any WebDAV client.`);
|
|
10650
13043
|
}
|
|
10651
13044
|
let madeMountpoint = false;
|
|
10652
|
-
if (plan.mountpoint && !
|
|
10653
|
-
|
|
13045
|
+
if (plan.mountpoint && !fs23.existsSync(plan.mountpoint)) {
|
|
13046
|
+
fs23.mkdirSync(plan.mountpoint, { recursive: true });
|
|
10654
13047
|
madeMountpoint = true;
|
|
10655
13048
|
}
|
|
10656
|
-
if (plan.link &&
|
|
13049
|
+
if (plan.link && fs23.existsSync(plan.link)) throw new CliError(`${plan.link} already exists; name a new folder for the link, or leave it out`);
|
|
10657
13050
|
const mounted = await runStep(plan.mount, helper, ctx.env);
|
|
10658
13051
|
if (mounted.code !== 0) {
|
|
10659
|
-
if (madeMountpoint)
|
|
13052
|
+
if (madeMountpoint) fs23.rmSync(plan.mountpoint, { recursive: false, force: true });
|
|
10660
13053
|
throw new CliError(`${plan.helper} could not mount it${mounted.output ? `: ${mounted.output}` : ""}`);
|
|
10661
13054
|
}
|
|
10662
13055
|
let where = plan.where;
|
|
@@ -10678,7 +13071,7 @@ password ${credentials.password}
|
|
|
10678
13071
|
}
|
|
10679
13072
|
let linked = false;
|
|
10680
13073
|
if (plan.link && where) {
|
|
10681
|
-
|
|
13074
|
+
fs23.symlinkSync(where, plan.link, "dir");
|
|
10682
13075
|
linked = true;
|
|
10683
13076
|
}
|
|
10684
13077
|
if (!gioOnly) {
|
|
@@ -10693,10 +13086,10 @@ password ${credentials.password}
|
|
|
10693
13086
|
const done = await runStep(unmount, findOnPath(unmount.command, ctx.env, ctx.platform) ?? unmount.command, ctx.env);
|
|
10694
13087
|
if (done.code !== 0) ctx.warn(`${unmount.command} ${unmount.args.join(" ")} failed${done.output ? `: ${done.output}` : ""}; unmount it yourself`);
|
|
10695
13088
|
}
|
|
10696
|
-
if (linked)
|
|
13089
|
+
if (linked) fs23.rmSync(plan.link, { force: true });
|
|
10697
13090
|
if (madeMountpoint) {
|
|
10698
13091
|
try {
|
|
10699
|
-
|
|
13092
|
+
fs23.rmdirSync(plan.mountpoint);
|
|
10700
13093
|
} catch {
|
|
10701
13094
|
}
|
|
10702
13095
|
}
|
|
@@ -10708,12 +13101,12 @@ password ${credentials.password}
|
|
|
10708
13101
|
var MOUNT_COMMANDS = [mount];
|
|
10709
13102
|
|
|
10710
13103
|
// src/commands/terminal.ts
|
|
10711
|
-
import
|
|
13104
|
+
import fs24 from "node:fs";
|
|
10712
13105
|
import os13 from "node:os";
|
|
10713
|
-
import
|
|
13106
|
+
import path31 from "node:path";
|
|
10714
13107
|
|
|
10715
13108
|
// src/ttyd.ts
|
|
10716
|
-
import { EventEmitter as
|
|
13109
|
+
import { EventEmitter as EventEmitter4 } from "node:events";
|
|
10717
13110
|
var CLIENT = { INPUT: 48, RESIZE: 49, PAUSE: 50, RESUME: 51 };
|
|
10718
13111
|
var SERVER = { OUTPUT: 48, TITLE: 49, PREFERENCES: 50 };
|
|
10719
13112
|
function handshakeFrame(columns, rows, authToken = "") {
|
|
@@ -10748,7 +13141,7 @@ function parseServerFrame(frame) {
|
|
|
10748
13141
|
return { kind: "unknown", command: command2 ?? -1 };
|
|
10749
13142
|
}
|
|
10750
13143
|
}
|
|
10751
|
-
var TtydSession = class extends
|
|
13144
|
+
var TtydSession = class extends EventEmitter4 {
|
|
10752
13145
|
constructor(client, basePath) {
|
|
10753
13146
|
super();
|
|
10754
13147
|
this.client = client;
|
|
@@ -11083,13 +13476,13 @@ function runTerminal(opts) {
|
|
|
11083
13476
|
opts.onCrash ?? defaultCrash(stderr)
|
|
11084
13477
|
);
|
|
11085
13478
|
const held = [];
|
|
11086
|
-
const
|
|
13479
|
+
const send2 = (data) => {
|
|
11087
13480
|
if (started) session.input(data);
|
|
11088
13481
|
else held.push(data);
|
|
11089
13482
|
};
|
|
11090
13483
|
const onInput = (chunk) => {
|
|
11091
13484
|
const { forward: forward2, detach } = filter.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
11092
|
-
if (forward2.length)
|
|
13485
|
+
if (forward2.length) send2(forward2);
|
|
11093
13486
|
if (detach) session.detach();
|
|
11094
13487
|
};
|
|
11095
13488
|
const onResize = () => {
|
|
@@ -11243,7 +13636,7 @@ the network: faster, and no redraw glitches. Install it now with herdr's install
|
|
|
11243
13636
|
return null;
|
|
11244
13637
|
}
|
|
11245
13638
|
const home2 = (win ? ctx.env.USERPROFILE : ctx.env.HOME) || os13.homedir();
|
|
11246
|
-
return findOnPath("herdr", ctx.env, ctx.platform) ?? [
|
|
13639
|
+
return findOnPath("herdr", ctx.env, ctx.platform) ?? [path31.join(home2, ".local", "bin", "herdr")].find((f) => fs24.existsSync(f)) ?? null;
|
|
11247
13640
|
}
|
|
11248
13641
|
var attach = command({
|
|
11249
13642
|
path: ["attach"],
|
|
@@ -11294,10 +13687,10 @@ var TERMINAL_COMMANDS = [attach, shell];
|
|
|
11294
13687
|
|
|
11295
13688
|
// src/commands/update.ts
|
|
11296
13689
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
11297
|
-
import { randomBytes as
|
|
11298
|
-
import
|
|
13690
|
+
import { randomBytes as randomBytes12 } from "node:crypto";
|
|
13691
|
+
import fs25 from "node:fs";
|
|
11299
13692
|
import os14 from "node:os";
|
|
11300
|
-
import
|
|
13693
|
+
import path32 from "node:path";
|
|
11301
13694
|
var MAX_BUNDLE = 16 * 1024 * 1024;
|
|
11302
13695
|
function bundleVersion(bundle) {
|
|
11303
13696
|
const head = (typeof bundle === "string" ? bundle : bundle.subarray(0, 512).toString("utf8")).split("\n", 3);
|
|
@@ -11310,17 +13703,17 @@ function selfInstall(argv1) {
|
|
|
11310
13703
|
if (!argv1) return null;
|
|
11311
13704
|
let file;
|
|
11312
13705
|
try {
|
|
11313
|
-
file =
|
|
13706
|
+
file = fs25.realpathSync(argv1);
|
|
11314
13707
|
} catch {
|
|
11315
13708
|
return null;
|
|
11316
13709
|
}
|
|
11317
13710
|
let checkout = false;
|
|
11318
13711
|
let npm = false;
|
|
11319
13712
|
try {
|
|
11320
|
-
const root =
|
|
11321
|
-
const pkg = JSON.parse(
|
|
13713
|
+
const root = path32.join(path32.dirname(file), "..");
|
|
13714
|
+
const pkg = JSON.parse(fs25.readFileSync(path32.join(root, "package.json"), "utf8"));
|
|
11322
13715
|
if (pkg.name === NPM_PACKAGE) {
|
|
11323
|
-
if (
|
|
13716
|
+
if (fs25.existsSync(path32.join(root, "src", "cli.ts"))) checkout = true;
|
|
11324
13717
|
else npm = true;
|
|
11325
13718
|
}
|
|
11326
13719
|
} catch {
|
|
@@ -11334,13 +13727,13 @@ function npmVersionFor(boxVersion) {
|
|
|
11334
13727
|
function nodeCredentials(env, platform = process.platform) {
|
|
11335
13728
|
const candidates = [];
|
|
11336
13729
|
if (platform === "win32") {
|
|
11337
|
-
if (env.LOCALAPPDATA &&
|
|
13730
|
+
if (env.LOCALAPPDATA && path32.win32.isAbsolute(env.LOCALAPPDATA)) candidates.push(path32.join(env.LOCALAPPDATA, "agentbox", "node", "credentials.json"));
|
|
11338
13731
|
} else {
|
|
11339
13732
|
const xdg = env.XDG_DATA_HOME;
|
|
11340
|
-
if (xdg &&
|
|
11341
|
-
candidates.push(
|
|
13733
|
+
if (xdg && path32.isAbsolute(xdg)) candidates.push(path32.join(xdg, "agentbox", "node", "credentials.json"));
|
|
13734
|
+
candidates.push(path32.join(env.HOME || os14.homedir(), ".local", "share", "agentbox", "node", "credentials.json"));
|
|
11342
13735
|
}
|
|
11343
|
-
return candidates.find((f) =>
|
|
13736
|
+
return candidates.find((f) => fs25.existsSync(f)) ?? null;
|
|
11344
13737
|
}
|
|
11345
13738
|
function signedUpdatesOnly(env, platform = process.platform, standalone = STANDALONE) {
|
|
11346
13739
|
const creds = nodeCredentials(env, platform);
|
|
@@ -11408,7 +13801,7 @@ var update = command({
|
|
|
11408
13801
|
}
|
|
11409
13802
|
return;
|
|
11410
13803
|
}
|
|
11411
|
-
const current =
|
|
13804
|
+
const current = fs25.readFileSync(self.file);
|
|
11412
13805
|
if (bundle.equals(current)) {
|
|
11413
13806
|
ctx.out(`Already up to date: agentbox ${VERSION}, the same build ${name} serves.
|
|
11414
13807
|
`);
|
|
@@ -11419,24 +13812,24 @@ var update = command({
|
|
|
11419
13812
|
`);
|
|
11420
13813
|
return;
|
|
11421
13814
|
}
|
|
11422
|
-
const dir =
|
|
11423
|
-
const tmp =
|
|
13815
|
+
const dir = path32.dirname(self.file);
|
|
13816
|
+
const tmp = path32.join(dir, `.agentbox-update-${randomBytes12(4).toString("hex")}.mjs`);
|
|
11424
13817
|
try {
|
|
11425
|
-
|
|
11426
|
-
|
|
13818
|
+
fs25.writeFileSync(tmp, bundle, { mode: 493 });
|
|
13819
|
+
fs25.chmodSync(tmp, 493);
|
|
11427
13820
|
} catch (err) {
|
|
11428
|
-
|
|
13821
|
+
fs25.rmSync(tmp, { force: true });
|
|
11429
13822
|
throw new CliError(`cannot write to ${dir} (${err.message}); reinstall with the box's install script`);
|
|
11430
13823
|
}
|
|
11431
13824
|
const check = spawnSync3(process.execPath, [tmp, "--version"], { encoding: "utf8", timeout: 3e4 });
|
|
11432
13825
|
if (check.status !== 0) {
|
|
11433
|
-
|
|
13826
|
+
fs25.rmSync(tmp, { force: true });
|
|
11434
13827
|
throw new CliError(`the downloaded build does not run here: ${(check.stderr || check.stdout || "").trim().slice(0, 300)}`);
|
|
11435
13828
|
}
|
|
11436
13829
|
try {
|
|
11437
|
-
|
|
13830
|
+
fs25.renameSync(tmp, self.file);
|
|
11438
13831
|
} catch (err) {
|
|
11439
|
-
|
|
13832
|
+
fs25.rmSync(tmp, { force: true });
|
|
11440
13833
|
throw new CliError(`cannot replace ${self.file}: ${err.message}`);
|
|
11441
13834
|
}
|
|
11442
13835
|
ctx.out(`Updated agentbox ${VERSION} \u2192 ${safeText(version)} from ${name}.
|
|
@@ -11461,8 +13854,8 @@ var COMMANDS2 = [
|
|
|
11461
13854
|
...UPDATE_COMMANDS
|
|
11462
13855
|
];
|
|
11463
13856
|
function optionLine(o) {
|
|
11464
|
-
const
|
|
11465
|
-
return ` ${
|
|
13857
|
+
const flag2 = `${o.short ? `-${o.short}, ` : " "}--${o.name}${o.value ? ` <${o.value}>` : ""}`;
|
|
13858
|
+
return ` ${flag2.padEnd(26)} ${o.description}`;
|
|
11466
13859
|
}
|
|
11467
13860
|
function mainHelp() {
|
|
11468
13861
|
const width2 = Math.max(...COMMANDS2.filter((c) => !c.hidden).map((c) => c.path.join(" ").length));
|