@bitcall/webrtc-sip-gateway 0.2.6 → 0.2.8
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 +18 -2
- package/lib/constants.js +1 -1
- package/lib/system.js +29 -12
- package/package.json +2 -2
- package/src/index.js +446 -131
package/README.md
CHANGED
|
@@ -5,13 +5,13 @@ Linux-only CLI to install and operate the Bitcall WebRTC-to-SIP gateway.
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
sudo npm i -g @bitcall/webrtc-sip-gateway@0.2.
|
|
8
|
+
sudo npm i -g @bitcall/webrtc-sip-gateway@0.2.8
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Main workflow
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
sudo bitcall-gateway init
|
|
14
|
+
sudo bitcall-gateway init --dev
|
|
15
15
|
sudo bitcall-gateway status
|
|
16
16
|
sudo bitcall-gateway logs -f
|
|
17
17
|
sudo bitcall-gateway media status
|
|
@@ -22,9 +22,25 @@ ports only. Host IPv6 remains enabled for signaling and non-media traffic.
|
|
|
22
22
|
Backend selection prefers nftables on non-UFW hosts and uses ip6tables when UFW
|
|
23
23
|
is active.
|
|
24
24
|
|
|
25
|
+
Default `init` and `init --dev` run in dev profile:
|
|
26
|
+
- `BITCALL_ENV=dev`
|
|
27
|
+
- `ROUTING_MODE=universal`
|
|
28
|
+
- provider allowlist/origin/source IPs are permissive by default (with warnings)
|
|
29
|
+
|
|
30
|
+
Use `sudo bitcall-gateway init --production` for strict input validation and
|
|
31
|
+
hardening checks. Production universal routing requires explicit
|
|
32
|
+
`ALLOWED_SIP_DOMAINS`.
|
|
33
|
+
Use `--verbose` to stream apt/docker output during install. Default mode keeps
|
|
34
|
+
console output concise and writes command details to
|
|
35
|
+
`/var/log/bitcall-gateway-install.log`.
|
|
36
|
+
|
|
25
37
|
## Commands
|
|
26
38
|
|
|
27
39
|
- `sudo bitcall-gateway init`
|
|
40
|
+
- `sudo bitcall-gateway init --dev`
|
|
41
|
+
- `sudo bitcall-gateway init --production`
|
|
42
|
+
- `sudo bitcall-gateway init --advanced`
|
|
43
|
+
- `sudo bitcall-gateway init --verbose`
|
|
28
44
|
- `sudo bitcall-gateway up`
|
|
29
45
|
- `sudo bitcall-gateway down`
|
|
30
46
|
- `sudo bitcall-gateway restart`
|
package/lib/constants.js
CHANGED
|
@@ -14,7 +14,7 @@ module.exports = {
|
|
|
14
14
|
SSL_DIR: path.join(GATEWAY_DIR, "ssl"),
|
|
15
15
|
ENV_PATH: path.join(GATEWAY_DIR, ".env"),
|
|
16
16
|
COMPOSE_PATH: path.join(GATEWAY_DIR, "docker-compose.yml"),
|
|
17
|
-
DEFAULT_GATEWAY_IMAGE: "ghcr.io/bitcallio/webrtc-sip-gateway:0.2.
|
|
17
|
+
DEFAULT_GATEWAY_IMAGE: "ghcr.io/bitcallio/webrtc-sip-gateway:0.2.8",
|
|
18
18
|
DEFAULT_PROVIDER_HOST: "sip.example.com",
|
|
19
19
|
DEFAULT_WEBPHONE_ORIGIN: "*",
|
|
20
20
|
RENEW_HOOK_PATH: "/etc/letsencrypt/renewal-hooks/deploy/bitcall-gateway.sh",
|
package/lib/system.js
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const { output, run, runShell, commandExists } = require("./shell");
|
|
5
5
|
|
|
6
|
+
function pickExec(options = {}) {
|
|
7
|
+
if (typeof options.exec === "function") {
|
|
8
|
+
return options.exec;
|
|
9
|
+
}
|
|
10
|
+
return (command, args = [], execOptions = {}) => run(command, args, execOptions);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function pickShell(options = {}) {
|
|
14
|
+
if (typeof options.shell === "function") {
|
|
15
|
+
return options.shell;
|
|
16
|
+
}
|
|
17
|
+
return (script, execOptions = {}) => runShell(script, execOptions);
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
function parseOsRelease() {
|
|
7
21
|
const info = {};
|
|
8
22
|
const content = fs.readFileSync("/etc/os-release", "utf8");
|
|
@@ -112,28 +126,31 @@ function portInUse(port) {
|
|
|
112
126
|
};
|
|
113
127
|
}
|
|
114
128
|
|
|
115
|
-
function ensureDockerInstalled() {
|
|
129
|
+
function ensureDockerInstalled(options = {}) {
|
|
130
|
+
const exec = pickExec(options);
|
|
131
|
+
const shell = pickShell(options);
|
|
132
|
+
|
|
116
133
|
if (commandExists("docker") && run("docker", ["info"], { check: false }).status === 0) {
|
|
117
134
|
return;
|
|
118
135
|
}
|
|
119
136
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
run("systemctl", ["enable", "containerd.service"], { stdio: "inherit" });
|
|
127
|
-
run("systemctl", ["start", "docker"], { stdio: "inherit" });
|
|
137
|
+
exec("apt-get", ["update"]);
|
|
138
|
+
exec("apt-get", ["install", "-y", "curl", "ca-certificates", "gnupg"]);
|
|
139
|
+
shell("curl -fsSL https://get.docker.com | sh");
|
|
140
|
+
exec("systemctl", ["enable", "docker.service"]);
|
|
141
|
+
exec("systemctl", ["enable", "containerd.service"]);
|
|
142
|
+
exec("systemctl", ["start", "docker"]);
|
|
128
143
|
}
|
|
129
144
|
|
|
130
|
-
function ensureComposePlugin() {
|
|
145
|
+
function ensureComposePlugin(options = {}) {
|
|
146
|
+
const exec = pickExec(options);
|
|
147
|
+
|
|
131
148
|
if (run("docker", ["compose", "version"], { check: false }).status === 0) {
|
|
132
149
|
return;
|
|
133
150
|
}
|
|
134
151
|
|
|
135
|
-
|
|
136
|
-
|
|
152
|
+
exec("apt-get", ["update"]);
|
|
153
|
+
exec("apt-get", ["install", "-y", "docker-compose-plugin"]);
|
|
137
154
|
}
|
|
138
155
|
|
|
139
156
|
module.exports = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitcall/webrtc-sip-gateway",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "Linux CLI for bootstrapping and managing the Bitcall WebRTC-to-SIP Gateway",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
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 && node test/firewall.test.js",
|
|
25
|
+
"test": "node test/smoke.test.js && node test/firewall.test.js && node test/init-config.test.js",
|
|
26
26
|
"pack:dry": "npm pack --dry-run"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
package/src/index.js
CHANGED
|
@@ -19,7 +19,7 @@ const {
|
|
|
19
19
|
RENEW_HOOK_PATH,
|
|
20
20
|
} = require("../lib/constants");
|
|
21
21
|
const { run, runShell, output, commandExists } = require("../lib/shell");
|
|
22
|
-
const { loadEnvFile,
|
|
22
|
+
const { loadEnvFile, writeEnvFile } = require("../lib/envfile");
|
|
23
23
|
const { Prompter } = require("../lib/prompt");
|
|
24
24
|
const {
|
|
25
25
|
parseOsRelease,
|
|
@@ -40,21 +40,109 @@ const {
|
|
|
40
40
|
isMediaIpv4OnlyRulesPresent,
|
|
41
41
|
} = require("../lib/firewall");
|
|
42
42
|
|
|
43
|
-
const PACKAGE_VERSION = "0.2.
|
|
43
|
+
const PACKAGE_VERSION = "0.2.8";
|
|
44
|
+
const INSTALL_LOG_PATH = "/var/log/bitcall-gateway-install.log";
|
|
44
45
|
|
|
45
|
-
function
|
|
46
|
-
|
|
46
|
+
function printBanner() {
|
|
47
|
+
console.log("========================================");
|
|
48
|
+
console.log(" BITCALL WEBRTC-SIP GATEWAY INSTALLER");
|
|
49
|
+
console.log(` version ${PACKAGE_VERSION}`);
|
|
50
|
+
console.log(" one-line deploy for browser SIP");
|
|
51
|
+
console.log("========================================\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createInstallContext(options = {}) {
|
|
55
|
+
const verbose = Boolean(options.verbose);
|
|
56
|
+
const steps = ["Checks", "Config", "Firewall", "TLS", "Start", "Done"];
|
|
57
|
+
const state = { index: 0 };
|
|
58
|
+
|
|
59
|
+
if (!verbose) {
|
|
60
|
+
fs.writeFileSync(INSTALL_LOG_PATH, "", { mode: 0o600 });
|
|
61
|
+
fs.chmodSync(INSTALL_LOG_PATH, 0o600);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function logLine(line) {
|
|
65
|
+
if (!verbose) {
|
|
66
|
+
fs.appendFileSync(INSTALL_LOG_PATH, `${line}\n`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatCommand(command, args) {
|
|
71
|
+
const joined = [command, ...(args || [])].join(" ").trim();
|
|
72
|
+
return joined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function exec(command, args = [], commandOptions = {}) {
|
|
76
|
+
const stdio = verbose ? "inherit" : "pipe";
|
|
77
|
+
const result = run(command, args, {
|
|
78
|
+
...commandOptions,
|
|
79
|
+
check: false,
|
|
80
|
+
stdio,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (!verbose) {
|
|
84
|
+
logLine(`$ ${formatCommand(command, args)}`);
|
|
85
|
+
if (result.stdout) {
|
|
86
|
+
fs.appendFileSync(INSTALL_LOG_PATH, result.stdout);
|
|
87
|
+
}
|
|
88
|
+
if (result.stderr) {
|
|
89
|
+
fs.appendFileSync(INSTALL_LOG_PATH, result.stderr);
|
|
90
|
+
}
|
|
91
|
+
logLine("");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (commandOptions.check !== false && result.status !== 0) {
|
|
95
|
+
throw new Error(`Command failed: ${formatCommand(command, args)}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function shell(script, commandOptions = {}) {
|
|
102
|
+
return exec("sh", ["-lc", script], commandOptions);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function step(label, fn) {
|
|
106
|
+
state.index += 1;
|
|
107
|
+
console.log(`[${state.index}/${steps.length}] ${label}...`);
|
|
108
|
+
const started = Date.now();
|
|
109
|
+
try {
|
|
110
|
+
const value = await fn();
|
|
111
|
+
const elapsed = ((Date.now() - started) / 1000).toFixed(1);
|
|
112
|
+
console.log(`✓ ${label} (${elapsed}s)\n`);
|
|
113
|
+
return value;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error(`✗ ${label} failed: ${error.message}`);
|
|
116
|
+
if (!verbose) {
|
|
117
|
+
console.error(`Install log: ${INSTALL_LOG_PATH}`);
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
verbose,
|
|
125
|
+
logPath: INSTALL_LOG_PATH,
|
|
126
|
+
exec,
|
|
127
|
+
shell,
|
|
128
|
+
step,
|
|
129
|
+
logLine,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function detectComposeCommand(exec = run) {
|
|
134
|
+
if (exec("docker", ["compose", "version"], { check: false }).status === 0) {
|
|
47
135
|
return { command: "docker", prefixArgs: ["compose"] };
|
|
48
136
|
}
|
|
49
|
-
if (
|
|
137
|
+
if (exec("docker-compose", ["version"], { check: false }).status === 0) {
|
|
50
138
|
return { command: "docker-compose", prefixArgs: [] };
|
|
51
139
|
}
|
|
52
140
|
throw new Error("docker compose not found. Install Docker compose plugin.");
|
|
53
141
|
}
|
|
54
142
|
|
|
55
|
-
function runCompose(args, options = {}) {
|
|
56
|
-
const compose = detectComposeCommand();
|
|
57
|
-
return
|
|
143
|
+
function runCompose(args, options = {}, exec = run) {
|
|
144
|
+
const compose = detectComposeCommand(exec);
|
|
145
|
+
return exec(compose.command, [...compose.prefixArgs, ...args], {
|
|
58
146
|
cwd: GATEWAY_DIR,
|
|
59
147
|
...options,
|
|
60
148
|
});
|
|
@@ -178,17 +266,13 @@ WantedBy=multi-user.target
|
|
|
178
266
|
`;
|
|
179
267
|
}
|
|
180
268
|
|
|
181
|
-
function installSystemdService() {
|
|
269
|
+
function installSystemdService(exec = run) {
|
|
182
270
|
writeFileWithMode(SERVICE_FILE, buildSystemdService(), 0o644);
|
|
183
|
-
|
|
184
|
-
|
|
271
|
+
exec("systemctl", ["daemon-reload"]);
|
|
272
|
+
exec("systemctl", ["enable", SERVICE_NAME]);
|
|
185
273
|
}
|
|
186
274
|
|
|
187
|
-
function
|
|
188
|
-
if (!commandExists("ufw")) {
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
275
|
+
function buildUfwRules(config) {
|
|
192
276
|
const rules = [
|
|
193
277
|
["22/tcp", "SSH"],
|
|
194
278
|
["80/tcp", "Bitcall ACME"],
|
|
@@ -202,15 +286,98 @@ function configureFirewall(config) {
|
|
|
202
286
|
rules.push(["5061/tcp", "Bitcall SIP TLS"]);
|
|
203
287
|
}
|
|
204
288
|
|
|
205
|
-
|
|
206
|
-
|
|
289
|
+
if (config.turnMode === "coturn") {
|
|
290
|
+
rules.push([`${config.turnUdpPort}/udp`, "Bitcall TURN UDP"]);
|
|
291
|
+
rules.push([`${config.turnUdpPort}/tcp`, "Bitcall TURN TCP"]);
|
|
292
|
+
rules.push([`${config.turnsTcpPort}/tcp`, "Bitcall TURNS TCP"]);
|
|
293
|
+
rules.push([
|
|
294
|
+
`${config.turnRelayMinPort}:${config.turnRelayMaxPort}/udp`,
|
|
295
|
+
"Bitcall TURN relay UDP",
|
|
296
|
+
]);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return rules;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function printRequiredPorts(config) {
|
|
303
|
+
console.log("Open these ports in your host/cloud firewall:");
|
|
304
|
+
for (const [port, label] of buildUfwRules(config)) {
|
|
305
|
+
console.log(` - ${port} (${label})`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function configureFirewall(config, exec = run) {
|
|
310
|
+
for (const [port, label] of buildUfwRules(config)) {
|
|
311
|
+
exec("ufw", ["allow", port, "comment", label], {
|
|
207
312
|
check: false,
|
|
208
|
-
stdio: "ignore",
|
|
209
313
|
});
|
|
210
314
|
}
|
|
211
315
|
|
|
212
|
-
|
|
213
|
-
|
|
316
|
+
exec("ufw", ["--force", "enable"], { check: false });
|
|
317
|
+
exec("ufw", ["reload"], { check: false });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function isSingleProviderConfigured(config) {
|
|
321
|
+
return config.routingMode === "single-provider" && Boolean((config.sipProviderUri || "").trim());
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isOriginWildcard(originPattern) {
|
|
325
|
+
const value = (originPattern || "").trim();
|
|
326
|
+
return value === ".*" || value === "^.*$" || value === "*";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function validateProductionConfig(config) {
|
|
330
|
+
const hasAllowedDomains = countAllowedDomains(config.allowedDomains) > 0;
|
|
331
|
+
const hasSingleProvider = isSingleProviderConfigured(config);
|
|
332
|
+
if (!hasAllowedDomains && !hasSingleProvider) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
"Production requires ALLOWED_SIP_DOMAINS or single-provider routing with SIP_PROVIDER_URI. Re-run: bitcall-gateway init --advanced --production"
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const originPattern = toOriginPattern(config.webphoneOrigin);
|
|
339
|
+
const hasStrictOrigin = !isOriginWildcard(originPattern);
|
|
340
|
+
const hasTurnToken = Boolean((config.turnApiToken || "").trim());
|
|
341
|
+
if (!hasStrictOrigin && !hasTurnToken) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
"Production requires a strict WEBPHONE_ORIGIN_PATTERN or TURN_API_TOKEN. Re-run: bitcall-gateway init --advanced --production"
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function buildDevWarnings(config) {
|
|
349
|
+
const warnings = [];
|
|
350
|
+
if (countAllowedDomains(config.allowedDomains) === 0) {
|
|
351
|
+
warnings.push("Provider allowlist: any (DEV mode)");
|
|
352
|
+
}
|
|
353
|
+
if (isOriginWildcard(toOriginPattern(config.webphoneOrigin))) {
|
|
354
|
+
warnings.push("Webphone origin: any (DEV mode)");
|
|
355
|
+
}
|
|
356
|
+
if (!(config.sipTrustedIps || "").trim()) {
|
|
357
|
+
warnings.push("Trusted SIP source IPs: any (DEV mode)");
|
|
358
|
+
}
|
|
359
|
+
return warnings;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function buildQuickFlowDefaults(initProfile, existing = {}) {
|
|
363
|
+
const defaults = {
|
|
364
|
+
bitcallEnv: initProfile,
|
|
365
|
+
routingMode: "universal",
|
|
366
|
+
sipProviderUri: "",
|
|
367
|
+
sipTransport: "udp",
|
|
368
|
+
sipPort: "5060",
|
|
369
|
+
allowedDomains: existing.ALLOWED_SIP_DOMAINS || "",
|
|
370
|
+
webphoneOrigin: existing.WEBPHONE_ORIGIN || DEFAULT_WEBPHONE_ORIGIN,
|
|
371
|
+
sipTrustedIps: existing.SIP_TRUSTED_IPS || "",
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
if (initProfile === "dev") {
|
|
375
|
+
defaults.allowedDomains = "";
|
|
376
|
+
defaults.webphoneOrigin = "*";
|
|
377
|
+
defaults.sipTrustedIps = "";
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return defaults;
|
|
214
381
|
}
|
|
215
382
|
|
|
216
383
|
function countAllowedDomains(raw) {
|
|
@@ -259,9 +426,33 @@ async function confirmContinueWithoutMediaBlock() {
|
|
|
259
426
|
}
|
|
260
427
|
}
|
|
261
428
|
|
|
262
|
-
function
|
|
429
|
+
async function confirmInstallUfw() {
|
|
430
|
+
const prompt = new Prompter();
|
|
431
|
+
try {
|
|
432
|
+
return await prompt.askYesNo("ufw is not installed. Install ufw now?", true);
|
|
433
|
+
} finally {
|
|
434
|
+
prompt.close();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function ensureUfwAvailable(ctx) {
|
|
439
|
+
if (commandExists("ufw")) {
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const installNow = await confirmInstallUfw();
|
|
444
|
+
if (!installNow) {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
ctx.exec("apt-get", ["update"]);
|
|
449
|
+
ctx.exec("apt-get", ["install", "-y", "ufw"]);
|
|
450
|
+
return commandExists("ufw");
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function generateSelfSigned(certPath, keyPath, domain, days = 1, exec = run) {
|
|
263
454
|
ensureDir(path.dirname(certPath), 0o700);
|
|
264
|
-
|
|
455
|
+
exec(
|
|
265
456
|
"openssl",
|
|
266
457
|
[
|
|
267
458
|
"req",
|
|
@@ -278,7 +469,7 @@ function generateSelfSigned(certPath, keyPath, domain, days = 1) {
|
|
|
278
469
|
"-subj",
|
|
279
470
|
`/CN=${domain}`,
|
|
280
471
|
],
|
|
281
|
-
{
|
|
472
|
+
{}
|
|
282
473
|
);
|
|
283
474
|
fs.chmodSync(certPath, 0o644);
|
|
284
475
|
fs.chmodSync(keyPath, 0o600);
|
|
@@ -353,7 +544,21 @@ function toOriginPattern(origin) {
|
|
|
353
544
|
return `^${escapeRegex(origin)}$`;
|
|
354
545
|
}
|
|
355
546
|
|
|
356
|
-
|
|
547
|
+
function normalizeInitProfile(initOptions = {}, existing = {}) {
|
|
548
|
+
void existing;
|
|
549
|
+
if (initOptions.dev && initOptions.production) {
|
|
550
|
+
throw new Error("Use only one mode: --dev or --production.");
|
|
551
|
+
}
|
|
552
|
+
if (initOptions.production) {
|
|
553
|
+
return "production";
|
|
554
|
+
}
|
|
555
|
+
if (initOptions.dev) {
|
|
556
|
+
return "dev";
|
|
557
|
+
}
|
|
558
|
+
return "dev";
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function runPreflight(ctx) {
|
|
357
562
|
if (process.platform !== "linux") {
|
|
358
563
|
throw new Error("bitcall-gateway supports Linux hosts only.");
|
|
359
564
|
}
|
|
@@ -365,13 +570,11 @@ async function runPreflight() {
|
|
|
365
570
|
);
|
|
366
571
|
}
|
|
367
572
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
stdio: "inherit",
|
|
371
|
-
});
|
|
573
|
+
ctx.exec("apt-get", ["update"]);
|
|
574
|
+
ctx.exec("apt-get", ["install", "-y", "curl", "ca-certificates", "lsof", "openssl", "gnupg"]);
|
|
372
575
|
|
|
373
|
-
ensureDockerInstalled();
|
|
374
|
-
ensureComposePlugin();
|
|
576
|
+
ensureDockerInstalled({ exec: ctx.exec, shell: ctx.shell });
|
|
577
|
+
ensureComposePlugin({ exec: ctx.exec });
|
|
375
578
|
|
|
376
579
|
const freeGb = diskFreeGb("/");
|
|
377
580
|
if (freeGb < 2) {
|
|
@@ -392,17 +595,85 @@ async function runPreflight() {
|
|
|
392
595
|
}
|
|
393
596
|
|
|
394
597
|
return {
|
|
598
|
+
os,
|
|
395
599
|
p80,
|
|
396
600
|
p443,
|
|
397
601
|
};
|
|
398
602
|
}
|
|
399
603
|
|
|
400
|
-
|
|
604
|
+
function printSummary(config, devWarnings) {
|
|
605
|
+
const allowedCount = countAllowedDomains(config.allowedDomains);
|
|
606
|
+
const showDevWarnings = config.bitcallEnv === "dev";
|
|
607
|
+
const providerAllowlistSummary =
|
|
608
|
+
allowedCount > 0
|
|
609
|
+
? config.allowedDomains
|
|
610
|
+
: config.bitcallEnv === "production"
|
|
611
|
+
? isSingleProviderConfigured(config)
|
|
612
|
+
? "(single-provider mode)"
|
|
613
|
+
: "(missing)"
|
|
614
|
+
: "(any)";
|
|
615
|
+
console.log("\nSummary:");
|
|
616
|
+
console.log(` Domain: ${config.domain}`);
|
|
617
|
+
console.log(` Environment: ${config.bitcallEnv}`);
|
|
618
|
+
console.log(` Routing: ${config.routingMode}`);
|
|
619
|
+
console.log(
|
|
620
|
+
` Provider allowlist: ${providerAllowlistSummary}${showDevWarnings && allowedCount === 0 ? " [DEV WARNING]" : ""}`
|
|
621
|
+
);
|
|
622
|
+
console.log(
|
|
623
|
+
` Webphone origin: ${config.webphoneOrigin === "*" ? `(any)${showDevWarnings ? " [DEV WARNING]" : ""}` : config.webphoneOrigin}`
|
|
624
|
+
);
|
|
625
|
+
console.log(` SIP source IPs: ${(config.sipTrustedIps || "").trim() ? config.sipTrustedIps : "(any)"}`);
|
|
626
|
+
console.log(` TLS: ${config.tlsMode === "letsencrypt" ? "Let's Encrypt" : config.tlsMode}`);
|
|
627
|
+
console.log(` TURN: ${config.turnMode === "coturn" ? "enabled (coturn)" : config.turnMode}`);
|
|
628
|
+
console.log(
|
|
629
|
+
` Media: ${config.mediaIpv4Only === "1" ? "IPv4-only (IPv6 media blocked)" : "dual-stack"}`
|
|
630
|
+
);
|
|
631
|
+
console.log(` Firewall: ${config.configureUfw ? "UFW enabled" : "manual setup"}`);
|
|
632
|
+
|
|
633
|
+
if (devWarnings.length > 0) {
|
|
634
|
+
console.log("\nWarnings:");
|
|
635
|
+
for (const warning of devWarnings) {
|
|
636
|
+
console.log(` - ${warning}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function shouldRequireAllowlist(bitcallEnv, routingMode) {
|
|
642
|
+
return bitcallEnv === "production" && routingMode === "universal";
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function parseProviderFromUri(uri = "") {
|
|
646
|
+
const clean = uri.replace(/^sip:/, "");
|
|
647
|
+
const [hostPort, transportPart] = clean.split(";");
|
|
648
|
+
const [host, port] = hostPort.split(":");
|
|
649
|
+
let transport = "udp";
|
|
650
|
+
if (transportPart && transportPart.includes("transport=")) {
|
|
651
|
+
transport = transportPart.split("transport=")[1] || "udp";
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
host: host || DEFAULT_PROVIDER_HOST,
|
|
655
|
+
port: port || (transport === "tls" ? "5061" : "5060"),
|
|
656
|
+
transport,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
|
|
401
661
|
const prompt = new Prompter();
|
|
402
662
|
|
|
403
663
|
try {
|
|
664
|
+
const initProfile = normalizeInitProfile(initOptions, existing);
|
|
665
|
+
let advanced = Boolean(initOptions.advanced);
|
|
666
|
+
if (initOptions.production && !initOptions.advanced) {
|
|
667
|
+
advanced = true;
|
|
668
|
+
} else if (!initOptions.dev && !initOptions.production && !initOptions.advanced) {
|
|
669
|
+
advanced = await prompt.askYesNo("Advanced setup", false);
|
|
670
|
+
}
|
|
671
|
+
|
|
404
672
|
const detectedIp = detectPublicIp();
|
|
405
|
-
const
|
|
673
|
+
const domainDefault = initOptions.domain || existing.DOMAIN || "";
|
|
674
|
+
const domain = domainDefault
|
|
675
|
+
? domainDefault
|
|
676
|
+
: await prompt.askText("Gateway domain", existing.DOMAIN || "", { required: true });
|
|
406
677
|
let publicIp = existing.PUBLIC_IP || detectedIp || "";
|
|
407
678
|
if (!publicIp) {
|
|
408
679
|
publicIp = await prompt.askText("Public IPv4 (auto-detect failed)", "", { required: true });
|
|
@@ -418,22 +689,21 @@ async function runWizard(existing = {}, preflight = {}) {
|
|
|
418
689
|
? "reverse-proxy"
|
|
419
690
|
: "standalone";
|
|
420
691
|
|
|
421
|
-
const advanced = await prompt.askYesNo("Advanced setup", false);
|
|
422
|
-
|
|
423
692
|
let deployMode = autoDeployMode;
|
|
424
693
|
let tlsMode = "letsencrypt";
|
|
425
|
-
let acmeEmail = existing.ACME_EMAIL || "";
|
|
694
|
+
let acmeEmail = initOptions.email || existing.ACME_EMAIL || "";
|
|
426
695
|
let customCertPath = "";
|
|
427
696
|
let customKeyPath = "";
|
|
428
|
-
let bitcallEnv =
|
|
697
|
+
let bitcallEnv = initProfile;
|
|
429
698
|
let routingMode = existing.ROUTING_MODE || "universal";
|
|
430
|
-
|
|
431
|
-
let
|
|
432
|
-
let
|
|
699
|
+
const providerFromEnv = parseProviderFromUri(existing.SIP_PROVIDER_URI || "");
|
|
700
|
+
let sipProviderHost = providerFromEnv.host || DEFAULT_PROVIDER_HOST;
|
|
701
|
+
let sipTransport = providerFromEnv.transport || "udp";
|
|
702
|
+
let sipPort = providerFromEnv.port || "5060";
|
|
433
703
|
let sipProviderUri = "";
|
|
434
704
|
let allowedDomains = existing.ALLOWED_SIP_DOMAINS || "";
|
|
435
705
|
let sipTrustedIps = existing.SIP_TRUSTED_IPS || "";
|
|
436
|
-
let turnMode =
|
|
706
|
+
let turnMode = "coturn";
|
|
437
707
|
let turnSecret = "";
|
|
438
708
|
let turnTtl = existing.TURN_TTL || "86400";
|
|
439
709
|
let turnApiToken = existing.TURN_API_TOKEN || "";
|
|
@@ -441,14 +711,22 @@ async function runWizard(existing = {}, preflight = {}) {
|
|
|
441
711
|
let turnExternalUsername = "";
|
|
442
712
|
let turnExternalCredential = "";
|
|
443
713
|
let webphoneOrigin = existing.WEBPHONE_ORIGIN || DEFAULT_WEBPHONE_ORIGIN;
|
|
444
|
-
let configureUfw = true;
|
|
714
|
+
let configureUfw = await prompt.askYesNo("Configure UFW firewall rules now?", true);
|
|
445
715
|
let mediaIpv4Only = existing.MEDIA_IPV4_ONLY ? existing.MEDIA_IPV4_ONLY === "1" : true;
|
|
446
716
|
|
|
447
|
-
if (
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
717
|
+
if (!advanced) {
|
|
718
|
+
acmeEmail = acmeEmail || (await prompt.askText("Let's Encrypt email", "", { required: true }));
|
|
719
|
+
turnMode = await prompt.askYesNo("Enable built-in TURN (coturn)?", true) ? "coturn" : "none";
|
|
720
|
+
const quickDefaults = buildQuickFlowDefaults(initProfile, existing);
|
|
721
|
+
bitcallEnv = quickDefaults.bitcallEnv;
|
|
722
|
+
routingMode = quickDefaults.routingMode;
|
|
723
|
+
sipProviderUri = quickDefaults.sipProviderUri;
|
|
724
|
+
sipTransport = quickDefaults.sipTransport;
|
|
725
|
+
sipPort = quickDefaults.sipPort;
|
|
726
|
+
allowedDomains = quickDefaults.allowedDomains;
|
|
727
|
+
webphoneOrigin = quickDefaults.webphoneOrigin;
|
|
728
|
+
sipTrustedIps = quickDefaults.sipTrustedIps;
|
|
729
|
+
} else {
|
|
452
730
|
deployMode = await prompt.askChoice(
|
|
453
731
|
"Deployment mode",
|
|
454
732
|
["standalone", "reverse-proxy"],
|
|
@@ -476,12 +754,9 @@ async function runWizard(existing = {}, preflight = {}) {
|
|
|
476
754
|
acmeEmail = "";
|
|
477
755
|
}
|
|
478
756
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
allowedDomains
|
|
483
|
-
);
|
|
484
|
-
|
|
757
|
+
if (!initOptions.dev && !initOptions.production) {
|
|
758
|
+
bitcallEnv = await prompt.askChoice("Environment", ["production", "dev"], bitcallEnv === "dev" ? 1 : 0);
|
|
759
|
+
}
|
|
485
760
|
routingMode = await prompt.askChoice(
|
|
486
761
|
"Routing mode",
|
|
487
762
|
["universal", "single-provider"],
|
|
@@ -520,12 +795,22 @@ async function runWizard(existing = {}, preflight = {}) {
|
|
|
520
795
|
sipProviderUri = `sip:${sipProviderHost}:${sipPort};transport=${sipTransport}`;
|
|
521
796
|
}
|
|
522
797
|
|
|
798
|
+
const requireAllowlist = shouldRequireAllowlist(bitcallEnv, routingMode);
|
|
799
|
+
allowedDomains = await prompt.askText(
|
|
800
|
+
requireAllowlist
|
|
801
|
+
? "Allowed SIP domains (comma-separated; required in production universal mode)"
|
|
802
|
+
: "Allowed SIP domains (comma-separated)",
|
|
803
|
+
allowedDomains,
|
|
804
|
+
{ required: requireAllowlist }
|
|
805
|
+
);
|
|
806
|
+
|
|
523
807
|
sipTrustedIps = await prompt.askText(
|
|
524
808
|
"Trusted SIP source IPs (optional, comma-separated)",
|
|
525
809
|
sipTrustedIps
|
|
526
810
|
);
|
|
527
811
|
|
|
528
|
-
const
|
|
812
|
+
const existingTurn = existing.TURN_MODE || "coturn";
|
|
813
|
+
const turnDefaultIndex = existingTurn === "external" ? 2 : existingTurn === "coturn" ? 1 : 0;
|
|
529
814
|
turnMode = await prompt.askChoice("TURN mode", ["none", "coturn", "external"], turnDefaultIndex);
|
|
530
815
|
if (turnMode === "coturn") {
|
|
531
816
|
turnSecret = crypto.randomBytes(32).toString("hex");
|
|
@@ -559,16 +844,10 @@ async function runWizard(existing = {}, preflight = {}) {
|
|
|
559
844
|
"Media IPv4-only mode (block IPv6 RTP/TURN on host firewall)",
|
|
560
845
|
mediaIpv4Only
|
|
561
846
|
);
|
|
562
|
-
|
|
563
|
-
configureUfw = await prompt.askYesNo("Configure ufw firewall rules now", true);
|
|
564
|
-
} else {
|
|
565
|
-
acmeEmail = await prompt.askText("Let's Encrypt email", acmeEmail, { required: true });
|
|
566
847
|
}
|
|
567
848
|
|
|
568
|
-
if (
|
|
569
|
-
|
|
570
|
-
"Production mode requires ALLOWED_SIP_DOMAINS. Re-run init with Advanced=Yes and provide allowlisted SIP domains, or switch Environment to dev."
|
|
571
|
-
);
|
|
849
|
+
if (turnMode === "coturn") {
|
|
850
|
+
turnSecret = crypto.randomBytes(32).toString("hex");
|
|
572
851
|
}
|
|
573
852
|
|
|
574
853
|
if (deployMode === "reverse-proxy") {
|
|
@@ -615,19 +894,12 @@ async function runWizard(existing = {}, preflight = {}) {
|
|
|
615
894
|
configureUfw,
|
|
616
895
|
};
|
|
617
896
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
console.log(` TURN: ${config.turnMode}`);
|
|
625
|
-
console.log(
|
|
626
|
-
` Media: ${config.mediaIpv4Only === "1" ? "IPv4-only (IPv6 signaling allowed; IPv6 media blocked)" : "dual-stack"}`
|
|
627
|
-
);
|
|
628
|
-
console.log(
|
|
629
|
-
` Security: Allowed SIP domains ${allowedCount > 0 ? `${allowedCount} entries` : "not set"}`
|
|
630
|
-
);
|
|
897
|
+
if (config.bitcallEnv === "production") {
|
|
898
|
+
validateProductionConfig(config);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const devWarnings = config.bitcallEnv === "dev" ? buildDevWarnings(config) : [];
|
|
902
|
+
printSummary(config, devWarnings);
|
|
631
903
|
|
|
632
904
|
const proceed = await prompt.askYesNo("Proceed with provisioning", true);
|
|
633
905
|
if (!proceed) {
|
|
@@ -722,16 +994,16 @@ function provisionCustomCert(config) {
|
|
|
722
994
|
return { certPath: certOut, keyPath: keyOut };
|
|
723
995
|
}
|
|
724
996
|
|
|
725
|
-
function startGatewayStack() {
|
|
726
|
-
runCompose(["pull"], {
|
|
727
|
-
runCompose(["up", "-d", "--remove-orphans"], {
|
|
997
|
+
function startGatewayStack(exec = run) {
|
|
998
|
+
runCompose(["pull"], {}, exec);
|
|
999
|
+
runCompose(["up", "-d", "--remove-orphans"], {}, exec);
|
|
728
1000
|
}
|
|
729
1001
|
|
|
730
|
-
function runLetsEncrypt(config) {
|
|
731
|
-
|
|
732
|
-
|
|
1002
|
+
function runLetsEncrypt(config, exec = run) {
|
|
1003
|
+
exec("apt-get", ["update"]);
|
|
1004
|
+
exec("apt-get", ["install", "-y", "certbot"]);
|
|
733
1005
|
|
|
734
|
-
|
|
1006
|
+
exec(
|
|
735
1007
|
"certbot",
|
|
736
1008
|
[
|
|
737
1009
|
"certonly",
|
|
@@ -745,7 +1017,7 @@ function runLetsEncrypt(config) {
|
|
|
745
1017
|
"--agree-tos",
|
|
746
1018
|
"--non-interactive",
|
|
747
1019
|
],
|
|
748
|
-
{
|
|
1020
|
+
{}
|
|
749
1021
|
);
|
|
750
1022
|
|
|
751
1023
|
const liveCert = `/etc/letsencrypt/live/${config.domain}/fullchain.pem`;
|
|
@@ -757,80 +1029,106 @@ function runLetsEncrypt(config) {
|
|
|
757
1029
|
});
|
|
758
1030
|
|
|
759
1031
|
installRenewHook();
|
|
760
|
-
runCompose(["up", "-d", "--remove-orphans"], {
|
|
1032
|
+
runCompose(["up", "-d", "--remove-orphans"], {}, exec);
|
|
761
1033
|
}
|
|
762
1034
|
|
|
763
|
-
function installGatewayService() {
|
|
764
|
-
installSystemdService();
|
|
765
|
-
|
|
1035
|
+
function installGatewayService(exec = run) {
|
|
1036
|
+
installSystemdService(exec);
|
|
1037
|
+
exec("systemctl", ["enable", "--now", SERVICE_NAME]);
|
|
766
1038
|
}
|
|
767
1039
|
|
|
768
|
-
async function initCommand() {
|
|
769
|
-
|
|
1040
|
+
async function initCommand(initOptions = {}) {
|
|
1041
|
+
printBanner();
|
|
1042
|
+
const ctx = createInstallContext(initOptions);
|
|
1043
|
+
|
|
1044
|
+
let preflight;
|
|
1045
|
+
await ctx.step("Checks", async () => {
|
|
1046
|
+
preflight = await runPreflight(ctx);
|
|
1047
|
+
});
|
|
770
1048
|
|
|
771
1049
|
let existingEnv = {};
|
|
772
1050
|
if (fs.existsSync(ENV_PATH)) {
|
|
773
1051
|
existingEnv = loadEnvFile(ENV_PATH);
|
|
774
1052
|
}
|
|
775
1053
|
|
|
776
|
-
|
|
1054
|
+
let config;
|
|
1055
|
+
await ctx.step("Config", async () => {
|
|
1056
|
+
config = await runWizard(existingEnv, preflight, initOptions);
|
|
1057
|
+
});
|
|
777
1058
|
|
|
778
1059
|
ensureInstallLayout();
|
|
779
1060
|
|
|
780
1061
|
let tlsCertPath;
|
|
781
1062
|
let tlsKeyPath;
|
|
782
1063
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
const envContent = renderEnvContent(config, tlsCertPath, tlsKeyPath);
|
|
798
|
-
writeFileWithMode(ENV_PATH, envContent, 0o600);
|
|
799
|
-
writeComposeTemplate();
|
|
800
|
-
|
|
801
|
-
if (config.configureUfw) {
|
|
802
|
-
configureFirewall(config);
|
|
803
|
-
}
|
|
1064
|
+
await ctx.step("Firewall", async () => {
|
|
1065
|
+
if (config.configureUfw) {
|
|
1066
|
+
const ufwReady = await ensureUfwAvailable(ctx);
|
|
1067
|
+
if (ufwReady) {
|
|
1068
|
+
configureFirewall(config, ctx.exec);
|
|
1069
|
+
} else {
|
|
1070
|
+
console.log("Skipping UFW configuration by request.");
|
|
1071
|
+
config.configureUfw = false;
|
|
1072
|
+
printRequiredPorts(config);
|
|
1073
|
+
}
|
|
1074
|
+
} else {
|
|
1075
|
+
printRequiredPorts(config);
|
|
1076
|
+
}
|
|
804
1077
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1078
|
+
if (config.mediaIpv4Only === "1") {
|
|
1079
|
+
try {
|
|
1080
|
+
const applied = applyMediaIpv4OnlyRules(mediaFirewallOptionsFromConfig(config));
|
|
1081
|
+
console.log(`Applied IPv6 media block rules (${applied.backend}).`);
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
console.error(`IPv6 media block setup failed: ${error.message}`);
|
|
1084
|
+
const proceed = await confirmContinueWithoutMediaBlock();
|
|
1085
|
+
if (!proceed) {
|
|
1086
|
+
throw new Error("Initialization stopped because media IPv4-only firewall rules were not applied.");
|
|
1087
|
+
}
|
|
1088
|
+
config.mediaIpv4Only = "0";
|
|
1089
|
+
console.log("Continuing without IPv6 media block rules.");
|
|
814
1090
|
}
|
|
815
|
-
updateGatewayEnv({ MEDIA_IPV4_ONLY: "0" });
|
|
816
|
-
config.mediaIpv4Only = "0";
|
|
817
|
-
console.log("Continuing without IPv6 media block rules.");
|
|
818
1091
|
}
|
|
819
|
-
}
|
|
1092
|
+
});
|
|
820
1093
|
|
|
821
|
-
|
|
1094
|
+
await ctx.step("TLS", async () => {
|
|
1095
|
+
if (config.tlsMode === "custom") {
|
|
1096
|
+
const custom = provisionCustomCert(config);
|
|
1097
|
+
tlsCertPath = custom.certPath;
|
|
1098
|
+
tlsKeyPath = custom.keyPath;
|
|
1099
|
+
} else if (config.tlsMode === "dev-self-signed") {
|
|
1100
|
+
tlsCertPath = path.join(SSL_DIR, "dev-fullchain.pem");
|
|
1101
|
+
tlsKeyPath = path.join(SSL_DIR, "dev-privkey.pem");
|
|
1102
|
+
generateSelfSigned(tlsCertPath, tlsKeyPath, config.domain, 30, ctx.exec);
|
|
1103
|
+
} else {
|
|
1104
|
+
tlsCertPath = path.join(SSL_DIR, "bootstrap-fullchain.pem");
|
|
1105
|
+
tlsKeyPath = path.join(SSL_DIR, "bootstrap-privkey.pem");
|
|
1106
|
+
generateSelfSigned(tlsCertPath, tlsKeyPath, config.domain, 1, ctx.exec);
|
|
1107
|
+
}
|
|
822
1108
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1109
|
+
const envContent = renderEnvContent(config, tlsCertPath, tlsKeyPath);
|
|
1110
|
+
writeFileWithMode(ENV_PATH, envContent, 0o600);
|
|
1111
|
+
writeComposeTemplate();
|
|
1112
|
+
});
|
|
826
1113
|
|
|
827
|
-
|
|
1114
|
+
await ctx.step("Start", async () => {
|
|
1115
|
+
startGatewayStack(ctx.exec);
|
|
1116
|
+
if (config.tlsMode === "letsencrypt") {
|
|
1117
|
+
runLetsEncrypt(config, ctx.exec);
|
|
1118
|
+
}
|
|
1119
|
+
installGatewayService(ctx.exec);
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
await ctx.step("Done", async () => {});
|
|
828
1123
|
|
|
829
1124
|
console.log("\nGateway initialized.");
|
|
830
1125
|
console.log(`WSS URL: wss://${config.domain}`);
|
|
831
1126
|
if (config.turnMode !== "none") {
|
|
832
1127
|
console.log(`TURN credentials URL: https://${config.domain}/turn-credentials`);
|
|
833
1128
|
}
|
|
1129
|
+
if (!ctx.verbose) {
|
|
1130
|
+
console.log(`Install log: ${ctx.logPath}`);
|
|
1131
|
+
}
|
|
834
1132
|
}
|
|
835
1133
|
|
|
836
1134
|
function runSystemctl(args, fallbackComposeArgs) {
|
|
@@ -940,7 +1238,7 @@ function statusCommand() {
|
|
|
940
1238
|
|
|
941
1239
|
console.log("\nConfig summary:");
|
|
942
1240
|
console.log(`DOMAIN=${envMap.DOMAIN || ""}`);
|
|
943
|
-
console.log(`BITCALL_ENV=${envMap.BITCALL_ENV || "
|
|
1241
|
+
console.log(`BITCALL_ENV=${envMap.BITCALL_ENV || "dev"}`);
|
|
944
1242
|
console.log(`ROUTING_MODE=${envMap.ROUTING_MODE || "universal"}`);
|
|
945
1243
|
console.log(`SIP_PROVIDER_URI=${envMap.SIP_PROVIDER_URI || ""}`);
|
|
946
1244
|
console.log(`DEPLOY_MODE=${envMap.DEPLOY_MODE || ""}`);
|
|
@@ -1129,7 +1427,16 @@ function buildProgram() {
|
|
|
1129
1427
|
.description("Install and operate Bitcall WebRTC-to-SIP gateway")
|
|
1130
1428
|
.version(PACKAGE_VERSION);
|
|
1131
1429
|
|
|
1132
|
-
program
|
|
1430
|
+
program
|
|
1431
|
+
.command("init")
|
|
1432
|
+
.description("Run setup wizard and provision gateway")
|
|
1433
|
+
.option("--advanced", "Show advanced configuration prompts")
|
|
1434
|
+
.option("--dev", "Quick dev setup (minimal prompts, permissive defaults)")
|
|
1435
|
+
.option("--production", "Production setup (strict validation enabled)")
|
|
1436
|
+
.option("--domain <domain>", "Gateway domain")
|
|
1437
|
+
.option("--email <email>", "Let's Encrypt email")
|
|
1438
|
+
.option("--verbose", "Stream full installer command output")
|
|
1439
|
+
.action(initCommand);
|
|
1133
1440
|
program.command("up").description("Start gateway services").action(upCommand);
|
|
1134
1441
|
program.command("down").description("Stop gateway services").action(downCommand);
|
|
1135
1442
|
program.command("stop").description("Alias for down").action(downCommand);
|
|
@@ -1179,6 +1486,14 @@ async function main(argv = process.argv) {
|
|
|
1179
1486
|
|
|
1180
1487
|
module.exports = {
|
|
1181
1488
|
main,
|
|
1489
|
+
normalizeInitProfile,
|
|
1490
|
+
validateProductionConfig,
|
|
1491
|
+
buildDevWarnings,
|
|
1492
|
+
buildQuickFlowDefaults,
|
|
1493
|
+
shouldRequireAllowlist,
|
|
1494
|
+
isOriginWildcard,
|
|
1495
|
+
isSingleProviderConfigured,
|
|
1496
|
+
printRequiredPorts,
|
|
1182
1497
|
};
|
|
1183
1498
|
|
|
1184
1499
|
if (require.main === module) {
|