@naviyra/cloud-panel 0.0.0-stage → 0.0.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/.env.example +40 -0
- package/README.md +134 -2
- package/apps/agent/package.json +13 -0
- package/apps/agent/src/cloudflare.mjs +733 -0
- package/apps/agent/src/native-worker.mjs +52 -0
- package/apps/agent/src/native.mjs +457 -0
- package/apps/agent/src/runtime.mjs +760 -0
- package/apps/agent/src/server.mjs +195 -0
- package/apps/agent/src/static-server.mjs +53 -0
- package/apps/panel/AGENTS.md +9 -0
- package/apps/panel/CLAUDE.md +1 -0
- package/apps/panel/app/api/[...path]/route.js +445 -0
- package/apps/panel/app/globals.css +2057 -0
- package/apps/panel/app/icon.png +0 -0
- package/apps/panel/app/layout.jsx +18 -0
- package/apps/panel/app/login/page.jsx +108 -0
- package/apps/panel/app/page.jsx +33 -0
- package/apps/panel/app/ui/cloudflare.jsx +926 -0
- package/apps/panel/app/ui/dashboard.jsx +1487 -0
- package/apps/panel/app/ui/databases.jsx +293 -0
- package/apps/panel/app/ui/skeleton.jsx +196 -0
- package/apps/panel/app/ui/theme.jsx +35 -0
- package/apps/panel/lib/agent.mjs +83 -0
- package/apps/panel/lib/auth.mjs +48 -0
- package/apps/panel/lib/database-runtime.mjs +243 -0
- package/apps/panel/lib/databases.mjs +292 -0
- package/apps/panel/lib/db.mjs +6 -0
- package/apps/panel/next.config.mjs +8 -0
- package/apps/panel/package.json +22 -0
- package/apps/panel/public/naviyra-logo.png +0 -0
- package/bin/naviyra.mjs +177 -0
- package/db/001_init.sql +28 -0
- package/db/002_native_runtimes.sql +3 -0
- package/db/003_databases.sql +8 -0
- package/db/004_database_engines.sql +4 -0
- package/deploy/naviyra-agent.service +17 -0
- package/docker-compose.yml +18 -0
- package/docs/managed-runtime.md +52 -0
- package/docs/native-hosts.md +48 -0
- package/package.json +61 -4
- package/packages/runtime/artifacts.json +32 -0
- package/packages/runtime/index.cjs +526 -0
- package/packages/runtime/index.mjs +10 -0
- package/packages/runtime/package.json +6 -0
- package/packages/shared/index.mjs +85 -0
- package/packages/shared/package.json +9 -0
- package/scripts/migrate.mjs +18 -0
- package/scripts/postgres-local.mjs +97 -0
- package/scripts/runtime-download-manifest.mjs +30 -0
- package/scripts/runtime.mjs +20 -0
- package/scripts/seed.mjs +35 -0
- package/scripts/service-supervisor.mjs +164 -0
- package/scripts/setup-local.mjs +215 -0
- package/scripts/start-naviyra.mjs +303 -0
- package/scripts/windows-boot.mjs +259 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { access, mkdir, writeFile, unlink } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import pg from "pg";
|
|
5
|
+
const root = path.resolve(".runtime/postgres");
|
|
6
|
+
const bin =
|
|
7
|
+
process.env.POSTGRES_BIN ||
|
|
8
|
+
(process.platform === "win32" ? "C:/Program Files/PostgreSQL/18/bin" : "");
|
|
9
|
+
const exe = (name) =>
|
|
10
|
+
path.join(bin, name + (process.platform === "win32" ? ".exe" : ""));
|
|
11
|
+
const run = (name, args) =>
|
|
12
|
+
new Promise((resolve, reject) => {
|
|
13
|
+
const child = spawn(exe(name), args, {
|
|
14
|
+
windowsHide: true,
|
|
15
|
+
stdio: "inherit",
|
|
16
|
+
shell: false,
|
|
17
|
+
});
|
|
18
|
+
child.on("error", reject);
|
|
19
|
+
child.on("exit", (code) =>
|
|
20
|
+
code === 0 ? resolve() : reject(Error(`${name} exited ${code}`)),
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
const url = new URL(process.env.DATABASE_URL || "");
|
|
24
|
+
if (!["localhost", "127.0.0.1"].includes(url.hostname))
|
|
25
|
+
throw Error(
|
|
26
|
+
"db:local only manages a loopback database; keep remote PostgreSQL separately managed",
|
|
27
|
+
);
|
|
28
|
+
const port = Number(url.port || 5432),
|
|
29
|
+
user = decodeURIComponent(url.username),
|
|
30
|
+
password = decodeURIComponent(url.password),
|
|
31
|
+
database = url.pathname.slice(1);
|
|
32
|
+
if (
|
|
33
|
+
!password ||
|
|
34
|
+
!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(user) ||
|
|
35
|
+
!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(database)
|
|
36
|
+
)
|
|
37
|
+
throw Error(
|
|
38
|
+
"Local database URL needs a password and simple database/user names",
|
|
39
|
+
);
|
|
40
|
+
if (process.argv.includes("--stop")) {
|
|
41
|
+
await run("pg_ctl", ["-D", root, "stop", "-m", "fast"]);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
await mkdir(".runtime", { recursive: true });
|
|
45
|
+
let initialized = false;
|
|
46
|
+
try {
|
|
47
|
+
await access(path.join(root, "PG_VERSION"));
|
|
48
|
+
initialized = true;
|
|
49
|
+
} catch {}
|
|
50
|
+
if (!initialized) {
|
|
51
|
+
const pwfile = path.resolve(".runtime/postgres-init-password");
|
|
52
|
+
await writeFile(pwfile, password, { mode: 0o600 });
|
|
53
|
+
try {
|
|
54
|
+
await run("initdb", [
|
|
55
|
+
"-D",
|
|
56
|
+
root,
|
|
57
|
+
"-U",
|
|
58
|
+
user,
|
|
59
|
+
"--auth=scram-sha-256",
|
|
60
|
+
"--pwfile",
|
|
61
|
+
pwfile,
|
|
62
|
+
"--encoding=UTF8",
|
|
63
|
+
"--locale=C",
|
|
64
|
+
]);
|
|
65
|
+
} finally {
|
|
66
|
+
await unlink(pwfile);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
await run("pg_ctl", [
|
|
70
|
+
"-D",
|
|
71
|
+
root,
|
|
72
|
+
"-l",
|
|
73
|
+
path.resolve(".runtime/postgres.log"),
|
|
74
|
+
"-o",
|
|
75
|
+
`-h 127.0.0.1 -p ${port}`,
|
|
76
|
+
"-w",
|
|
77
|
+
"start",
|
|
78
|
+
]);
|
|
79
|
+
const adminUrl = new URL(url);
|
|
80
|
+
adminUrl.pathname = "/postgres";
|
|
81
|
+
const client = new pg.Client({ connectionString: adminUrl.href });
|
|
82
|
+
await client.connect();
|
|
83
|
+
try {
|
|
84
|
+
if (
|
|
85
|
+
!(
|
|
86
|
+
await client.query("SELECT 1 FROM pg_database WHERE datname=$1", [
|
|
87
|
+
database,
|
|
88
|
+
])
|
|
89
|
+
).rowCount
|
|
90
|
+
)
|
|
91
|
+
await client.query(`CREATE DATABASE "${database}"`);
|
|
92
|
+
} finally {
|
|
93
|
+
await client.end();
|
|
94
|
+
}
|
|
95
|
+
console.log(
|
|
96
|
+
"Native PostgreSQL is ready. Run npm run db:migrate and npm run db:seed.",
|
|
97
|
+
);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Maintainer utility: resolve hashes from the official HTTPS distribution endpoints.
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
const artifacts = {};
|
|
5
|
+
await mkdir(".runtime/runtime-downloads", { recursive: true });
|
|
6
|
+
for (const arch of ["x86_64", "aarch64"]) {
|
|
7
|
+
for (const name of ["docker", "docker-rootless-extras"]) {
|
|
8
|
+
const file = `${name}-29.8.2-${arch}.tgz`;
|
|
9
|
+
const url = `https://download.docker.com/linux/static/stable/${arch}/${name}-29.8.2.tgz`;
|
|
10
|
+
const response = await fetch(url);
|
|
11
|
+
if (!response.ok) throw Error(`${response.status}: ${url}`);
|
|
12
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
13
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
14
|
+
await writeFile(`.runtime/runtime-downloads/${file}`, bytes);
|
|
15
|
+
artifacts[`${name}-${arch}`] = { file, url, sha256 };
|
|
16
|
+
console.log(file, sha256);
|
|
17
|
+
}
|
|
18
|
+
const file = `alpine-minirootfs-3.23.6-${arch}.tar.gz`;
|
|
19
|
+
const url = `https://dl-cdn.alpinelinux.org/alpine/v3.23/releases/${arch}/${file}`;
|
|
20
|
+
const response = await fetch(`${url}.sha256`);
|
|
21
|
+
if (!response.ok)
|
|
22
|
+
throw Error(`Cannot read Alpine checksum: ${response.status}`);
|
|
23
|
+
const sha256 = (await response.text()).split(/\s/)[0];
|
|
24
|
+
if (!/^[0-9a-f]{64}$/.test(sha256)) throw Error("Invalid Alpine checksum");
|
|
25
|
+
artifacts[`alpine-${arch}`] = { file, url, sha256 };
|
|
26
|
+
}
|
|
27
|
+
await writeFile(
|
|
28
|
+
"packages/runtime/artifacts.json",
|
|
29
|
+
JSON.stringify(artifacts, null, 2) + "\n",
|
|
30
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { managedRuntime } from "../packages/runtime/index.mjs";
|
|
2
|
+
try {
|
|
3
|
+
const runtime = managedRuntime();
|
|
4
|
+
const action = process.argv[2] || "setup";
|
|
5
|
+
if (action === "status")
|
|
6
|
+
console.log(JSON.stringify(await runtime.status(), null, 2));
|
|
7
|
+
else if (action === "stop") {
|
|
8
|
+
await runtime.stop();
|
|
9
|
+
console.log("Naviyra Docker stopped. Database data is preserved.");
|
|
10
|
+
} else if (["setup", "start"].includes(action)) {
|
|
11
|
+
console.log(
|
|
12
|
+
"Preparing Naviyra's private Docker runtime. First setup downloads verified runtime files.",
|
|
13
|
+
);
|
|
14
|
+
await runtime.ensure();
|
|
15
|
+
console.log(JSON.stringify(await runtime.status(), null, 2));
|
|
16
|
+
} else throw Error("Use setup, start, status, or stop");
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error(error.message);
|
|
19
|
+
process.exitCode = 1;
|
|
20
|
+
}
|
package/scripts/seed.mjs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import pg from "pg";
|
|
2
|
+
import bcrypt from "bcryptjs";
|
|
3
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
4
|
+
if (
|
|
5
|
+
!process.env.ADMIN_EMAIL ||
|
|
6
|
+
(process.env.ADMIN_PASSWORD?.length || 0) < 12 ||
|
|
7
|
+
(process.env.AGENT_TOKEN?.length || 0) < 32
|
|
8
|
+
)
|
|
9
|
+
throw Error(
|
|
10
|
+
"Set ADMIN_EMAIL, ADMIN_PASSWORD (12+ characters), and AGENT_TOKEN (32+ characters).",
|
|
11
|
+
);
|
|
12
|
+
const client = new pg.Client({ connectionString: process.env.DATABASE_URL });
|
|
13
|
+
await client.connect();
|
|
14
|
+
try {
|
|
15
|
+
await client.query(
|
|
16
|
+
"INSERT INTO users(id,email,password_hash,role) VALUES($1,$2,$3,$4) ON CONFLICT(email) DO UPDATE SET password_hash=$3",
|
|
17
|
+
[
|
|
18
|
+
randomUUID(),
|
|
19
|
+
process.env.ADMIN_EMAIL.toLowerCase(),
|
|
20
|
+
await bcrypt.hash(process.env.ADMIN_PASSWORD, 12),
|
|
21
|
+
"admin",
|
|
22
|
+
],
|
|
23
|
+
);
|
|
24
|
+
await client.query(
|
|
25
|
+
"INSERT INTO agents(id,name,token_hash) VALUES($1,$2,$3) ON CONFLICT(name) DO UPDATE SET token_hash=$3",
|
|
26
|
+
[
|
|
27
|
+
randomUUID(),
|
|
28
|
+
"primary",
|
|
29
|
+
createHash("sha256").update(process.env.AGENT_TOKEN).digest("hex"),
|
|
30
|
+
],
|
|
31
|
+
);
|
|
32
|
+
console.log("Administrator and primary agent ready.");
|
|
33
|
+
} finally {
|
|
34
|
+
await client.end();
|
|
35
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { mkdir, stat, rename, writeFile, unlink } from "node:fs/promises";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import net from "node:net";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
8
|
+
|
|
9
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
|
|
11
|
+
function portOpen(port) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
const socket = net.createConnection({ host: "127.0.0.1", port });
|
|
14
|
+
const finish = (open) => {
|
|
15
|
+
socket.destroy();
|
|
16
|
+
resolve(open);
|
|
17
|
+
};
|
|
18
|
+
socket.once("connect", () => finish(true));
|
|
19
|
+
socket.once("error", () => finish(false));
|
|
20
|
+
socket.setTimeout(1500, () => finish(false));
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function stackUp() {
|
|
25
|
+
try {
|
|
26
|
+
process.loadEnvFile(path.join(root, ".env"));
|
|
27
|
+
} catch {
|
|
28
|
+
// The launcher creates .env when it is missing.
|
|
29
|
+
}
|
|
30
|
+
const panelPort = Number(process.env.PANEL_PORT || 3000);
|
|
31
|
+
const agentPort = Number(process.env.AGENT_PORT || 4001);
|
|
32
|
+
if (
|
|
33
|
+
!Number.isInteger(panelPort) ||
|
|
34
|
+
!Number.isInteger(agentPort) ||
|
|
35
|
+
panelPort < 1 ||
|
|
36
|
+
agentPort < 1
|
|
37
|
+
)
|
|
38
|
+
return false;
|
|
39
|
+
return (await portOpen(panelPort)) && (await portOpen(agentPort));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function killTree(pid) {
|
|
43
|
+
if (!pid) return;
|
|
44
|
+
if (process.platform === "win32") {
|
|
45
|
+
const killer = spawn("taskkill.exe", ["/PID", String(pid), "/T", "/F"], {
|
|
46
|
+
windowsHide: true,
|
|
47
|
+
stdio: "ignore",
|
|
48
|
+
});
|
|
49
|
+
killer.once("error", () => {});
|
|
50
|
+
killer.unref();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
process.kill(pid, "SIGTERM");
|
|
55
|
+
} catch {
|
|
56
|
+
// The child has already exited.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function supervise(options = {}) {
|
|
61
|
+
const log = options.log ?? (() => {});
|
|
62
|
+
const spawnChild = options.spawnChild;
|
|
63
|
+
const healthy = options.stackUp ?? stackUp;
|
|
64
|
+
const sleep =
|
|
65
|
+
options.sleep ??
|
|
66
|
+
(async (ms, shouldStop) => {
|
|
67
|
+
const step = 200;
|
|
68
|
+
for (let waited = 0; waited < ms && !shouldStop(); waited += step)
|
|
69
|
+
await delay(Math.min(step, ms - waited));
|
|
70
|
+
});
|
|
71
|
+
let stopping = false;
|
|
72
|
+
const shouldStop = options.shouldStop ?? (() => stopping);
|
|
73
|
+
let current = null;
|
|
74
|
+
if (options.hookSignals !== false && !options.shouldStop) {
|
|
75
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
76
|
+
process.on(signal, () => {
|
|
77
|
+
stopping = true;
|
|
78
|
+
current?.kill();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
let backoff = 3000;
|
|
82
|
+
log("Naviyra service supervisor started.");
|
|
83
|
+
while (!shouldStop()) {
|
|
84
|
+
const started = Date.now();
|
|
85
|
+
current = spawnChild();
|
|
86
|
+
const code = await current.exited;
|
|
87
|
+
current = null;
|
|
88
|
+
if (shouldStop()) break;
|
|
89
|
+
const ran = Date.now() - started;
|
|
90
|
+
if (code === 0 && (await healthy())) {
|
|
91
|
+
log("Launcher exited cleanly. Watching until the panel or agent stops.");
|
|
92
|
+
while (!shouldStop() && (await healthy())) await sleep(5000, shouldStop);
|
|
93
|
+
if (shouldStop()) break;
|
|
94
|
+
backoff = 3000;
|
|
95
|
+
log("Panel or agent is not listening. Starting again in 3s.");
|
|
96
|
+
await sleep(3000, shouldStop);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (ran > 60000) backoff = 3000;
|
|
100
|
+
const wait = code === 0 ? Math.max(backoff, 3000) : backoff;
|
|
101
|
+
log(
|
|
102
|
+
`Launcher exited with code ${code}. Restarting in ${Math.round(wait / 1000)}s.`,
|
|
103
|
+
);
|
|
104
|
+
await sleep(wait, shouldStop);
|
|
105
|
+
backoff = Math.min(Math.max(wait, 3000) * 2, 60000);
|
|
106
|
+
}
|
|
107
|
+
log("Naviyra service supervisor stopped.");
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function spawnLauncher(logStream) {
|
|
112
|
+
const child = spawn(
|
|
113
|
+
process.execPath,
|
|
114
|
+
["scripts/start-naviyra.mjs", "--no-browser"],
|
|
115
|
+
{
|
|
116
|
+
cwd: root,
|
|
117
|
+
windowsHide: true,
|
|
118
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
child.stdout?.on("data", (chunk) => logStream.write(chunk));
|
|
122
|
+
child.stderr?.on("data", (chunk) => logStream.write(chunk));
|
|
123
|
+
return {
|
|
124
|
+
exited: new Promise((resolve) => {
|
|
125
|
+
child.once("error", () => resolve(1));
|
|
126
|
+
child.once("exit", (code) => resolve(code ?? 1));
|
|
127
|
+
}),
|
|
128
|
+
kill() {
|
|
129
|
+
killTree(child.pid);
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function main() {
|
|
135
|
+
const runtime = path.join(root, ".runtime");
|
|
136
|
+
await mkdir(runtime, { recursive: true });
|
|
137
|
+
const logPath = path.join(runtime, "service.log");
|
|
138
|
+
try {
|
|
139
|
+
if ((await stat(logPath)).size > 2_000_000)
|
|
140
|
+
await rename(logPath, `${logPath}.1`);
|
|
141
|
+
} catch {
|
|
142
|
+
// A missing log is created by the write stream.
|
|
143
|
+
}
|
|
144
|
+
const logStream = createWriteStream(logPath, { flags: "a" });
|
|
145
|
+
const log = (line) => {
|
|
146
|
+
logStream.write(`[${new Date().toISOString()}] ${line}\n`);
|
|
147
|
+
};
|
|
148
|
+
const pidFile = path.join(runtime, "service.pid");
|
|
149
|
+
await writeFile(pidFile, String(process.pid));
|
|
150
|
+
try {
|
|
151
|
+
await supervise({ log, spawnChild: () => spawnLauncher(logStream), stackUp });
|
|
152
|
+
} finally {
|
|
153
|
+
await unlink(pidFile).catch(() => {});
|
|
154
|
+
logStream.end();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
159
|
+
process.chdir(root);
|
|
160
|
+
main().catch((error) => {
|
|
161
|
+
console.error(error.message);
|
|
162
|
+
process.exitCode = 1;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
8
|
+
|
|
9
|
+
export function envAssignment(name, value) {
|
|
10
|
+
const plain = /^[A-Za-z0-9@._:/+-]+$/.test(value);
|
|
11
|
+
const encoded = plain
|
|
12
|
+
? value
|
|
13
|
+
: `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
14
|
+
return `${name}=${encoded}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function applySettings(template, settings) {
|
|
18
|
+
const replacements = [
|
|
19
|
+
[
|
|
20
|
+
"JWT_SECRET=replace-with-at-least-32-random-characters",
|
|
21
|
+
envAssignment("JWT_SECRET", settings.jwt),
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
"AGENT_TOKEN=replace-with-at-least-32-random-characters",
|
|
25
|
+
envAssignment("AGENT_TOKEN", settings.token),
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
"ADMIN_PASSWORD=replace-with-a-strong-password",
|
|
29
|
+
envAssignment("ADMIN_PASSWORD", settings.password),
|
|
30
|
+
],
|
|
31
|
+
[
|
|
32
|
+
"ADMIN_EMAIL=admin@example.com",
|
|
33
|
+
envAssignment("ADMIN_EMAIL", settings.email),
|
|
34
|
+
],
|
|
35
|
+
["POSTGRES_PORT=55432", `POSTGRES_PORT=${settings.postgresPort}`],
|
|
36
|
+
[
|
|
37
|
+
"postgresql://naviyra:naviyra@127.0.0.1:55432/naviyra",
|
|
38
|
+
`postgresql://naviyra:naviyra@127.0.0.1:${settings.postgresPort}/naviyra`,
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
"AGENT_URL=http://127.0.0.1:4001",
|
|
42
|
+
`AGENT_URL=http://127.0.0.1:${settings.agentPort}`,
|
|
43
|
+
],
|
|
44
|
+
["AGENT_PORT=4001", `AGENT_PORT=${settings.agentPort}`],
|
|
45
|
+
["PANEL_PORT=3000", `PANEL_PORT=${settings.panelPort}`],
|
|
46
|
+
];
|
|
47
|
+
let env = template;
|
|
48
|
+
for (const [from, to] of replacements) {
|
|
49
|
+
if (!env.includes(from))
|
|
50
|
+
throw Error(`Environment template is missing ${from.split("=")[0]}.`);
|
|
51
|
+
env = env.replace(from, to);
|
|
52
|
+
}
|
|
53
|
+
return env;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function askPort(ask, label, fallback, used, log) {
|
|
57
|
+
for (;;) {
|
|
58
|
+
const raw = (await ask(label)).trim();
|
|
59
|
+
const port = raw === "" ? fallback : Number(raw);
|
|
60
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
61
|
+
log("Enter a port from 1 to 65535.");
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (used.includes(port)) {
|
|
65
|
+
log("That port is already used by another Naviyra service.");
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
return port;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function promptSettings(ask, askSecret, log = console.log) {
|
|
73
|
+
log("Enter the panel settings. Press Enter to accept a default.");
|
|
74
|
+
let email = "";
|
|
75
|
+
for (;;) {
|
|
76
|
+
const typed = (await ask("Administrator email [admin@example.com]: ")).trim();
|
|
77
|
+
email = (typed || "admin@example.com").toLowerCase();
|
|
78
|
+
if (emailPattern.test(email)) break;
|
|
79
|
+
log("Enter an email address.");
|
|
80
|
+
}
|
|
81
|
+
let password = "";
|
|
82
|
+
for (;;) {
|
|
83
|
+
password = await askSecret("Administrator password (12+ characters): ");
|
|
84
|
+
if (password.length < 12) {
|
|
85
|
+
log("Use at least 12 characters.");
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (password !== (await askSecret("Confirm password: "))) {
|
|
89
|
+
log("Passwords do not match.");
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
const agentPort = await askPort(
|
|
95
|
+
ask,
|
|
96
|
+
"Agent port [4001]: ",
|
|
97
|
+
4001,
|
|
98
|
+
[3000, 55432],
|
|
99
|
+
log,
|
|
100
|
+
);
|
|
101
|
+
return {
|
|
102
|
+
email,
|
|
103
|
+
password,
|
|
104
|
+
panelPort: 3000,
|
|
105
|
+
agentPort,
|
|
106
|
+
postgresPort: 55432,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function defaultAsk(question) {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const rl = createInterface({
|
|
113
|
+
input: process.stdin,
|
|
114
|
+
output: process.stdout,
|
|
115
|
+
});
|
|
116
|
+
rl.question(question)
|
|
117
|
+
.then((answer) => {
|
|
118
|
+
rl.close();
|
|
119
|
+
resolve(answer);
|
|
120
|
+
})
|
|
121
|
+
.catch((error) => {
|
|
122
|
+
rl.close();
|
|
123
|
+
reject(error);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function defaultAskSecret(question) {
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
const input = process.stdin;
|
|
131
|
+
const output = process.stdout;
|
|
132
|
+
output.write(question);
|
|
133
|
+
input.setRawMode(true);
|
|
134
|
+
input.resume();
|
|
135
|
+
let value = "";
|
|
136
|
+
const finish = (result) => {
|
|
137
|
+
input.setRawMode(false);
|
|
138
|
+
input.pause();
|
|
139
|
+
input.off("data", onData);
|
|
140
|
+
output.write("\n");
|
|
141
|
+
resolve(result);
|
|
142
|
+
};
|
|
143
|
+
const onData = (chunk) => {
|
|
144
|
+
for (const char of chunk.toString("utf8")) {
|
|
145
|
+
if (char === "\u0003") {
|
|
146
|
+
input.setRawMode(false);
|
|
147
|
+
process.exit(130);
|
|
148
|
+
}
|
|
149
|
+
if (char === "\r" || char === "\n") {
|
|
150
|
+
finish(value);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (char === "\u007f" || char === "\b") {
|
|
154
|
+
if (value) output.write("\b \b");
|
|
155
|
+
value = value.slice(0, -1);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (char < " ") continue;
|
|
159
|
+
value += char;
|
|
160
|
+
output.write("*");
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
input.on("data", onData);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function createLocalEnv(options = {}) {
|
|
168
|
+
const readExisting = options.readExisting ?? (() => readFile(".env"));
|
|
169
|
+
try {
|
|
170
|
+
await readExisting();
|
|
171
|
+
console.log(".env already exists; left unchanged.");
|
|
172
|
+
return { created: false };
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error.code !== "ENOENT") throw error;
|
|
175
|
+
}
|
|
176
|
+
const secret = () => randomBytes(32).toString("hex");
|
|
177
|
+
const tty = options.tty ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
178
|
+
const settings = tty
|
|
179
|
+
? await promptSettings(
|
|
180
|
+
options.ask ?? defaultAsk,
|
|
181
|
+
options.askSecret ?? defaultAskSecret,
|
|
182
|
+
options.log ?? console.log,
|
|
183
|
+
)
|
|
184
|
+
: {
|
|
185
|
+
email: "admin@example.com",
|
|
186
|
+
password: secret().slice(0, 24),
|
|
187
|
+
panelPort: 3000,
|
|
188
|
+
agentPort: 4001,
|
|
189
|
+
postgresPort: 55432,
|
|
190
|
+
};
|
|
191
|
+
const template = await (options.readTemplate ?? (() => readFile(".env.example", "utf8")))();
|
|
192
|
+
const env = applySettings(template, {
|
|
193
|
+
...settings,
|
|
194
|
+
jwt: secret(),
|
|
195
|
+
token: secret(),
|
|
196
|
+
});
|
|
197
|
+
await (options.writeEnv ??
|
|
198
|
+
((text) => writeFile(".env", text, { flag: "wx", mode: 0o600 })))(env);
|
|
199
|
+
await mkdir(".runtime", { recursive: true });
|
|
200
|
+
const login = `Local installation\nURL: http://127.0.0.1:${settings.panelPort}\nEmail: ${settings.email}\nPassword: ${settings.password}\n`;
|
|
201
|
+
await (options.writeLogin ??
|
|
202
|
+
((text) => writeFile(".runtime/local-login.txt", text, { mode: 0o600 })))(login);
|
|
203
|
+
console.log("Created .env. Sign-in details saved to .runtime/local-login.txt.");
|
|
204
|
+
return { created: true, settings };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const isMain =
|
|
208
|
+
process.argv[1] &&
|
|
209
|
+
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
210
|
+
if (isMain) {
|
|
211
|
+
createLocalEnv().catch((error) => {
|
|
212
|
+
console.error(error.message);
|
|
213
|
+
process.exitCode = 1;
|
|
214
|
+
});
|
|
215
|
+
}
|