@bitcall/webrtc-sip-gateway 0.2.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/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @bitcall/webrtc-sip-gateway
2
+
3
+ Linux-only CLI to install and operate the Bitcall WebRTC-to-SIP gateway.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ sudo npm i -g @bitcall/webrtc-sip-gateway@0.2.1
9
+ ```
10
+
11
+ ## Main workflow
12
+
13
+ ```bash
14
+ sudo bitcall-gateway init
15
+ sudo bitcall-gateway status
16
+ sudo bitcall-gateway logs -f
17
+ ```
18
+
19
+ ## Commands
20
+
21
+ - `sudo bitcall-gateway init`
22
+ - `sudo bitcall-gateway up`
23
+ - `sudo bitcall-gateway down`
24
+ - `sudo bitcall-gateway restart`
25
+ - `sudo bitcall-gateway status`
26
+ - `sudo bitcall-gateway logs [-f] [service]`
27
+ - `sudo bitcall-gateway cert status`
28
+ - `sudo bitcall-gateway cert renew`
29
+ - `sudo bitcall-gateway cert install --cert /path/cert.pem --key /path/key.pem`
30
+ - `sudo bitcall-gateway update`
31
+ - `sudo bitcall-gateway uninstall`
32
+
33
+ ## Files created by init
34
+
35
+ - `/opt/bitcall-gateway/.env`
36
+ - `/opt/bitcall-gateway/docker-compose.yml`
37
+ - `/opt/bitcall-gateway/acme-webroot/.well-known/acme-challenge/healthcheck`
38
+ - `/etc/systemd/system/bitcall-gateway.service`
39
+
40
+ ## Publishing npm package
41
+
42
+ 1. Bump version in `cli/package.json`.
43
+ 2. Commit the change.
44
+ 3. Create tag `vX.Y.Z`.
45
+ 4. Push tags; GitHub Actions publishes automatically.
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ if (process.getuid && process.getuid() !== 0) {
3
+ console.error("Error: bitcall-gateway requires root privileges.");
4
+ console.error(`Run with: sudo bitcall-gateway ${process.argv.slice(2).join(" ")}`);
5
+ process.exit(1);
6
+ }
7
+
8
+ require("../src/index").main().catch((error) => {
9
+ console.error(error.message);
10
+ process.exit(1);
11
+ });
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+
3
+ const path = require("path");
4
+
5
+ const GATEWAY_DIR = "/opt/bitcall-gateway";
6
+ const SERVICE_NAME = "bitcall-gateway";
7
+ const SERVICE_FILE = `/etc/systemd/system/${SERVICE_NAME}.service`;
8
+
9
+ module.exports = {
10
+ GATEWAY_DIR,
11
+ SERVICE_NAME,
12
+ SERVICE_FILE,
13
+ ACME_WEBROOT: path.join(GATEWAY_DIR, "acme-webroot"),
14
+ SSL_DIR: path.join(GATEWAY_DIR, "ssl"),
15
+ ENV_PATH: path.join(GATEWAY_DIR, ".env"),
16
+ COMPOSE_PATH: path.join(GATEWAY_DIR, "docker-compose.yml"),
17
+ DEFAULT_GATEWAY_IMAGE: "ghcr.io/bitcallio/webrtc-sip-gateway:0.2.1",
18
+ DEFAULT_PROVIDER_HOST: "sip.example.com",
19
+ DEFAULT_WEBPHONE_ORIGIN: "*",
20
+ RENEW_HOOK_PATH: "/etc/letsencrypt/renewal-hooks/deploy/bitcall-gateway.sh",
21
+ };
package/lib/envfile.js ADDED
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+
5
+ function parseEnv(content) {
6
+ const data = {};
7
+
8
+ for (const rawLine of content.split(/\r?\n/)) {
9
+ const line = rawLine.trim();
10
+ if (!line || line.startsWith("#")) {
11
+ continue;
12
+ }
13
+
14
+ const idx = line.indexOf("=");
15
+ if (idx < 0) {
16
+ continue;
17
+ }
18
+
19
+ const key = line.slice(0, idx).trim();
20
+ const value = line.slice(idx + 1);
21
+ data[key] = value;
22
+ }
23
+
24
+ return data;
25
+ }
26
+
27
+ function loadEnvFile(filePath) {
28
+ return parseEnv(fs.readFileSync(filePath, "utf8"));
29
+ }
30
+
31
+ function serializeEnv(entries) {
32
+ return entries
33
+ .filter((entry) => entry && entry.key)
34
+ .map((entry) => `${entry.key}=${entry.value === undefined || entry.value === null ? "" : entry.value}`)
35
+ .join("\n") + "\n";
36
+ }
37
+
38
+ function writeEnvFile(filePath, entries, mode = 0o600) {
39
+ const payload = serializeEnv(entries);
40
+ fs.writeFileSync(filePath, payload, { mode });
41
+ }
42
+
43
+ module.exports = {
44
+ parseEnv,
45
+ loadEnvFile,
46
+ serializeEnv,
47
+ writeEnvFile,
48
+ };
package/lib/prompt.js ADDED
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+
3
+ const readline = require("readline");
4
+ const { stdin: input, stdout: output } = require("process");
5
+
6
+ class Prompter {
7
+ constructor() {
8
+ this.rl = readline.createInterface({ input, output });
9
+ }
10
+
11
+ close() {
12
+ this.rl.close();
13
+ }
14
+
15
+ async askText(label, defaultValue = "", { required = false } = {}) {
16
+ for (;;) {
17
+ const suffix = defaultValue !== "" ? ` [${defaultValue}]` : "";
18
+ const answer = (await this._question(`${label}${suffix}: `)).trim();
19
+ const value = answer || defaultValue;
20
+
21
+ if (required && !value) {
22
+ console.log("Value is required.");
23
+ continue;
24
+ }
25
+
26
+ return value;
27
+ }
28
+ }
29
+
30
+ async askChoice(label, options, defaultIndex = 0) {
31
+ console.log(label);
32
+ options.forEach((option, index) => {
33
+ console.log(` ${index + 1}. ${option}`);
34
+ });
35
+
36
+ for (;;) {
37
+ const answer = (await this._question(`Choose [${defaultIndex + 1}]: `)).trim();
38
+ const selected = answer ? Number.parseInt(answer, 10) : defaultIndex + 1;
39
+ if (!Number.isNaN(selected) && selected >= 1 && selected <= options.length) {
40
+ return options[selected - 1];
41
+ }
42
+ console.log("Invalid selection.");
43
+ }
44
+ }
45
+
46
+ async askYesNo(label, defaultYes = true) {
47
+ const hint = defaultYes ? "[Y/n]" : "[y/N]";
48
+ for (;;) {
49
+ const answer = (await this._question(`${label} ${hint}: `)).trim().toLowerCase();
50
+ if (!answer) {
51
+ return defaultYes;
52
+ }
53
+ if (answer === "y" || answer === "yes") {
54
+ return true;
55
+ }
56
+ if (answer === "n" || answer === "no") {
57
+ return false;
58
+ }
59
+ console.log("Please answer yes or no.");
60
+ }
61
+ }
62
+
63
+ _question(message) {
64
+ return new Promise((resolve) => {
65
+ this.rl.question(message, (answer) => resolve(answer));
66
+ });
67
+ }
68
+ }
69
+
70
+ module.exports = {
71
+ Prompter,
72
+ };
package/lib/shell.js ADDED
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+
3
+ const { spawnSync } = require("child_process");
4
+
5
+ function run(command, args = [], options = {}) {
6
+ const {
7
+ cwd,
8
+ env,
9
+ stdio = "pipe",
10
+ check = true,
11
+ input,
12
+ quiet = false,
13
+ } = options;
14
+
15
+ const result = spawnSync(command, args, {
16
+ cwd,
17
+ env,
18
+ input,
19
+ stdio,
20
+ encoding: "utf8",
21
+ });
22
+
23
+ if (result.error) {
24
+ throw result.error;
25
+ }
26
+
27
+ if (check && typeof result.status === "number" && result.status !== 0) {
28
+ const detail = [result.stdout, result.stderr]
29
+ .filter(Boolean)
30
+ .join("\n")
31
+ .trim();
32
+ const suffix = detail ? `\n${detail}` : "";
33
+ throw new Error(`Command failed: ${command} ${args.join(" ")}${suffix}`);
34
+ }
35
+
36
+ if (!quiet && stdio === "inherit") {
37
+ return result;
38
+ }
39
+
40
+ return result;
41
+ }
42
+
43
+ function runShell(script, options = {}) {
44
+ return run("sh", ["-lc", script], options);
45
+ }
46
+
47
+ function commandExists(command) {
48
+ const result = spawnSync("sh", ["-lc", `command -v ${command}`], {
49
+ stdio: "ignore",
50
+ });
51
+ return result.status === 0;
52
+ }
53
+
54
+ function output(command, args = [], options = {}) {
55
+ const result = run(command, args, { ...options, stdio: "pipe" });
56
+ return (result.stdout || "").trim();
57
+ }
58
+
59
+ module.exports = {
60
+ run,
61
+ runShell,
62
+ output,
63
+ commandExists,
64
+ };
package/lib/system.js ADDED
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const { output, run, runShell, commandExists } = require("./shell");
5
+
6
+ function parseOsRelease() {
7
+ const info = {};
8
+ const content = fs.readFileSync("/etc/os-release", "utf8");
9
+
10
+ for (const line of content.split(/\r?\n/)) {
11
+ if (!line || line.startsWith("#")) {
12
+ continue;
13
+ }
14
+ const idx = line.indexOf("=");
15
+ if (idx < 0) {
16
+ continue;
17
+ }
18
+ const key = line.slice(0, idx);
19
+ const value = line.slice(idx + 1).replace(/^"|"$/g, "");
20
+ info[key] = value;
21
+ }
22
+
23
+ return info;
24
+ }
25
+
26
+ function isSupportedDistro() {
27
+ const os = parseOsRelease();
28
+ const id = (os.ID || "").toLowerCase();
29
+ const version = Number.parseFloat(os.VERSION_ID || "0");
30
+
31
+ if (id === "ubuntu") {
32
+ return version >= 22;
33
+ }
34
+
35
+ if (id === "debian") {
36
+ return version >= 12;
37
+ }
38
+
39
+ return false;
40
+ }
41
+
42
+ function detectPublicIp() {
43
+ const probes = [
44
+ "https://api.ipify.org",
45
+ "https://ifconfig.me/ip",
46
+ "https://icanhazip.com",
47
+ ];
48
+
49
+ for (const url of probes) {
50
+ const result = run("curl", ["-fsS", "--max-time", "5", url], {
51
+ check: false,
52
+ stdio: "pipe",
53
+ });
54
+
55
+ if (result.status === 0) {
56
+ const ip = (result.stdout || "").trim();
57
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
58
+ return ip;
59
+ }
60
+ }
61
+ }
62
+
63
+ return "";
64
+ }
65
+
66
+ function resolveDomainIpv4(domain) {
67
+ const result = run("getent", ["ahostsv4", domain], { check: false });
68
+ if (result.status !== 0) {
69
+ return [];
70
+ }
71
+
72
+ const values = new Set();
73
+ for (const line of (result.stdout || "").split(/\r?\n/)) {
74
+ const ip = line.trim().split(/\s+/)[0];
75
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(ip)) {
76
+ values.add(ip);
77
+ }
78
+ }
79
+
80
+ return [...values];
81
+ }
82
+
83
+ function diskFreeGb(targetPath = "/") {
84
+ const text = output("df", ["-Pk", targetPath]);
85
+ const lines = text.split(/\r?\n/).filter(Boolean);
86
+ if (lines.length < 2) {
87
+ return 0;
88
+ }
89
+
90
+ const parts = lines[1].trim().split(/\s+/);
91
+ const availableKb = Number.parseInt(parts[3], 10);
92
+ return Math.floor(availableKb / 1024 / 1024);
93
+ }
94
+
95
+ function memoryTotalMb() {
96
+ const content = fs.readFileSync("/proc/meminfo", "utf8");
97
+ const match = content.match(/^MemTotal:\s+(\d+)\s+kB$/m);
98
+ if (!match) {
99
+ return 0;
100
+ }
101
+ return Math.floor(Number.parseInt(match[1], 10) / 1024);
102
+ }
103
+
104
+ function portInUse(port) {
105
+ const result = runShell(`ss -H -ltnup | grep -E '[:.]${port}\\b'`, {
106
+ check: false,
107
+ });
108
+
109
+ return {
110
+ inUse: result.status === 0,
111
+ detail: (result.stdout || "").trim(),
112
+ };
113
+ }
114
+
115
+ function ensureDockerInstalled() {
116
+ if (commandExists("docker") && run("docker", ["info"], { check: false }).status === 0) {
117
+ return;
118
+ }
119
+
120
+ run("apt-get", ["update"], { stdio: "inherit" });
121
+ run("apt-get", ["install", "-y", "curl", "ca-certificates", "gnupg"], {
122
+ stdio: "inherit",
123
+ });
124
+ runShell("curl -fsSL https://get.docker.com | sh", { stdio: "inherit" });
125
+ run("systemctl", ["enable", "docker.service"], { stdio: "inherit" });
126
+ run("systemctl", ["enable", "containerd.service"], { stdio: "inherit" });
127
+ run("systemctl", ["start", "docker"], { stdio: "inherit" });
128
+ }
129
+
130
+ function ensureComposePlugin() {
131
+ if (run("docker", ["compose", "version"], { check: false }).status === 0) {
132
+ return;
133
+ }
134
+
135
+ run("apt-get", ["update"], { stdio: "inherit" });
136
+ run("apt-get", ["install", "-y", "docker-compose-plugin"], { stdio: "inherit" });
137
+ }
138
+
139
+ module.exports = {
140
+ parseOsRelease,
141
+ isSupportedDistro,
142
+ detectPublicIp,
143
+ resolveDomainIpv4,
144
+ diskFreeGb,
145
+ memoryTotalMb,
146
+ portInUse,
147
+ ensureDockerInstalled,
148
+ ensureComposePlugin,
149
+ };
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ const TEMPLATE_DIR = path.resolve(__dirname, "../templates");
7
+
8
+ function readTemplate(name) {
9
+ return fs.readFileSync(path.join(TEMPLATE_DIR, name), "utf8");
10
+ }
11
+
12
+ function renderTemplate(name, values) {
13
+ let content = readTemplate(name);
14
+ for (const [key, value] of Object.entries(values)) {
15
+ const token = `__${key}__`;
16
+ content = content.split(token).join(String(value === undefined || value === null ? "" : value));
17
+ }
18
+ return content;
19
+ }
20
+
21
+ module.exports = {
22
+ readTemplate,
23
+ renderTemplate,
24
+ };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@bitcall/webrtc-sip-gateway",
3
+ "version": "0.2.1",
4
+ "description": "Linux CLI for bootstrapping and managing the Bitcall WebRTC-to-SIP Gateway",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/Bitcallio/webrtc-sip-gateway"
8
+ },
9
+ "license": "MIT",
10
+ "bin": {
11
+ "bitcall-gateway": "bin/bitcall-gateway.js"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "src",
16
+ "lib",
17
+ "templates",
18
+ "README.md"
19
+ ],
20
+ "engines": {
21
+ "node": ">=18.0.0"
22
+ },
23
+ "scripts": {
24
+ "lint": "node --check src/index.js && node --check bin/bitcall-gateway.js && for f in lib/*.js; do node --check \"$f\"; done",
25
+ "test": "node test/smoke.test.js",
26
+ "pack:dry": "npm pack --dry-run"
27
+ },
28
+ "dependencies": {
29
+ "commander": "^12.1.0"
30
+ }
31
+ }