easy-vps 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/bin/cli.js +180 -0
  2. package/client/assets/index-C4GZ_uMC.js +104 -0
  3. package/client/assets/index-Dq-W19hL.css +2 -0
  4. package/client/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
  5. package/client/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
  6. package/client/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
  7. package/client/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
  8. package/client/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
  9. package/client/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
  10. package/client/favicon.svg +1 -0
  11. package/client/icons.svg +24 -0
  12. package/client/index.html +14 -0
  13. package/dist/index.d.ts +22 -0
  14. package/dist/index.js +143 -0
  15. package/dist/routes/async-route.d.ts +3 -0
  16. package/dist/routes/async-route.js +9 -0
  17. package/dist/routes/auth.d.ts +5 -0
  18. package/dist/routes/auth.js +101 -0
  19. package/dist/routes/database.d.ts +2 -0
  20. package/dist/routes/database.js +192 -0
  21. package/dist/routes/deploy.d.ts +2 -0
  22. package/dist/routes/deploy.js +161 -0
  23. package/dist/routes/domain.d.ts +2 -0
  24. package/dist/routes/domain.js +54 -0
  25. package/dist/routes/firewall.d.ts +2 -0
  26. package/dist/routes/firewall.js +63 -0
  27. package/dist/routes/instances.d.ts +2 -0
  28. package/dist/routes/instances.js +77 -0
  29. package/dist/routes/logs.d.ts +2 -0
  30. package/dist/routes/logs.js +67 -0
  31. package/dist/routes/system.d.ts +2 -0
  32. package/dist/routes/system.js +83 -0
  33. package/dist/services/auth.d.ts +25 -0
  34. package/dist/services/auth.js +128 -0
  35. package/dist/services/backup-config.d.ts +16 -0
  36. package/dist/services/backup-config.js +51 -0
  37. package/dist/services/backup-scheduler.d.ts +2 -0
  38. package/dist/services/backup-scheduler.js +104 -0
  39. package/dist/services/daemon.d.ts +28 -0
  40. package/dist/services/daemon.js +180 -0
  41. package/dist/services/database.d.ts +60 -0
  42. package/dist/services/database.js +540 -0
  43. package/dist/services/deploy.d.ts +102 -0
  44. package/dist/services/deploy.js +540 -0
  45. package/dist/services/domain.d.ts +26 -0
  46. package/dist/services/domain.js +170 -0
  47. package/dist/services/firewall.d.ts +24 -0
  48. package/dist/services/firewall.js +64 -0
  49. package/dist/services/instances.d.ts +17 -0
  50. package/dist/services/instances.js +67 -0
  51. package/dist/services/logs.d.ts +8 -0
  52. package/dist/services/logs.js +61 -0
  53. package/dist/services/metrics-history.d.ts +13 -0
  54. package/dist/services/metrics-history.js +36 -0
  55. package/dist/services/packages.d.ts +22 -0
  56. package/dist/services/packages.js +124 -0
  57. package/dist/services/s3.d.ts +17 -0
  58. package/dist/services/s3.js +93 -0
  59. package/dist/services/ssh.d.ts +78 -0
  60. package/dist/services/ssh.js +309 -0
  61. package/dist/services/system.d.ts +74 -0
  62. package/dist/services/system.js +286 -0
  63. package/package.json +55 -0
  64. package/scripts/dev.js +88 -0
  65. package/scripts/postinstall.js +123 -0
  66. package/scripts/preuninstall.js +21 -0
  67. package/scripts/try-install.sh +65 -0
  68. package/scripts/ui.js +54 -0
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.list = list;
40
+ exports.addDomain = addDomain;
41
+ exports.removeDomain = removeDomain;
42
+ exports.issueCertificate = issueCertificate;
43
+ // Nginx config writer & Certbot SSL, applied to the managed VPS over SSH.
44
+ const node_path_1 = __importDefault(require("node:path"));
45
+ const system = __importStar(require("./system"));
46
+ const ssh_1 = require("./ssh");
47
+ const SITES_AVAILABLE = '/etc/nginx/sites-available';
48
+ const SITES_ENABLED = '/etc/nginx/sites-enabled';
49
+ const LETSENCRYPT_LIVE = '/etc/letsencrypt/live';
50
+ // Same shape as the client's validateDomain() in client-ui/src/modules/domains/validation.ts —
51
+ // re-checked here because the domain lands verbatim in an Nginx server_name directive.
52
+ const HOSTNAME_RE = /^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i;
53
+ function availablePath(domain) {
54
+ return node_path_1.default.join(SITES_AVAILABLE, domain);
55
+ }
56
+ function enabledPath(domain) {
57
+ return node_path_1.default.join(SITES_ENABLED, domain);
58
+ }
59
+ /** Every mutation here writes under /etc, so fail early with a useful message. */
60
+ async function assertPrivileged(remote) {
61
+ if (await system.canAdminister(remote))
62
+ return;
63
+ throw new Error(`"${remote.username}" cannot write Nginx config on ${remote.host}. ` +
64
+ 'Log in as root, or enable passwordless sudo.');
65
+ }
66
+ function assertValidDomain(domain) {
67
+ if (!HOSTNAME_RE.test(domain)) {
68
+ throw new Error(`"${domain}" is not a valid domain name`);
69
+ }
70
+ }
71
+ function renderConfig(config) {
72
+ return `server {
73
+ listen 80;
74
+ server_name ${config.domain} www.${config.domain};
75
+
76
+ location / {
77
+ proxy_pass http://localhost:${config.port};
78
+ proxy_http_version 1.1;
79
+ proxy_set_header Upgrade $http_upgrade;
80
+ proxy_set_header Connection 'upgrade';
81
+ proxy_set_header Host $host;
82
+ proxy_cache_bypass $http_upgrade;
83
+ }
84
+ }
85
+ `;
86
+ }
87
+ /** Parses the port an existing site config proxies to, e.g. "proxy_pass http://localhost:3000;" -> 3000. */
88
+ function parsePort(contents) {
89
+ const match = /proxy_pass\s+http:\/\/localhost:(\d+);/.exec(contents);
90
+ return match ? Number(match[1]) : 0;
91
+ }
92
+ async function testConfig(remote) {
93
+ const command = await remote.privileged('nginx -t');
94
+ const { stderr, stdout, code } = await remote.exec(command);
95
+ if (code !== 0) {
96
+ throw new Error((stderr || stdout).trim() || 'nginx -t failed');
97
+ }
98
+ }
99
+ async function reloadNginx(remote) {
100
+ await remote.execOrThrow(await remote.privileged('systemctl reload nginx'));
101
+ }
102
+ async function list(remote) {
103
+ const domains = await remote.listDir(SITES_AVAILABLE);
104
+ return Promise.all(domains.map(async (domain) => {
105
+ const [contents, certificateActive, active] = await Promise.all([
106
+ remote.readFile(availablePath(domain)).catch(() => ''),
107
+ remote.exists(node_path_1.default.join(LETSENCRYPT_LIVE, domain)),
108
+ remote.exists(enabledPath(domain)),
109
+ ]);
110
+ return {
111
+ domain,
112
+ port: parsePort(contents),
113
+ ssl: certificateActive,
114
+ active,
115
+ certificateActive,
116
+ };
117
+ }));
118
+ }
119
+ async function addDomain(remote, config) {
120
+ assertValidDomain(config.domain);
121
+ await assertPrivileged(remote);
122
+ await remote.writeFile(availablePath(config.domain), renderConfig(config));
123
+ await remote.symlink(availablePath(config.domain), enabledPath(config.domain));
124
+ try {
125
+ await testConfig(remote);
126
+ }
127
+ catch (error) {
128
+ await remote.remove(enabledPath(config.domain));
129
+ await remote.remove(availablePath(config.domain));
130
+ throw error;
131
+ }
132
+ await reloadNginx(remote);
133
+ if (!config.ssl)
134
+ return { domain: config.domain, ssl: 'skipped' };
135
+ // The vhost is already live at this point, so a certificate failure is a partial
136
+ // success, not a failed request — report it instead of throwing the domain away.
137
+ try {
138
+ await issueCertificate(remote, config.domain);
139
+ return { domain: config.domain, ssl: 'issued' };
140
+ }
141
+ catch (error) {
142
+ const message = error instanceof Error ? error.message : String(error);
143
+ return { domain: config.domain, ssl: 'failed', sslError: message };
144
+ }
145
+ }
146
+ async function removeDomain(remote, domain) {
147
+ assertValidDomain(domain);
148
+ await assertPrivileged(remote);
149
+ await remote.remove(enabledPath(domain));
150
+ await remote.remove(availablePath(domain));
151
+ await remote.exec(await remote.privileged(`certbot delete --cert-name ${(0, ssh_1.quote)(domain)} --non-interactive`));
152
+ await testConfig(remote);
153
+ await reloadNginx(remote);
154
+ }
155
+ async function issueCertificate(remote, domain) {
156
+ assertValidDomain(domain);
157
+ await assertPrivileged(remote);
158
+ const certbot = await system.detect(remote, 'certbot');
159
+ if (!certbot.installed) {
160
+ throw new Error('Certbot is not installed on this server');
161
+ }
162
+ const command = [
163
+ 'certbot --nginx',
164
+ `-d ${(0, ssh_1.quote)(domain)}`,
165
+ `-d ${(0, ssh_1.quote)(`www.${domain}`)}`,
166
+ '--non-interactive --agree-tos --redirect --register-unsafely-without-email',
167
+ ].join(' ');
168
+ // Certbot talks to Let's Encrypt, so it needs longer than the default budget.
169
+ await remote.execOrThrow(await remote.privileged(command), 120_000);
170
+ }
@@ -0,0 +1,24 @@
1
+ import { Remote } from './ssh';
2
+ export interface FirewallRule {
3
+ /** 1-based position as printed by `ufw status numbered`, used for deletes. */
4
+ number: number;
5
+ /** Destination, e.g. "22/tcp" or "Anywhere". */
6
+ to: string;
7
+ /** Action with direction, e.g. "ALLOW IN". */
8
+ action: string;
9
+ /** Source, e.g. "Anywhere" or "192.168.0.0/16". */
10
+ from: string;
11
+ }
12
+ export interface FirewallStatus {
13
+ /** ufw is present on the server. */
14
+ installed: boolean;
15
+ /** ufw is running and enforcing rules. */
16
+ active: boolean;
17
+ rules: FirewallRule[];
18
+ }
19
+ export declare function getStatus(remote: Remote): Promise<FirewallStatus>;
20
+ export declare function allowPort(remote: Remote, port: number, protocol?: 'tcp' | 'udp'): Promise<void>;
21
+ export declare function deleteRule(remote: Remote, number: number): Promise<void>;
22
+ /** Turns the firewall on, answering ufw's "disrupt SSH?" prompt with yes. */
23
+ export declare function enableFirewall(remote: Remote): Promise<void>;
24
+ export declare function disableFirewall(remote: Remote): Promise<void>;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getStatus = getStatus;
4
+ exports.allowPort = allowPort;
5
+ exports.deleteRule = deleteRule;
6
+ exports.enableFirewall = enableFirewall;
7
+ exports.disableFirewall = disableFirewall;
8
+ // Reads and mutates the managed server's ufw firewall over SSH. All commands
9
+ // run on the remote; the panel process itself never touches a local firewall.
10
+ const ssh_1 = require("./ssh");
11
+ // `[ 1] 22/tcp ALLOW IN Anywhere` — action is two words ("ALLOW IN").
12
+ const RULE_RE = /\[\s*(\d+)\]\s+(\S+)\s+(\S+\s+\S+)\s+(.+)/;
13
+ function parseStatus(output) {
14
+ const active = /Status:\s*active/i.test(output);
15
+ const rules = [];
16
+ for (const line of output.split('\n')) {
17
+ const match = RULE_RE.exec(line);
18
+ if (match) {
19
+ rules.push({
20
+ number: Number(match[1]),
21
+ to: match[2],
22
+ action: match[3],
23
+ from: match[4].trim(),
24
+ });
25
+ }
26
+ }
27
+ return { active, rules };
28
+ }
29
+ async function getStatus(remote) {
30
+ const command = await remote.privileged('ufw status numbered');
31
+ const { code, stdout, stderr } = await remote.exec(command);
32
+ if (code !== 0) {
33
+ if (/not installed|command not found|No such file/i.test(`${stderr} ${stdout}`)) {
34
+ return { installed: false, active: false, rules: [] };
35
+ }
36
+ throw new ssh_1.RemoteError(stderr.trim() || 'Failed to read firewall status');
37
+ }
38
+ const parsed = parseStatus(stdout);
39
+ return { installed: true, ...parsed };
40
+ }
41
+ async function allowPort(remote, port, protocol = 'tcp') {
42
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
43
+ throw new ssh_1.RemoteError('Port must be a number between 1 and 65535');
44
+ }
45
+ const command = await remote.privileged(`ufw allow ${port}/${protocol}`);
46
+ await remote.execOrThrow(command);
47
+ }
48
+ async function deleteRule(remote, number) {
49
+ if (!Number.isInteger(number) || number < 1) {
50
+ throw new ssh_1.RemoteError('Invalid rule number');
51
+ }
52
+ // `--force` avoids the interactive confirmation ufw prints for the last rule.
53
+ const command = await remote.privileged(`ufw --force delete ${number}`);
54
+ await remote.execOrThrow(command);
55
+ }
56
+ /** Turns the firewall on, answering ufw's "disrupt SSH?" prompt with yes. */
57
+ async function enableFirewall(remote) {
58
+ const command = await remote.privileged(`printf 'y\\n' | ufw enable`);
59
+ await remote.execOrThrow(command);
60
+ }
61
+ async function disableFirewall(remote) {
62
+ const command = await remote.privileged('ufw disable');
63
+ await remote.execOrThrow(command);
64
+ }
@@ -0,0 +1,17 @@
1
+ import { type Remote, type StreamHandlers } from './ssh';
2
+ export type InstanceState = 'running' | 'stopped' | 'unknown';
3
+ export interface Pm2Instance {
4
+ name: string;
5
+ pid: number | null;
6
+ status: InstanceState;
7
+ instanceId: number;
8
+ cpu: number;
9
+ memoryMb: number;
10
+ restarts: number;
11
+ uptime: string;
12
+ }
13
+ export declare function list(remote: Remote): Promise<Pm2Instance[]>;
14
+ export declare function restart(remote: Remote, name: string): Promise<void>;
15
+ export declare function stop(remote: Remote, name: string): Promise<void>;
16
+ export declare function remove(remote: Remote, name: string): Promise<void>;
17
+ export declare function logStream(remote: Remote, name: string, handlers: StreamHandlers): () => void;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.list = list;
4
+ exports.restart = restart;
5
+ exports.stop = stop;
6
+ exports.remove = remove;
7
+ exports.logStream = logStream;
8
+ // PM2 process list — status, cpu, memory and restarts per instance.
9
+ const ssh_1 = require("./ssh");
10
+ function formatUptime(ms) {
11
+ const seconds = Math.floor(ms / 1000);
12
+ const minutes = Math.floor(seconds / 60);
13
+ const hours = Math.floor(minutes / 60);
14
+ const days = Math.floor(hours / 24);
15
+ if (days > 0)
16
+ return `${days}d ${hours % 24}h`;
17
+ if (hours > 0)
18
+ return `${hours}h ${minutes % 60}m`;
19
+ if (minutes > 0)
20
+ return `${minutes}m ${seconds % 60}s`;
21
+ return `${seconds}s`;
22
+ }
23
+ async function list(remote) {
24
+ const { stdout, code } = await remote.exec((0, ssh_1.loginShell)('pm2 jlist'), 15_000);
25
+ if (code !== 0)
26
+ return [];
27
+ const start = stdout.indexOf('[');
28
+ const end = stdout.lastIndexOf(']');
29
+ if (start === -1 || end <= start)
30
+ return [];
31
+ try {
32
+ const parsed = JSON.parse(stdout.slice(start, end + 1));
33
+ return parsed
34
+ .filter((p) => p.name)
35
+ .map((p) => {
36
+ const now = Date.now();
37
+ const uptimeMs = p.pm2_env?.pm_uptime ? now - p.pm2_env.pm_uptime : 0;
38
+ return {
39
+ name: p.name,
40
+ pid: p.pid ?? null,
41
+ status: p.pm2_env?.status ?? 'unknown',
42
+ instanceId: p.pm_id ?? 0,
43
+ cpu: p.monit?.cpu ?? 0,
44
+ memoryMb: Math.round((p.monit?.memory ?? 0) / 1024 / 1024),
45
+ restarts: p.pm2_env?.restart_time ?? 0,
46
+ uptime: formatUptime(uptimeMs),
47
+ };
48
+ });
49
+ }
50
+ catch {
51
+ return [];
52
+ }
53
+ }
54
+ async function restart(remote, name) {
55
+ await remote.execOrThrow((0, ssh_1.loginShell)(`pm2 restart ${(0, ssh_1.quote)(name)} --update-env`), 30_000);
56
+ }
57
+ async function stop(remote, name) {
58
+ await remote.execOrThrow((0, ssh_1.loginShell)(`pm2 stop ${(0, ssh_1.quote)(name)}`), 30_000);
59
+ }
60
+ async function remove(remote, name) {
61
+ await remote.execOrThrow((0, ssh_1.loginShell)(`pm2 delete ${(0, ssh_1.quote)(name)}`), 30_000);
62
+ }
63
+ function logStream(remote, name, handlers) {
64
+ const command = `pm2 logs ${(0, ssh_1.quote)(name)} --lines 200 --raw`;
65
+ handlers.onOutput(`$ ${command}`);
66
+ return remote.execStream((0, ssh_1.loginShell)(command), handlers);
67
+ }
@@ -0,0 +1,8 @@
1
+ import { type Remote, type StreamHandlers } from './ssh';
2
+ export type LogSourceId = 'panel' | 'nginx' | 'pm2' | 'docker';
3
+ export declare function isValidSource(id: string): id is LogSourceId;
4
+ /**
5
+ * Starts streaming logs for the given source. Returns a cancel function that
6
+ * tears down the SSH channel when the client disconnects.
7
+ */
8
+ export declare function stream(remote: Remote, sourceId: LogSourceId, handlers: StreamHandlers): () => void;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ // Streams runtime logs from four sources on the managed VPS: the panel
3
+ // (journalctl), Nginx error log, PM2 aggregate output, and Docker events.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.isValidSource = isValidSource;
6
+ exports.stream = stream;
7
+ const ssh_1 = require("./ssh");
8
+ const VALID_SOURCES = new Set([
9
+ 'panel',
10
+ 'nginx',
11
+ 'pm2',
12
+ 'docker',
13
+ ]);
14
+ function isValidSource(id) {
15
+ return VALID_SOURCES.has(id);
16
+ }
17
+ /**
18
+ * Starts streaming logs for the given source. Returns a cancel function that
19
+ * tears down the SSH channel when the client disconnects.
20
+ */
21
+ function stream(remote, sourceId, handlers) {
22
+ switch (sourceId) {
23
+ case 'panel':
24
+ return streamPanel(remote, handlers);
25
+ case 'nginx':
26
+ return streamNginx(remote, handlers);
27
+ case 'pm2':
28
+ return streamPm2(remote, handlers);
29
+ case 'docker':
30
+ return streamDocker(remote, handlers);
31
+ }
32
+ }
33
+ /** journalctl -f for the easy-vps service unit. */
34
+ function streamPanel(remote, handlers) {
35
+ const command = 'journalctl -u easy-vps -f --no-pager -o short-iso';
36
+ handlers.onOutput(`$ ${command}`);
37
+ return remote.execStream((0, ssh_1.loginShell)(command), handlers);
38
+ }
39
+ /** Follow the Nginx error log. */
40
+ function streamNginx(remote, handlers) {
41
+ const command = 'tail -f -n 200 /var/log/nginx/error.log';
42
+ handlers.onOutput(`$ ${command}`);
43
+ return remote.execStream((0, ssh_1.loginShell)(command), handlers);
44
+ }
45
+ /** Aggregate PM2 log output from all managed processes. */
46
+ function streamPm2(remote, handlers) {
47
+ const command = 'pm2 logs --raw --nostream --lines 200 && pm2 logs --raw';
48
+ handlers.onOutput(`$ pm2 logs --raw`);
49
+ return remote.execStream((0, ssh_1.loginShell)(command), handlers);
50
+ }
51
+ /**
52
+ * Docker doesn't have a single "follow all containers" command, so we stream
53
+ * `docker events` which shows container lifecycle activity (start, stop, die,
54
+ * oom, etc.) in real time. Application-level logs remain per-site in the
55
+ * website detail page.
56
+ */
57
+ function streamDocker(remote, handlers) {
58
+ const command = 'docker events --format "{{json .}}" --since 1h';
59
+ handlers.onOutput(`$ docker events`);
60
+ return remote.execStream((0, ssh_1.loginShell)(command), handlers);
61
+ }
@@ -0,0 +1,13 @@
1
+ import type { LoadAverage } from './system';
2
+ /** A single load-average reading captured at a point in time. */
3
+ export interface LoadSample {
4
+ /** Epoch milliseconds when the sample was captured. */
5
+ t: number;
6
+ one: number;
7
+ five: number;
8
+ fifteen: number;
9
+ }
10
+ /** Records the latest load reading for a host, one sample per 5-minute bucket. */
11
+ export declare function recordLoad(host: string, load: LoadAverage): void;
12
+ /** Returns the last hour of load samples for a host, oldest first. */
13
+ export declare function getLoadHistory(host: string): LoadSample[];
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.recordLoad = recordLoad;
4
+ exports.getLoadHistory = getLoadHistory;
5
+ const BUCKET_MS = 5 * 60 * 1000;
6
+ const BUCKETS = 12; // 12 × 5 min = 1 hour
7
+ /**
8
+ * In-memory, per-host ring of load-average samples bucketed to 5-minute
9
+ * windows. The panel only collects metrics on demand, so this fills in over
10
+ * the first hour a session is open; gaps before then are simply absent.
11
+ */
12
+ const history = new Map();
13
+ /** Records the latest load reading for a host, one sample per 5-minute bucket. */
14
+ function recordLoad(host, load) {
15
+ const now = Date.now();
16
+ const bucket = Math.floor(now / BUCKET_MS);
17
+ let buckets = history.get(host);
18
+ if (!buckets) {
19
+ buckets = new Map();
20
+ history.set(host, buckets);
21
+ }
22
+ buckets.set(bucket, { t: now, one: load.one, five: load.five, fifteen: load.fifteen });
23
+ // Drop anything older than the retained window.
24
+ const cutoff = bucket - BUCKETS + 1;
25
+ for (const key of buckets.keys()) {
26
+ if (key < cutoff)
27
+ buckets.delete(key);
28
+ }
29
+ }
30
+ /** Returns the last hour of load samples for a host, oldest first. */
31
+ function getLoadHistory(host) {
32
+ const buckets = history.get(host);
33
+ if (!buckets)
34
+ return [];
35
+ return [...buckets.values()].sort((a, b) => a.t - b.t);
36
+ }
@@ -0,0 +1,22 @@
1
+ export type DependencyName = 'nginx' | 'pm2' | 'docker' | 'certbot' | 'git' | 'nodejs' | 'redis' | 'ufw' | 'fail2ban' | 'htop';
2
+ export interface PackageDefinition {
3
+ packageName: DependencyName;
4
+ /** Prints the version and exits non-zero when the package is absent. */
5
+ versionCommand: string;
6
+ installCommand: string;
7
+ /** Purges the package *and* the paths listed in `removes`. */
8
+ uninstallCommand: string;
9
+ /**
10
+ * Everything the uninstall deletes, beyond the package itself. Sent to the UI
11
+ * so the confirmation dialog can name what is about to be destroyed rather
12
+ * than asking the user to take it on faith.
13
+ */
14
+ removes: string[];
15
+ }
16
+ /**
17
+ * Stands in for the absolute path to npm, resolved on the server at run time.
18
+ * npm is not reliably on a non-interactive PATH — see resolveNpm() in system.ts.
19
+ */
20
+ export declare const NPM_PLACEHOLDER = "{{npm}}";
21
+ export declare const PACKAGES: Record<DependencyName, PackageDefinition>;
22
+ export declare const DEPENDENCY_NAMES: DependencyName[];
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ // One definition per managed dependency: how to detect it, install it, and
3
+ // remove it along with the files it leaves behind.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.DEPENDENCY_NAMES = exports.PACKAGES = exports.NPM_PLACEHOLDER = void 0;
6
+ /**
7
+ * Stands in for the absolute path to npm, resolved on the server at run time.
8
+ * npm is not reliably on a non-interactive PATH — see resolveNpm() in system.ts.
9
+ */
10
+ exports.NPM_PLACEHOLDER = '{{npm}}';
11
+ const apt = (packages, removes) => [
12
+ `apt-get purge -y ${packages}`,
13
+ 'apt-get autoremove -y',
14
+ ...removes.map((target) => `rm -rf ${target}`),
15
+ ].join(' && ');
16
+ exports.PACKAGES = {
17
+ nginx: {
18
+ packageName: 'nginx',
19
+ versionCommand: 'nginx -v',
20
+ installCommand: 'apt-get install -y nginx',
21
+ uninstallCommand: apt('nginx nginx-common nginx-core', [
22
+ '/etc/nginx',
23
+ '/var/log/nginx',
24
+ '/var/www/html',
25
+ ]),
26
+ removes: ['/etc/nginx', '/var/log/nginx', '/var/www/html'],
27
+ },
28
+ pm2: {
29
+ packageName: 'pm2',
30
+ versionCommand: 'pm2 --version',
31
+ installCommand: `${exports.NPM_PLACEHOLDER} install -g pm2`,
32
+ // Kill the daemon first, or it lingers holding the old binary's state.
33
+ uninstallCommand: [
34
+ `(${exports.NPM_PLACEHOLDER} exec -- pm2 kill || pm2 kill || true)`,
35
+ `(${exports.NPM_PLACEHOLDER} uninstall -g pm2 || true)`,
36
+ 'rm -rf "$HOME"/.pm2',
37
+ ].join(' && '),
38
+ removes: ['~/.pm2 (process list, logs and dumps)'],
39
+ },
40
+ docker: {
41
+ packageName: 'docker',
42
+ versionCommand: 'docker --version',
43
+ installCommand: 'apt-get install -y docker.io',
44
+ uninstallCommand: apt('docker.io docker-compose', [
45
+ '/var/lib/docker',
46
+ '/var/lib/containerd',
47
+ '/etc/docker',
48
+ ]),
49
+ removes: [
50
+ '/var/lib/docker (all images, containers and volumes)',
51
+ '/var/lib/containerd',
52
+ '/etc/docker',
53
+ ],
54
+ },
55
+ certbot: {
56
+ packageName: 'certbot',
57
+ versionCommand: 'certbot --version',
58
+ installCommand: 'apt-get install -y certbot python3-certbot-nginx',
59
+ uninstallCommand: apt('certbot python3-certbot-nginx', [
60
+ '/etc/letsencrypt',
61
+ '/var/lib/letsencrypt',
62
+ '/var/log/letsencrypt',
63
+ ]),
64
+ removes: [
65
+ '/etc/letsencrypt (every certificate and private key)',
66
+ '/var/lib/letsencrypt',
67
+ '/var/log/letsencrypt',
68
+ ],
69
+ },
70
+ git: {
71
+ packageName: 'git',
72
+ versionCommand: 'git --version',
73
+ // Leaves ~/.gitconfig and cloned repositories alone: those are user data,
74
+ // not configuration this panel created.
75
+ installCommand: 'apt-get install -y git',
76
+ uninstallCommand: apt('git', []),
77
+ removes: [],
78
+ },
79
+ nodejs: {
80
+ packageName: 'nodejs',
81
+ versionCommand: 'node --version',
82
+ installCommand: 'apt-get install -y nodejs npm',
83
+ // Globally installed packages live here, pm2 among them.
84
+ uninstallCommand: apt('nodejs npm', ['/usr/lib/node_modules', '"$HOME"/.npm']),
85
+ removes: ['/usr/lib/node_modules (all global npm packages, including PM2)', '~/.npm'],
86
+ },
87
+ redis: {
88
+ packageName: 'redis',
89
+ versionCommand: 'redis-server --version',
90
+ installCommand: 'apt-get install -y redis-server',
91
+ uninstallCommand: apt('redis-server redis-tools', [
92
+ '/etc/redis',
93
+ '/var/lib/redis',
94
+ '/var/log/redis',
95
+ ]),
96
+ removes: ['/etc/redis', '/var/lib/redis (persisted keys)', '/var/log/redis'],
97
+ },
98
+ ufw: {
99
+ packageName: 'ufw',
100
+ versionCommand: 'ufw --version',
101
+ installCommand: 'apt-get install -y ufw',
102
+ uninstallCommand: apt('ufw', ['/etc/ufw', '/lib/ufw']),
103
+ removes: ['/etc/ufw (all firewall rules)', '/lib/ufw'],
104
+ },
105
+ fail2ban: {
106
+ packageName: 'fail2ban',
107
+ versionCommand: 'fail2ban-client --version',
108
+ installCommand: 'apt-get install -y fail2ban',
109
+ uninstallCommand: apt('fail2ban', [
110
+ '/etc/fail2ban',
111
+ '/var/lib/fail2ban',
112
+ '/var/log/fail2ban.log',
113
+ ]),
114
+ removes: ['/etc/fail2ban (jails and filters)', '/var/lib/fail2ban (ban database)'],
115
+ },
116
+ htop: {
117
+ packageName: 'htop',
118
+ versionCommand: 'htop --version',
119
+ installCommand: 'apt-get install -y htop',
120
+ uninstallCommand: apt('htop', []),
121
+ removes: [],
122
+ },
123
+ };
124
+ exports.DEPENDENCY_NAMES = Object.keys(exports.PACKAGES);
@@ -0,0 +1,17 @@
1
+ import { S3Client } from '@aws-sdk/client-s3';
2
+ import type { S3Config } from './backup-config';
3
+ export interface S3BackupEntry {
4
+ key: string;
5
+ size: number;
6
+ lastModified: Date;
7
+ }
8
+ export declare function createS3Client(config: S3Config): S3Client;
9
+ export declare function backupS3Key(engine: string, dbName: string, timestamp: string): string;
10
+ export declare function backupPrefix(engine: string, dbName: string): string;
11
+ export declare function uploadToS3(s3Config: S3Config, localPath: string, s3Key: string): Promise<{
12
+ key: string;
13
+ }>;
14
+ export declare function listS3Backups(s3Config: S3Config, engine: string, dbName: string): Promise<S3BackupEntry[]>;
15
+ export declare function downloadFromS3(s3Config: S3Config, s3Key: string, localPath: string): Promise<void>;
16
+ export declare function deleteFromS3(s3Config: S3Config, s3Key: string): Promise<void>;
17
+ export declare function enforceRetention(s3Config: S3Config, engine: string, dbName: string, retention: number): Promise<void>;