@x-tech-laboratories/x-libs 0.14.0 → 0.14.1
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/web/react/menus.js +11 -7
- package/package.json +1 -1
- package/dist/agent/client.d.ts +0 -32
- package/dist/agent/client.js +0 -148
- package/dist/agent/commands.d.ts +0 -23
- package/dist/agent/commands.js +0 -118
- package/dist/agent/keys.d.ts +0 -14
- package/dist/agent/keys.js +0 -81
- package/dist/agent/long_runs.d.ts +0 -32
- package/dist/agent/long_runs.js +0 -316
- package/dist/agent/protocol.d.ts +0 -148
- package/dist/agent/protocol.js +0 -131
- package/dist/agent/session.d.ts +0 -50
- package/dist/agent/session.js +0 -225
- package/dist/agent/tasks.d.ts +0 -17
- package/dist/agent/tasks.js +0 -15
- package/dist/backups.d.ts +0 -31
- package/dist/backups.js +0 -43
- package/dist/dates.d.ts +0 -9
- package/dist/dates.js +0 -60
- package/dist/durations.d.ts +0 -11
- package/dist/durations.js +0 -49
- package/dist/electron/app.d.ts +0 -13
- package/dist/electron/app.js +0 -27
- package/dist/electron/bridge.d.ts +0 -55
- package/dist/electron/bridge.js +0 -105
- package/dist/electron/events.d.ts +0 -21
- package/dist/electron/events.js +0 -25
- package/dist/electron/fetch.d.ts +0 -52
- package/dist/electron/fetch.js +0 -175
- package/dist/electron/window.d.ts +0 -96
- package/dist/electron/window.js +0 -128
- package/dist/log.d.ts +0 -30
- package/dist/log.js +0 -79
- package/dist/server/agents/agent.d.ts +0 -27
- package/dist/server/agents/agent.js +0 -16
- package/dist/server/agents/connection.d.ts +0 -51
- package/dist/server/agents/connection.js +0 -239
- package/dist/server/agents/hub.d.ts +0 -86
- package/dist/server/agents/hub.js +0 -241
- package/dist/server/agents/local.d.ts +0 -24
- package/dist/server/agents/local.js +0 -62
- package/dist/server/agents/route.d.ts +0 -3
- package/dist/server/agents/route.js +0 -24
- package/dist/server/backup_files.d.ts +0 -3
- package/dist/server/backup_files.js +0 -25
- package/dist/server/config.d.ts +0 -39
- package/dist/server/config.js +0 -109
- package/dist/server/cors.d.ts +0 -8
- package/dist/server/cors.js +0 -13
- package/dist/server/events.d.ts +0 -33
- package/dist/server/events.js +0 -68
- package/dist/server/hash_password.d.ts +0 -1
- package/dist/server/hash_password.js +0 -46
- package/dist/server/login_page.d.ts +0 -8
- package/dist/server/login_page.js +0 -79
- package/dist/server/passwords.d.ts +0 -8
- package/dist/server/passwords.js +0 -59
- package/dist/server/replies.d.ts +0 -12
- package/dist/server/replies.js +0 -41
- package/dist/server/sign_in.d.ts +0 -27
- package/dist/server/sign_in.js +0 -95
- package/dist/server/web_folder.d.ts +0 -11
- package/dist/server/web_folder.js +0 -40
- package/dist/web/dom.d.ts +0 -7
- package/dist/web/dom.js +0 -32
- package/dist/web/drag.d.ts +0 -3
- package/dist/web/drag.js +0 -13
- package/dist/web/errors.d.ts +0 -1
- package/dist/web/errors.js +0 -27
- package/dist/web/format.d.ts +0 -11
- package/dist/web/format.js +0 -82
- package/dist/web/menus.d.ts +0 -17
- package/dist/web/menus.js +0 -40
- package/dist/web/prefs.d.ts +0 -12
- package/dist/web/prefs.js +0 -62
- package/dist/web/react/badges.d.ts +0 -35
- package/dist/web/react/badges.js +0 -32
- package/dist/web/react/controls.d.ts +0 -86
- package/dist/web/react/controls.js +0 -154
- package/dist/web/react/dialog.d.ts +0 -45
- package/dist/web/react/dialog.js +0 -82
- package/dist/web/react/following.d.ts +0 -25
- package/dist/web/react/following.js +0 -34
- package/dist/web/react/layout.d.ts +0 -111
- package/dist/web/react/layout.js +0 -85
- package/dist/web/react/list_picker.d.ts +0 -19
- package/dist/web/react/list_picker.js +0 -50
- package/dist/web/react/markdown.d.ts +0 -8
- package/dist/web/react/markdown.js +0 -40
- package/dist/web/react/media.d.ts +0 -24
- package/dist/web/react/media.js +0 -18
- package/dist/web/react/menu_button.d.ts +0 -10
- package/dist/web/react/menu_button.js +0 -16
- package/dist/web/react/menus.d.ts +0 -31
- package/dist/web/react/page_view.d.ts +0 -16
- package/dist/web/react/page_view.js +0 -6
- package/dist/web/react/prefs.d.ts +0 -2
- package/dist/web/react/prefs.js +0 -11
- package/dist/web/react/progress.d.ts +0 -10
- package/dist/web/react/progress.js +0 -9
- package/dist/web/react/rating.d.ts +0 -8
- package/dist/web/react/rating.js +0 -23
- package/dist/web/react/reveal.d.ts +0 -10
- package/dist/web/react/reveal.js +0 -30
- package/dist/web/react/shell.d.ts +0 -46
- package/dist/web/react/shell.js +0 -94
- package/dist/web/react/shortcuts.d.ts +0 -23
- package/dist/web/react/shortcuts.js +0 -95
- package/dist/web/react/split.d.ts +0 -23
- package/dist/web/react/split.js +0 -94
- package/dist/web/react/states.d.ts +0 -36
- package/dist/web/react/states.js +0 -64
- package/dist/web/react/store.d.ts +0 -4
- package/dist/web/react/store.js +0 -7
- package/dist/web/react/suggestions.d.ts +0 -23
- package/dist/web/react/suggestions.js +0 -57
- package/dist/web/react/table.d.ts +0 -78
- package/dist/web/react/table.js +0 -404
- package/dist/web/react/test_stubs.d.ts +0 -1
- package/dist/web/react/test_stubs.js +0 -25
- package/dist/web/react/time.d.ts +0 -5
- package/dist/web/react/time.js +0 -19
- package/dist/web/react/toasts.d.ts +0 -29
- package/dist/web/react/toasts.js +0 -72
- package/dist/web/react/tree.d.ts +0 -28
- package/dist/web/react/tree.js +0 -183
- package/dist/web/react/volume.d.ts +0 -8
- package/dist/web/react/volume.js +0 -42
- package/dist/web/react/watch.d.ts +0 -3
- package/dist/web/react/watch.js +0 -5
- package/dist/web/router.d.ts +0 -7
- package/dist/web/router.js +0 -29
- package/dist/web/selection.d.ts +0 -25
- package/dist/web/selection.js +0 -106
- package/dist/web/split.d.ts +0 -15
- package/dist/web/split.js +0 -104
- package/dist/web/states.d.ts +0 -3
- package/dist/web/states.js +0 -10
- package/dist/web/store.d.ts +0 -11
- package/dist/web/store.js +0 -17
- package/dist/web/tables.d.ts +0 -28
- package/dist/web/tables.js +0 -88
- package/dist/web/theme.d.ts +0 -14
- package/dist/web/theme.js +0 -52
- package/dist/web/tree.d.ts +0 -40
- package/dist/web/tree.js +0 -130
- package/dist/web/virtual.d.ts +0 -7
- package/dist/web/virtual.js +0 -10
- package/dist/web/watch.d.ts +0 -11
- package/dist/web/watch.js +0 -37
package/dist/web/react/menus.js
CHANGED
|
@@ -2,8 +2,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
// Menus and popovers on the native popover: closed by Escape or a click elsewhere, and here also by scrolling or leaving the window.
|
|
3
3
|
import { Fragment, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
4
4
|
import { item_after_key, item_starting_with, on_screen_position } from "../menus.js";
|
|
5
|
-
// Shows the element as a popover while it is rendered, calling the latest on_close once it closes for any reason.
|
|
6
|
-
function use_popover(on_close, opener) {
|
|
5
|
+
// Shows the element as a popover while it is rendered, calling the latest on_close once it closes for any reason; a manual one closes itself on a press outside it.
|
|
6
|
+
function use_popover(on_close, opener, manual = false) {
|
|
7
7
|
const element = useRef(null);
|
|
8
8
|
const close = useRef(on_close);
|
|
9
9
|
close.current = on_close;
|
|
@@ -14,15 +14,18 @@ function use_popover(on_close, opener) {
|
|
|
14
14
|
popover.hidePopover(); };
|
|
15
15
|
const toggled = (event) => { if (event.newState === "closed")
|
|
16
16
|
close.current(); };
|
|
17
|
-
const
|
|
17
|
+
const outside = (event) => { if (!popover.contains(event.target))
|
|
18
18
|
hide(); };
|
|
19
19
|
popover.addEventListener("toggle", toggled);
|
|
20
20
|
window.addEventListener("blur", hide);
|
|
21
|
-
window.addEventListener("scroll",
|
|
21
|
+
window.addEventListener("scroll", outside, true);
|
|
22
|
+
if (manual)
|
|
23
|
+
window.addEventListener("pointerdown", outside, true);
|
|
22
24
|
return () => {
|
|
23
25
|
popover.removeEventListener("toggle", toggled);
|
|
24
26
|
window.removeEventListener("blur", hide);
|
|
25
|
-
window.removeEventListener("scroll",
|
|
27
|
+
window.removeEventListener("scroll", outside, true);
|
|
28
|
+
window.removeEventListener("pointerdown", outside, true);
|
|
26
29
|
};
|
|
27
30
|
}, []);
|
|
28
31
|
const return_focus = () => {
|
|
@@ -126,14 +129,15 @@ export function MenuList({ items, on_close }) {
|
|
|
126
129
|
item.run();
|
|
127
130
|
}, children: [_jsx("span", { className: "menu-label", children: item.label }), item.hint && _jsx("span", { className: "menu-hint", children: item.hint })] })] }, item.label))) }));
|
|
128
131
|
}
|
|
132
|
+
// Manual, since on Linux the menu opens on the button's press and an auto popover would close on its release.
|
|
129
133
|
export function ContextMenu({ x, y, items, on_close, label }) {
|
|
130
|
-
const { element: menu, dismiss, return_focus } = use_popover(on_close);
|
|
134
|
+
const { element: menu, dismiss, return_focus } = use_popover(on_close, undefined, true);
|
|
131
135
|
const keys = use_menu_keys(menu, dismiss, true);
|
|
132
136
|
useLayoutEffect(() => {
|
|
133
137
|
show_at(menu.current, x, y);
|
|
134
138
|
menu.current.querySelector(item_selector)?.focus();
|
|
135
139
|
}, [x, y]);
|
|
136
|
-
return (_jsx("div", { ref: menu, popover: "
|
|
140
|
+
return (_jsx("div", { ref: menu, popover: "manual", className: "menu", role: "menu", "aria-label": label, tabIndex: -1, onKeyDown: keys, children: _jsx(MenuList, { items: items, on_close: () => { return_focus(); on_close(); } }) }));
|
|
137
141
|
}
|
|
138
142
|
let open_menu = null;
|
|
139
143
|
let next_menu_id = 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x-tech-laboratories/x-libs",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Code the X-apps share: agents and their long runs, passwords and sign-in, the Fastify server pieces, durations, and the web page's helpers and widgets",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
package/dist/agent/client.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { KeyObject } from "node:crypto";
|
|
2
|
-
import { LongRuns, type LongRunOptions } from "./long_runs.ts";
|
|
3
|
-
import { type AgentApp } from "./protocol.ts";
|
|
4
|
-
import { AgentSession, type SessionHooks } from "./session.ts";
|
|
5
|
-
import type { TaskRunner } from "./tasks.ts";
|
|
6
|
-
export interface AgentConfig<Task> extends SessionHooks<Task> {
|
|
7
|
-
app: AgentApp<Task>;
|
|
8
|
-
runner: TaskRunner<Task>;
|
|
9
|
-
url: string;
|
|
10
|
-
key_file?: string;
|
|
11
|
-
token_file?: string;
|
|
12
|
-
state_directory: string;
|
|
13
|
-
host: string;
|
|
14
|
-
agent_version: string;
|
|
15
|
-
heartbeat_in_milliseconds?: number;
|
|
16
|
-
backoff_initial_in_milliseconds?: number;
|
|
17
|
-
backoff_longest_in_milliseconds?: number;
|
|
18
|
-
long_runs?: LongRunOptions;
|
|
19
|
-
log?: (line: string) => void;
|
|
20
|
-
}
|
|
21
|
-
export declare class AgentConfigError extends Error {
|
|
22
|
-
constructor(message: string);
|
|
23
|
-
}
|
|
24
|
-
export declare function check_url(text: string): URL;
|
|
25
|
-
export declare function read_key_file(file: string): KeyObject;
|
|
26
|
-
export interface RunningAgent<Task> {
|
|
27
|
-
readonly runs: LongRuns<Task>;
|
|
28
|
-
connected(): boolean;
|
|
29
|
-
session(): AgentSession<Task> | null;
|
|
30
|
-
stop(): Promise<void>;
|
|
31
|
-
}
|
|
32
|
-
export declare function start_agent<Task>(config: AgentConfig<Task>): RunningAgent<Task>;
|
package/dist/agent/client.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
// The agent's connection to the server: dials out over a WebSocket with its host's key or token, hands the connection
|
|
2
|
-
// to a session, and dials again with a growing, jittered wait whenever it drops.
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
|
-
import { AgentKeyError, parse_private_key } from "./keys.js";
|
|
5
|
-
import { LongRuns } from "./long_runs.js";
|
|
6
|
-
import { key_host_header } from "./protocol.js";
|
|
7
|
-
import { AgentSession } from "./session.js";
|
|
8
|
-
export class AgentConfigError extends Error {
|
|
9
|
-
constructor(message) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.name = "AgentConfigError";
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
const local_names = new Set(["localhost", "[::1]", "::1"]);
|
|
15
|
-
// ── Checks ───────────────────────────────────────────────────────────────────
|
|
16
|
-
// The server's URL, which must be wss:// so secrets and output travel encrypted, unless it is this machine.
|
|
17
|
-
export function check_url(text) {
|
|
18
|
-
let url;
|
|
19
|
-
try {
|
|
20
|
-
url = new URL(text);
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
throw new AgentConfigError(`'${text}' is not a URL.`);
|
|
24
|
-
}
|
|
25
|
-
if (url.protocol === "wss:")
|
|
26
|
-
return url;
|
|
27
|
-
if (url.protocol !== "ws:")
|
|
28
|
-
throw new AgentConfigError(`The server URL must start with wss://, not ${url.protocol}//.`);
|
|
29
|
-
const local = local_names.has(url.hostname) || /^127\.\d+\.\d+\.\d+$/.test(url.hostname);
|
|
30
|
-
if (!local)
|
|
31
|
-
throw new AgentConfigError(`ws:// is unencrypted, so it is only allowed to this machine; use wss://${url.host}${url.pathname}.`);
|
|
32
|
-
return url;
|
|
33
|
-
}
|
|
34
|
-
function login_of(config) {
|
|
35
|
-
const has_key = Boolean(config.key_file);
|
|
36
|
-
const has_token = Boolean(config.token_file);
|
|
37
|
-
if (has_key === has_token)
|
|
38
|
-
throw new AgentConfigError("Give exactly one of a key file and a token file.");
|
|
39
|
-
return has_key ? { kind: "key", file: config.key_file } : { kind: "token", file: config.token_file };
|
|
40
|
-
}
|
|
41
|
-
// The key in the file, or an AgentConfigError naming the file for one that is not a usable key.
|
|
42
|
-
export function read_key_file(file) {
|
|
43
|
-
const text = readFileSync(file, "utf8");
|
|
44
|
-
try {
|
|
45
|
-
return parse_private_key(text);
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
if (error instanceof AgentKeyError)
|
|
49
|
-
throw new AgentConfigError(`The key file ${file} ${error.message}`);
|
|
50
|
-
throw error;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
export function start_agent(config) {
|
|
54
|
-
const url = check_url(config.url);
|
|
55
|
-
const login = login_of(config);
|
|
56
|
-
// A key that is there but unusable stops the agent at once; one not there yet, as before secrets are in place, is retried.
|
|
57
|
-
if (login.kind === "key") {
|
|
58
|
-
try {
|
|
59
|
-
read_key_file(login.file);
|
|
60
|
-
}
|
|
61
|
-
catch (error) {
|
|
62
|
-
if (error instanceof AgentConfigError)
|
|
63
|
-
throw error;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
const runs = new LongRuns(config.state_directory, config.runner, config.long_runs);
|
|
67
|
-
const log = config.log ?? ((line) => console.log(line));
|
|
68
|
-
const heartbeat = config.heartbeat_in_milliseconds ?? 15_000;
|
|
69
|
-
const initial = config.backoff_initial_in_milliseconds ?? 1000;
|
|
70
|
-
const longest = config.backoff_longest_in_milliseconds ?? 60_000;
|
|
71
|
-
let socket = null;
|
|
72
|
-
let session = null;
|
|
73
|
-
let failures = 0;
|
|
74
|
-
let stopping = false;
|
|
75
|
-
let retry_timer;
|
|
76
|
-
let closed_resolve = null;
|
|
77
|
-
const dial_later = () => {
|
|
78
|
-
if (stopping)
|
|
79
|
-
return;
|
|
80
|
-
// Half the wait is fixed and half random, so hosts that lost the server together do not all return at once.
|
|
81
|
-
const wait = Math.min(longest, initial * 2 ** failures);
|
|
82
|
-
failures++;
|
|
83
|
-
retry_timer = setTimeout(dial, wait / 2 + Math.random() * (wait / 2));
|
|
84
|
-
};
|
|
85
|
-
const dial = () => {
|
|
86
|
-
if (stopping)
|
|
87
|
-
return;
|
|
88
|
-
let headers;
|
|
89
|
-
let key = null;
|
|
90
|
-
try {
|
|
91
|
-
if (login.kind === "key") {
|
|
92
|
-
key = read_key_file(login.file);
|
|
93
|
-
headers = { [key_host_header(config.app.name)]: config.host };
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
headers = { authorization: `Bearer ${readFileSync(login.file, "utf8").trim()}` };
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
catch (error) {
|
|
100
|
-
log(`Could not read the ${login.kind} file: ${error.message}`);
|
|
101
|
-
dial_later();
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
// Node's WebSocket takes headers, which the browser's type has no room for.
|
|
105
|
-
const current = new WebSocket(url, { headers });
|
|
106
|
-
current.binaryType = "arraybuffer";
|
|
107
|
-
socket = current;
|
|
108
|
-
current.addEventListener("open", () => {
|
|
109
|
-
failures = 0;
|
|
110
|
-
log(`Connected to ${url.origin}`);
|
|
111
|
-
session = new AgentSession(current, runs, {
|
|
112
|
-
app: config.app, runner: config.runner, host: config.host, agent_version: config.agent_version, heartbeat_in_milliseconds: heartbeat, key,
|
|
113
|
-
requests: config.requests, on_message: config.on_message, on_hello: config.on_hello,
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
current.addEventListener("message", (event) => {
|
|
117
|
-
if (typeof event.data === "string")
|
|
118
|
-
session?.receive(event.data);
|
|
119
|
-
});
|
|
120
|
-
current.addEventListener("error", () => { });
|
|
121
|
-
current.addEventListener("close", (event) => {
|
|
122
|
-
if (session !== null)
|
|
123
|
-
log(`Disconnected from ${url.origin} (${event.code}${event.reason ? `: ${event.reason}` : ""})`);
|
|
124
|
-
session?.close();
|
|
125
|
-
session = null;
|
|
126
|
-
socket = null;
|
|
127
|
-
if (closed_resolve !== null)
|
|
128
|
-
closed_resolve();
|
|
129
|
-
else
|
|
130
|
-
dial_later();
|
|
131
|
-
});
|
|
132
|
-
};
|
|
133
|
-
dial();
|
|
134
|
-
return {
|
|
135
|
-
runs,
|
|
136
|
-
connected: () => session !== null,
|
|
137
|
-
session: () => session,
|
|
138
|
-
stop: () => {
|
|
139
|
-
stopping = true;
|
|
140
|
-
clearTimeout(retry_timer);
|
|
141
|
-
if (socket === null)
|
|
142
|
-
return Promise.resolve();
|
|
143
|
-
const closing = new Promise((resolve) => (closed_resolve = resolve));
|
|
144
|
-
socket.close(1000, "The agent is stopping.");
|
|
145
|
-
return closing;
|
|
146
|
-
},
|
|
147
|
-
};
|
|
148
|
-
}
|
package/dist/agent/commands.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { Readable } from "node:stream";
|
|
2
|
-
import type { RunResult } from "./protocol.ts";
|
|
3
|
-
export interface RunOptions {
|
|
4
|
-
timeout_in_seconds: number;
|
|
5
|
-
stdin?: string | null;
|
|
6
|
-
}
|
|
7
|
-
export interface StreamOptions {
|
|
8
|
-
stdin?: string | null;
|
|
9
|
-
}
|
|
10
|
-
export interface CommandStream {
|
|
11
|
-
stdout: Readable;
|
|
12
|
-
done: Promise<{
|
|
13
|
-
exit_code: number;
|
|
14
|
-
stderr: string;
|
|
15
|
-
}>;
|
|
16
|
-
stop: () => void;
|
|
17
|
-
}
|
|
18
|
-
export declare const kill_grace_in_milliseconds = 5000;
|
|
19
|
-
export declare const run_stdout_limit_in_bytes: number;
|
|
20
|
-
export declare const stderr_limit_in_bytes: number;
|
|
21
|
-
export declare function signal_group(pid: number | undefined, signal: NodeJS.Signals): void;
|
|
22
|
-
export declare function run_command(command: string, options: RunOptions): Promise<RunResult>;
|
|
23
|
-
export declare function stream_command(command: string, options?: StreamOptions): CommandStream;
|
package/dist/agent/commands.js
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
// Runs one shell command on this machine, either to its end with its output kept or with its stdout handed back as it
|
|
2
|
-
// comes. Both the agent and the server's built-in agent run commands through here.
|
|
3
|
-
// ── Imports ──────────────────────────────────────────────────────────────────
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
-
// Give a stopped command this long to exit before it is killed outright.
|
|
6
|
-
export const kill_grace_in_milliseconds = 5000;
|
|
7
|
-
// A run keeps at most this much stdout, since it travels whole in one message.
|
|
8
|
-
export const run_stdout_limit_in_bytes = 32 * 1024 * 1024;
|
|
9
|
-
// Stderr is only ever an error message, so this much is plenty.
|
|
10
|
-
export const stderr_limit_in_bytes = 64 * 1024;
|
|
11
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
12
|
-
// Sends the signal to a process group, which may have ended already.
|
|
13
|
-
export function signal_group(pid, signal) {
|
|
14
|
-
if (pid === undefined)
|
|
15
|
-
return;
|
|
16
|
-
try {
|
|
17
|
-
process.kill(-pid, signal);
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
// The process group is already gone.
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
// Collects chunks up to a size, noting whether any were dropped.
|
|
24
|
-
class BoundedBuffer {
|
|
25
|
-
chunks = [];
|
|
26
|
-
size = 0;
|
|
27
|
-
truncated = false;
|
|
28
|
-
limit;
|
|
29
|
-
constructor(limit) {
|
|
30
|
-
this.limit = limit;
|
|
31
|
-
}
|
|
32
|
-
add(chunk) {
|
|
33
|
-
const room = this.limit - this.size;
|
|
34
|
-
if (room <= 0) {
|
|
35
|
-
this.truncated = true;
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
const kept = chunk.length > room ? chunk.subarray(0, room) : chunk;
|
|
39
|
-
if (kept !== chunk)
|
|
40
|
-
this.truncated = true;
|
|
41
|
-
this.chunks.push(kept);
|
|
42
|
-
this.size += kept.length;
|
|
43
|
-
}
|
|
44
|
-
text() {
|
|
45
|
-
return Buffer.concat(this.chunks).toString("utf8").trim();
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
// Its own process group, so stopping it stops the command's children too.
|
|
49
|
-
function spawn_shell(command, stdin) {
|
|
50
|
-
const child = spawn("sh", ["-c", command], { detached: true, stdio: [stdin == null ? "ignore" : "pipe", "pipe", "pipe"] });
|
|
51
|
-
if (stdin != null) {
|
|
52
|
-
child.stdin.on("error", () => { });
|
|
53
|
-
child.stdin.end(stdin);
|
|
54
|
-
}
|
|
55
|
-
return child;
|
|
56
|
-
}
|
|
57
|
-
// ── Running ──────────────────────────────────────────────────────────────────
|
|
58
|
-
export function run_command(command, options) {
|
|
59
|
-
return new Promise((resolve) => {
|
|
60
|
-
const child = spawn_shell(command, options.stdin);
|
|
61
|
-
const stdout = new BoundedBuffer(run_stdout_limit_in_bytes);
|
|
62
|
-
const stderr = new BoundedBuffer(stderr_limit_in_bytes);
|
|
63
|
-
let timed_out = false;
|
|
64
|
-
let kill_timer;
|
|
65
|
-
const timeout_timer = setTimeout(() => {
|
|
66
|
-
timed_out = true;
|
|
67
|
-
signal_group(child.pid, "SIGTERM");
|
|
68
|
-
kill_timer = setTimeout(() => signal_group(child.pid, "SIGKILL"), kill_grace_in_milliseconds);
|
|
69
|
-
}, options.timeout_in_seconds * 1000);
|
|
70
|
-
child.stdout.on("data", (chunk) => stdout.add(chunk));
|
|
71
|
-
child.stderr.on("data", (chunk) => stderr.add(chunk));
|
|
72
|
-
child.on("error", (error) => {
|
|
73
|
-
clearTimeout(timeout_timer);
|
|
74
|
-
clearTimeout(kill_timer);
|
|
75
|
-
resolve({ exit_code: -1, stdout: "", stderr: `Could not start sh: ${error.message}` });
|
|
76
|
-
});
|
|
77
|
-
child.on("close", (exit_code) => {
|
|
78
|
-
clearTimeout(timeout_timer);
|
|
79
|
-
clearTimeout(kill_timer);
|
|
80
|
-
if (timed_out) {
|
|
81
|
-
resolve({ exit_code: -1, stdout: stdout.text(), stderr: `Timed out after ${options.timeout_in_seconds}s` });
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
const note = stdout.truncated ? `Output cut at ${run_stdout_limit_in_bytes} bytes` : "";
|
|
85
|
-
resolve({ exit_code: exit_code ?? -1, stdout: stdout.text(), stderr: [stderr.text(), note].filter(Boolean).join("\n") });
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
// ── Streaming ────────────────────────────────────────────────────────────────
|
|
90
|
-
// Runs the command with no timeout and hands back its stdout as it comes, for output too big to hold.
|
|
91
|
-
export function stream_command(command, options = {}) {
|
|
92
|
-
const child = spawn_shell(command, options.stdin);
|
|
93
|
-
const stderr = new BoundedBuffer(stderr_limit_in_bytes);
|
|
94
|
-
let ended = false;
|
|
95
|
-
let kill_timer;
|
|
96
|
-
child.stderr.on("data", (chunk) => stderr.add(chunk));
|
|
97
|
-
const done = new Promise((resolve) => {
|
|
98
|
-
child.on("error", (error) => {
|
|
99
|
-
ended = true;
|
|
100
|
-
resolve({ exit_code: -1, stderr: `Could not start sh: ${error.message}` });
|
|
101
|
-
});
|
|
102
|
-
child.on("close", (exit_code) => {
|
|
103
|
-
ended = true;
|
|
104
|
-
clearTimeout(kill_timer);
|
|
105
|
-
resolve({ exit_code: exit_code ?? -1, stderr: stderr.text() });
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
const stop = () => {
|
|
109
|
-
if (ended)
|
|
110
|
-
return;
|
|
111
|
-
signal_group(child.pid, "SIGTERM");
|
|
112
|
-
// Output nobody reads any more is thrown away, so a full pipe cannot keep the command from ending.
|
|
113
|
-
child.stdout.unpipe();
|
|
114
|
-
child.stdout.destroy();
|
|
115
|
-
kill_timer ??= setTimeout(() => signal_group(child.pid, "SIGKILL"), kill_grace_in_milliseconds);
|
|
116
|
-
};
|
|
117
|
-
return { stdout: child.stdout, done, stop };
|
|
118
|
-
}
|
package/dist/agent/keys.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { type KeyObject } from "node:crypto";
|
|
2
|
-
export declare class AgentKeyError extends Error {
|
|
3
|
-
constructor(message: string);
|
|
4
|
-
}
|
|
5
|
-
export declare function challenge_text(app: string, host: string, nonce: string): Buffer;
|
|
6
|
-
export declare function sign_challenge(private_key: KeyObject, app: string, host: string, nonce: string): string;
|
|
7
|
-
export declare function verify_challenge(public_key: KeyObject, app: string, host: string, nonce: string, signature: string): boolean;
|
|
8
|
-
export declare function parse_public_key(line: string): KeyObject;
|
|
9
|
-
export declare function key_fingerprint(line: string): string;
|
|
10
|
-
export declare function parse_private_key(text: string): KeyObject;
|
|
11
|
-
export declare function generate_agent_key(comment: string): {
|
|
12
|
-
private_text: string;
|
|
13
|
-
public_line: string;
|
|
14
|
-
};
|
package/dist/agent/keys.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
// A host's agent key: an ed25519 key made by `ssh-keygen -t ed25519 -N ""`, whose private half the agent holds and
|
|
2
|
-
// whose public line the server's config declares. The agent proves it holds the key by signing the server's nonce.
|
|
3
|
-
// sshpk reads and writes the OpenSSH formats, which Node's crypto does not, and Node's crypto signs.
|
|
4
|
-
// ── Imports ──────────────────────────────────────────────────────────────────
|
|
5
|
-
import { createPrivateKey, createPublicKey, sign, verify } from "node:crypto";
|
|
6
|
-
import sshpk from "sshpk";
|
|
7
|
-
// ── Settings ─────────────────────────────────────────────────────────────────
|
|
8
|
-
export class AgentKeyError extends Error {
|
|
9
|
-
constructor(message) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.name = "AgentKeyError";
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
// ── Challenge ────────────────────────────────────────────────────────────────
|
|
15
|
-
// What the agent signs: the app, the protocol, the host it logs in as and the server's nonce, so a signature fits one
|
|
16
|
-
// login to one app only; `app` is the app's name, such as "x-nix".
|
|
17
|
-
export function challenge_text(app, host, nonce) {
|
|
18
|
-
return Buffer.from(`${app}-agent-v1|${host}|${nonce}`, "utf8");
|
|
19
|
-
}
|
|
20
|
-
export function sign_challenge(private_key, app, host, nonce) {
|
|
21
|
-
return sign(null, challenge_text(app, host, nonce), private_key).toString("base64");
|
|
22
|
-
}
|
|
23
|
-
export function verify_challenge(public_key, app, host, nonce, signature) {
|
|
24
|
-
try {
|
|
25
|
-
return verify(null, challenge_text(app, host, nonce), public_key, Buffer.from(signature, "base64"));
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
// ── Public key ───────────────────────────────────────────────────────────────
|
|
32
|
-
// Reads an OpenSSH public key line; the messages never quote the key itself.
|
|
33
|
-
export function parse_public_key(line) {
|
|
34
|
-
const parts = line.trim().split(/\s+/);
|
|
35
|
-
if (parts.length < 2 || parts[0] === "")
|
|
36
|
-
throw new AgentKeyError('must be an OpenSSH public key line such as "ssh-ed25519 AAAA... comment".');
|
|
37
|
-
if (parts[0] !== "ssh-ed25519")
|
|
38
|
-
throw new AgentKeyError(`must be an ssh-ed25519 key, not ${parts[0]}; make one with ssh-keygen -t ed25519.`);
|
|
39
|
-
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(parts[1]))
|
|
40
|
-
throw new AgentKeyError("holds a key that is not base64.");
|
|
41
|
-
let key;
|
|
42
|
-
try {
|
|
43
|
-
key = sshpk.parseKey(line.trim(), "ssh");
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
throw new AgentKeyError("is cut short or damaged.");
|
|
47
|
-
}
|
|
48
|
-
if (key.type !== "ed25519")
|
|
49
|
-
throw new AgentKeyError(`says ssh-ed25519 but holds a ${key.type} key.`);
|
|
50
|
-
return createPublicKey(key.toString("pkcs8"));
|
|
51
|
-
}
|
|
52
|
-
// The key's SHA256 fingerprint as ssh-keygen -l prints it.
|
|
53
|
-
export function key_fingerprint(line) {
|
|
54
|
-
return sshpk.parseKey(line.trim(), "ssh").fingerprint("sha256").toString();
|
|
55
|
-
}
|
|
56
|
-
// ── Private key ──────────────────────────────────────────────────────────────
|
|
57
|
-
// Reads an unencrypted OpenSSH ed25519 private key; the messages never quote the key itself.
|
|
58
|
-
export function parse_private_key(text) {
|
|
59
|
-
if (!/-{5}BEGIN OPENSSH PRIVATE KEY-{5}/.test(text))
|
|
60
|
-
throw new AgentKeyError('is not an OpenSSH private key; make one with ssh-keygen -t ed25519 -N "".');
|
|
61
|
-
let key;
|
|
62
|
-
try {
|
|
63
|
-
key = sshpk.parsePrivateKey(text, "ssh");
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
if (error.name === "KeyEncryptedError") {
|
|
67
|
-
throw new AgentKeyError('is encrypted with a passphrase, which the agent cannot type; make it with -N "" and keep it in a secret store instead.');
|
|
68
|
-
}
|
|
69
|
-
throw new AgentKeyError("is cut short or damaged.");
|
|
70
|
-
}
|
|
71
|
-
if (key.type !== "ed25519")
|
|
72
|
-
throw new AgentKeyError(`must be an ssh-ed25519 key, not ${key.type}; make one with ssh-keygen -t ed25519.`);
|
|
73
|
-
return createPrivateKey(key.toString("pkcs8"));
|
|
74
|
-
}
|
|
75
|
-
// ── New keys ─────────────────────────────────────────────────────────────────
|
|
76
|
-
// A new ed25519 key in the files ssh-keygen would write: the unencrypted OpenSSH private key and its public line.
|
|
77
|
-
export function generate_agent_key(comment) {
|
|
78
|
-
const key = sshpk.generatePrivateKey("ed25519");
|
|
79
|
-
key.comment = comment;
|
|
80
|
-
return { private_text: key.toString("ssh"), public_line: key.toPublic().toString("ssh") };
|
|
81
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { type CommandStream } from "./commands.ts";
|
|
2
|
-
import { type LongRun, type LongRunPoll } from "./protocol.ts";
|
|
3
|
-
import type { TaskRunner } from "./tasks.ts";
|
|
4
|
-
export declare const poll_limit_in_bytes: number;
|
|
5
|
-
export interface LongRunOptions {
|
|
6
|
-
kill_grace_in_milliseconds?: number;
|
|
7
|
-
tail_interval_in_milliseconds?: number;
|
|
8
|
-
}
|
|
9
|
-
export declare class LongRunError extends Error {
|
|
10
|
-
constructor(message: string);
|
|
11
|
-
}
|
|
12
|
-
export declare class LongRuns<Task> {
|
|
13
|
-
private readonly directory;
|
|
14
|
-
private readonly grace;
|
|
15
|
-
private readonly tail_interval;
|
|
16
|
-
private readonly runner;
|
|
17
|
-
private readonly children;
|
|
18
|
-
constructor(state_directory: string, runner: TaskRunner<Task>, options?: LongRunOptions);
|
|
19
|
-
start(run_id: string, task: Task, stdin: string | null): LongRun;
|
|
20
|
-
list(): LongRun[];
|
|
21
|
-
get(run_id: string): LongRun;
|
|
22
|
-
poll(run_id: string, offset: number, limit?: number): Promise<LongRunPoll>;
|
|
23
|
-
tail(run_id: string, offset: number): CommandStream;
|
|
24
|
-
stop(run_id: string): Promise<LongRun>;
|
|
25
|
-
forget(run_id: string): void;
|
|
26
|
-
prune(max_age_in_seconds: number): void;
|
|
27
|
-
private folder;
|
|
28
|
-
private record;
|
|
29
|
-
private require;
|
|
30
|
-
private is_alive;
|
|
31
|
-
private describe;
|
|
32
|
-
}
|