@bitcall/webrtc-sip-gateway 0.2.6 → 0.2.7
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 +12 -2
- package/lib/constants.js +1 -1
- package/lib/system.js +29 -12
- package/package.json +2 -2
- package/src/index.js +433 -130
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.7
|
|
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,19 @@ 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
|
+
Use `sudo bitcall-gateway init --production` for strict input validation and
|
|
26
|
+
hardening checks.
|
|
27
|
+
Use `--verbose` to stream apt/docker output during install. Default mode keeps
|
|
28
|
+
console output concise and writes command details to
|
|
29
|
+
`/var/log/bitcall-gateway-install.log`.
|
|
30
|
+
|
|
25
31
|
## Commands
|
|
26
32
|
|
|
27
33
|
- `sudo bitcall-gateway init`
|
|
34
|
+
- `sudo bitcall-gateway init --dev`
|
|
35
|
+
- `sudo bitcall-gateway init --production`
|
|
36
|
+
- `sudo bitcall-gateway init --advanced`
|
|
37
|
+
- `sudo bitcall-gateway init --verbose`
|
|
28
38
|
- `sudo bitcall-gateway up`
|
|
29
39
|
- `sudo bitcall-gateway down`
|
|
30
40
|
- `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.7",
|
|
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.7",
|
|
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.7";
|
|
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,20 @@ function toOriginPattern(origin) {
|
|
|
353
544
|
return `^${escapeRegex(origin)}$`;
|
|
354
545
|
}
|
|
355
546
|
|
|
356
|
-
|
|
547
|
+
function normalizeInitProfile(initOptions = {}, existing = {}) {
|
|
548
|
+
if (initOptions.dev && initOptions.production) {
|
|
549
|
+
throw new Error("Use only one mode: --dev or --production.");
|
|
550
|
+
}
|
|
551
|
+
if (initOptions.production) {
|
|
552
|
+
return "production";
|
|
553
|
+
}
|
|
554
|
+
if (initOptions.dev) {
|
|
555
|
+
return "dev";
|
|
556
|
+
}
|
|
557
|
+
return existing.BITCALL_ENV || "dev";
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function runPreflight(ctx) {
|
|
357
561
|
if (process.platform !== "linux") {
|
|
358
562
|
throw new Error("bitcall-gateway supports Linux hosts only.");
|
|
359
563
|
}
|
|
@@ -365,13 +569,11 @@ async function runPreflight() {
|
|
|
365
569
|
);
|
|
366
570
|
}
|
|
367
571
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
stdio: "inherit",
|
|
371
|
-
});
|
|
572
|
+
ctx.exec("apt-get", ["update"]);
|
|
573
|
+
ctx.exec("apt-get", ["install", "-y", "curl", "ca-certificates", "lsof", "openssl", "gnupg"]);
|
|
372
574
|
|
|
373
|
-
ensureDockerInstalled();
|
|
374
|
-
ensureComposePlugin();
|
|
575
|
+
ensureDockerInstalled({ exec: ctx.exec, shell: ctx.shell });
|
|
576
|
+
ensureComposePlugin({ exec: ctx.exec });
|
|
375
577
|
|
|
376
578
|
const freeGb = diskFreeGb("/");
|
|
377
579
|
if (freeGb < 2) {
|
|
@@ -392,17 +594,73 @@ async function runPreflight() {
|
|
|
392
594
|
}
|
|
393
595
|
|
|
394
596
|
return {
|
|
597
|
+
os,
|
|
395
598
|
p80,
|
|
396
599
|
p443,
|
|
397
600
|
};
|
|
398
601
|
}
|
|
399
602
|
|
|
400
|
-
|
|
603
|
+
function printSummary(config, devWarnings) {
|
|
604
|
+
const allowedCount = countAllowedDomains(config.allowedDomains);
|
|
605
|
+
const showDevWarnings = config.bitcallEnv === "dev";
|
|
606
|
+
console.log("\nSummary:");
|
|
607
|
+
console.log(` Domain: ${config.domain}`);
|
|
608
|
+
console.log(` Environment: ${config.bitcallEnv}`);
|
|
609
|
+
console.log(` Routing: ${config.routingMode}`);
|
|
610
|
+
console.log(
|
|
611
|
+
` Provider allowlist: ${allowedCount > 0 ? config.allowedDomains : "(any)"}${showDevWarnings && allowedCount === 0 ? " [DEV WARNING]" : ""}`
|
|
612
|
+
);
|
|
613
|
+
console.log(
|
|
614
|
+
` Webphone origin: ${config.webphoneOrigin === "*" ? `(any)${showDevWarnings ? " [DEV WARNING]" : ""}` : config.webphoneOrigin}`
|
|
615
|
+
);
|
|
616
|
+
console.log(` SIP source IPs: ${(config.sipTrustedIps || "").trim() ? config.sipTrustedIps : "(any)"}`);
|
|
617
|
+
console.log(` TLS: ${config.tlsMode === "letsencrypt" ? "Let's Encrypt" : config.tlsMode}`);
|
|
618
|
+
console.log(` TURN: ${config.turnMode === "coturn" ? "enabled (coturn)" : config.turnMode}`);
|
|
619
|
+
console.log(
|
|
620
|
+
` Media: ${config.mediaIpv4Only === "1" ? "IPv4-only (IPv6 media blocked)" : "dual-stack"}`
|
|
621
|
+
);
|
|
622
|
+
console.log(` Firewall: ${config.configureUfw ? "UFW enabled" : "manual setup"}`);
|
|
623
|
+
|
|
624
|
+
if (devWarnings.length > 0) {
|
|
625
|
+
console.log("\nWarnings:");
|
|
626
|
+
for (const warning of devWarnings) {
|
|
627
|
+
console.log(` - ${warning}`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function parseProviderFromUri(uri = "") {
|
|
633
|
+
const clean = uri.replace(/^sip:/, "");
|
|
634
|
+
const [hostPort, transportPart] = clean.split(";");
|
|
635
|
+
const [host, port] = hostPort.split(":");
|
|
636
|
+
let transport = "udp";
|
|
637
|
+
if (transportPart && transportPart.includes("transport=")) {
|
|
638
|
+
transport = transportPart.split("transport=")[1] || "udp";
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
host: host || DEFAULT_PROVIDER_HOST,
|
|
642
|
+
port: port || (transport === "tls" ? "5061" : "5060"),
|
|
643
|
+
transport,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
|
|
401
648
|
const prompt = new Prompter();
|
|
402
649
|
|
|
403
650
|
try {
|
|
651
|
+
const initProfile = normalizeInitProfile(initOptions, existing);
|
|
652
|
+
let advanced = Boolean(initOptions.advanced);
|
|
653
|
+
if (initOptions.production && !initOptions.advanced) {
|
|
654
|
+
advanced = true;
|
|
655
|
+
} else if (!initOptions.dev && !initOptions.production && !initOptions.advanced) {
|
|
656
|
+
advanced = await prompt.askYesNo("Advanced setup", false);
|
|
657
|
+
}
|
|
658
|
+
|
|
404
659
|
const detectedIp = detectPublicIp();
|
|
405
|
-
const
|
|
660
|
+
const domainDefault = initOptions.domain || existing.DOMAIN || "";
|
|
661
|
+
const domain = domainDefault
|
|
662
|
+
? domainDefault
|
|
663
|
+
: await prompt.askText("Gateway domain", existing.DOMAIN || "", { required: true });
|
|
406
664
|
let publicIp = existing.PUBLIC_IP || detectedIp || "";
|
|
407
665
|
if (!publicIp) {
|
|
408
666
|
publicIp = await prompt.askText("Public IPv4 (auto-detect failed)", "", { required: true });
|
|
@@ -418,22 +676,21 @@ async function runWizard(existing = {}, preflight = {}) {
|
|
|
418
676
|
? "reverse-proxy"
|
|
419
677
|
: "standalone";
|
|
420
678
|
|
|
421
|
-
const advanced = await prompt.askYesNo("Advanced setup", false);
|
|
422
|
-
|
|
423
679
|
let deployMode = autoDeployMode;
|
|
424
680
|
let tlsMode = "letsencrypt";
|
|
425
|
-
let acmeEmail = existing.ACME_EMAIL || "";
|
|
681
|
+
let acmeEmail = initOptions.email || existing.ACME_EMAIL || "";
|
|
426
682
|
let customCertPath = "";
|
|
427
683
|
let customKeyPath = "";
|
|
428
|
-
let bitcallEnv =
|
|
684
|
+
let bitcallEnv = initProfile;
|
|
429
685
|
let routingMode = existing.ROUTING_MODE || "universal";
|
|
430
|
-
|
|
431
|
-
let
|
|
432
|
-
let
|
|
686
|
+
const providerFromEnv = parseProviderFromUri(existing.SIP_PROVIDER_URI || "");
|
|
687
|
+
let sipProviderHost = providerFromEnv.host || DEFAULT_PROVIDER_HOST;
|
|
688
|
+
let sipTransport = providerFromEnv.transport || "udp";
|
|
689
|
+
let sipPort = providerFromEnv.port || "5060";
|
|
433
690
|
let sipProviderUri = "";
|
|
434
691
|
let allowedDomains = existing.ALLOWED_SIP_DOMAINS || "";
|
|
435
692
|
let sipTrustedIps = existing.SIP_TRUSTED_IPS || "";
|
|
436
|
-
let turnMode =
|
|
693
|
+
let turnMode = "coturn";
|
|
437
694
|
let turnSecret = "";
|
|
438
695
|
let turnTtl = existing.TURN_TTL || "86400";
|
|
439
696
|
let turnApiToken = existing.TURN_API_TOKEN || "";
|
|
@@ -441,14 +698,26 @@ async function runWizard(existing = {}, preflight = {}) {
|
|
|
441
698
|
let turnExternalUsername = "";
|
|
442
699
|
let turnExternalCredential = "";
|
|
443
700
|
let webphoneOrigin = existing.WEBPHONE_ORIGIN || DEFAULT_WEBPHONE_ORIGIN;
|
|
444
|
-
let configureUfw = true;
|
|
701
|
+
let configureUfw = initOptions.dev ? true : await prompt.askYesNo("Configure UFW firewall rules now?", true);
|
|
445
702
|
let mediaIpv4Only = existing.MEDIA_IPV4_ONLY ? existing.MEDIA_IPV4_ONLY === "1" : true;
|
|
446
703
|
|
|
447
|
-
if (
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
704
|
+
if (!advanced) {
|
|
705
|
+
acmeEmail = acmeEmail || (await prompt.askText("Let's Encrypt email", "", { required: true }));
|
|
706
|
+
if (!initOptions.dev) {
|
|
707
|
+
turnMode = await prompt.askYesNo("Enable built-in TURN (coturn)?", true) ? "coturn" : "none";
|
|
708
|
+
} else {
|
|
709
|
+
turnMode = "coturn";
|
|
710
|
+
}
|
|
711
|
+
const quickDefaults = buildQuickFlowDefaults(initProfile, existing);
|
|
712
|
+
bitcallEnv = quickDefaults.bitcallEnv;
|
|
713
|
+
routingMode = quickDefaults.routingMode;
|
|
714
|
+
sipProviderUri = quickDefaults.sipProviderUri;
|
|
715
|
+
sipTransport = quickDefaults.sipTransport;
|
|
716
|
+
sipPort = quickDefaults.sipPort;
|
|
717
|
+
allowedDomains = quickDefaults.allowedDomains;
|
|
718
|
+
webphoneOrigin = quickDefaults.webphoneOrigin;
|
|
719
|
+
sipTrustedIps = quickDefaults.sipTrustedIps;
|
|
720
|
+
} else {
|
|
452
721
|
deployMode = await prompt.askChoice(
|
|
453
722
|
"Deployment mode",
|
|
454
723
|
["standalone", "reverse-proxy"],
|
|
@@ -476,9 +745,11 @@ async function runWizard(existing = {}, preflight = {}) {
|
|
|
476
745
|
acmeEmail = "";
|
|
477
746
|
}
|
|
478
747
|
|
|
479
|
-
|
|
748
|
+
if (!initOptions.dev && !initOptions.production) {
|
|
749
|
+
bitcallEnv = await prompt.askChoice("Environment", ["production", "dev"], bitcallEnv === "dev" ? 1 : 0);
|
|
750
|
+
}
|
|
480
751
|
allowedDomains = await prompt.askText(
|
|
481
|
-
"Allowed SIP domains (comma-separated
|
|
752
|
+
"Allowed SIP domains (comma-separated)",
|
|
482
753
|
allowedDomains
|
|
483
754
|
);
|
|
484
755
|
|
|
@@ -525,7 +796,8 @@ async function runWizard(existing = {}, preflight = {}) {
|
|
|
525
796
|
sipTrustedIps
|
|
526
797
|
);
|
|
527
798
|
|
|
528
|
-
const
|
|
799
|
+
const existingTurn = existing.TURN_MODE || "coturn";
|
|
800
|
+
const turnDefaultIndex = existingTurn === "external" ? 2 : existingTurn === "coturn" ? 1 : 0;
|
|
529
801
|
turnMode = await prompt.askChoice("TURN mode", ["none", "coturn", "external"], turnDefaultIndex);
|
|
530
802
|
if (turnMode === "coturn") {
|
|
531
803
|
turnSecret = crypto.randomBytes(32).toString("hex");
|
|
@@ -559,16 +831,10 @@ async function runWizard(existing = {}, preflight = {}) {
|
|
|
559
831
|
"Media IPv4-only mode (block IPv6 RTP/TURN on host firewall)",
|
|
560
832
|
mediaIpv4Only
|
|
561
833
|
);
|
|
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
834
|
}
|
|
567
835
|
|
|
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
|
-
);
|
|
836
|
+
if (turnMode === "coturn") {
|
|
837
|
+
turnSecret = crypto.randomBytes(32).toString("hex");
|
|
572
838
|
}
|
|
573
839
|
|
|
574
840
|
if (deployMode === "reverse-proxy") {
|
|
@@ -615,23 +881,18 @@ async function runWizard(existing = {}, preflight = {}) {
|
|
|
615
881
|
configureUfw,
|
|
616
882
|
};
|
|
617
883
|
|
|
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
|
-
);
|
|
884
|
+
if (config.bitcallEnv === "production") {
|
|
885
|
+
validateProductionConfig(config);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const devWarnings = config.bitcallEnv === "dev" ? buildDevWarnings(config) : [];
|
|
889
|
+
printSummary(config, devWarnings);
|
|
631
890
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
891
|
+
if (!(initOptions.dev && !advanced)) {
|
|
892
|
+
const proceed = await prompt.askYesNo("Proceed with provisioning", true);
|
|
893
|
+
if (!proceed) {
|
|
894
|
+
throw new Error("Initialization canceled.");
|
|
895
|
+
}
|
|
635
896
|
}
|
|
636
897
|
|
|
637
898
|
return config;
|
|
@@ -722,16 +983,16 @@ function provisionCustomCert(config) {
|
|
|
722
983
|
return { certPath: certOut, keyPath: keyOut };
|
|
723
984
|
}
|
|
724
985
|
|
|
725
|
-
function startGatewayStack() {
|
|
726
|
-
runCompose(["pull"], {
|
|
727
|
-
runCompose(["up", "-d", "--remove-orphans"], {
|
|
986
|
+
function startGatewayStack(exec = run) {
|
|
987
|
+
runCompose(["pull"], {}, exec);
|
|
988
|
+
runCompose(["up", "-d", "--remove-orphans"], {}, exec);
|
|
728
989
|
}
|
|
729
990
|
|
|
730
|
-
function runLetsEncrypt(config) {
|
|
731
|
-
|
|
732
|
-
|
|
991
|
+
function runLetsEncrypt(config, exec = run) {
|
|
992
|
+
exec("apt-get", ["update"]);
|
|
993
|
+
exec("apt-get", ["install", "-y", "certbot"]);
|
|
733
994
|
|
|
734
|
-
|
|
995
|
+
exec(
|
|
735
996
|
"certbot",
|
|
736
997
|
[
|
|
737
998
|
"certonly",
|
|
@@ -745,7 +1006,7 @@ function runLetsEncrypt(config) {
|
|
|
745
1006
|
"--agree-tos",
|
|
746
1007
|
"--non-interactive",
|
|
747
1008
|
],
|
|
748
|
-
{
|
|
1009
|
+
{}
|
|
749
1010
|
);
|
|
750
1011
|
|
|
751
1012
|
const liveCert = `/etc/letsencrypt/live/${config.domain}/fullchain.pem`;
|
|
@@ -757,80 +1018,106 @@ function runLetsEncrypt(config) {
|
|
|
757
1018
|
});
|
|
758
1019
|
|
|
759
1020
|
installRenewHook();
|
|
760
|
-
runCompose(["up", "-d", "--remove-orphans"], {
|
|
1021
|
+
runCompose(["up", "-d", "--remove-orphans"], {}, exec);
|
|
761
1022
|
}
|
|
762
1023
|
|
|
763
|
-
function installGatewayService() {
|
|
764
|
-
installSystemdService();
|
|
765
|
-
|
|
1024
|
+
function installGatewayService(exec = run) {
|
|
1025
|
+
installSystemdService(exec);
|
|
1026
|
+
exec("systemctl", ["enable", "--now", SERVICE_NAME]);
|
|
766
1027
|
}
|
|
767
1028
|
|
|
768
|
-
async function initCommand() {
|
|
769
|
-
|
|
1029
|
+
async function initCommand(initOptions = {}) {
|
|
1030
|
+
printBanner();
|
|
1031
|
+
const ctx = createInstallContext(initOptions);
|
|
1032
|
+
|
|
1033
|
+
let preflight;
|
|
1034
|
+
await ctx.step("Checks", async () => {
|
|
1035
|
+
preflight = await runPreflight(ctx);
|
|
1036
|
+
});
|
|
770
1037
|
|
|
771
1038
|
let existingEnv = {};
|
|
772
1039
|
if (fs.existsSync(ENV_PATH)) {
|
|
773
1040
|
existingEnv = loadEnvFile(ENV_PATH);
|
|
774
1041
|
}
|
|
775
1042
|
|
|
776
|
-
|
|
1043
|
+
let config;
|
|
1044
|
+
await ctx.step("Config", async () => {
|
|
1045
|
+
config = await runWizard(existingEnv, preflight, initOptions);
|
|
1046
|
+
});
|
|
777
1047
|
|
|
778
1048
|
ensureInstallLayout();
|
|
779
1049
|
|
|
780
1050
|
let tlsCertPath;
|
|
781
1051
|
let tlsKeyPath;
|
|
782
1052
|
|
|
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
|
-
}
|
|
1053
|
+
await ctx.step("Firewall", async () => {
|
|
1054
|
+
if (config.configureUfw) {
|
|
1055
|
+
const ufwReady = await ensureUfwAvailable(ctx);
|
|
1056
|
+
if (ufwReady) {
|
|
1057
|
+
configureFirewall(config, ctx.exec);
|
|
1058
|
+
} else {
|
|
1059
|
+
console.log("Skipping UFW configuration by request.");
|
|
1060
|
+
config.configureUfw = false;
|
|
1061
|
+
printRequiredPorts(config);
|
|
1062
|
+
}
|
|
1063
|
+
} else {
|
|
1064
|
+
printRequiredPorts(config);
|
|
1065
|
+
}
|
|
804
1066
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1067
|
+
if (config.mediaIpv4Only === "1") {
|
|
1068
|
+
try {
|
|
1069
|
+
const applied = applyMediaIpv4OnlyRules(mediaFirewallOptionsFromConfig(config));
|
|
1070
|
+
console.log(`Applied IPv6 media block rules (${applied.backend}).`);
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
console.error(`IPv6 media block setup failed: ${error.message}`);
|
|
1073
|
+
const proceed = await confirmContinueWithoutMediaBlock();
|
|
1074
|
+
if (!proceed) {
|
|
1075
|
+
throw new Error("Initialization stopped because media IPv4-only firewall rules were not applied.");
|
|
1076
|
+
}
|
|
1077
|
+
config.mediaIpv4Only = "0";
|
|
1078
|
+
console.log("Continuing without IPv6 media block rules.");
|
|
814
1079
|
}
|
|
815
|
-
updateGatewayEnv({ MEDIA_IPV4_ONLY: "0" });
|
|
816
|
-
config.mediaIpv4Only = "0";
|
|
817
|
-
console.log("Continuing without IPv6 media block rules.");
|
|
818
1080
|
}
|
|
819
|
-
}
|
|
1081
|
+
});
|
|
820
1082
|
|
|
821
|
-
|
|
1083
|
+
await ctx.step("TLS", async () => {
|
|
1084
|
+
if (config.tlsMode === "custom") {
|
|
1085
|
+
const custom = provisionCustomCert(config);
|
|
1086
|
+
tlsCertPath = custom.certPath;
|
|
1087
|
+
tlsKeyPath = custom.keyPath;
|
|
1088
|
+
} else if (config.tlsMode === "dev-self-signed") {
|
|
1089
|
+
tlsCertPath = path.join(SSL_DIR, "dev-fullchain.pem");
|
|
1090
|
+
tlsKeyPath = path.join(SSL_DIR, "dev-privkey.pem");
|
|
1091
|
+
generateSelfSigned(tlsCertPath, tlsKeyPath, config.domain, 30, ctx.exec);
|
|
1092
|
+
} else {
|
|
1093
|
+
tlsCertPath = path.join(SSL_DIR, "bootstrap-fullchain.pem");
|
|
1094
|
+
tlsKeyPath = path.join(SSL_DIR, "bootstrap-privkey.pem");
|
|
1095
|
+
generateSelfSigned(tlsCertPath, tlsKeyPath, config.domain, 1, ctx.exec);
|
|
1096
|
+
}
|
|
822
1097
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1098
|
+
const envContent = renderEnvContent(config, tlsCertPath, tlsKeyPath);
|
|
1099
|
+
writeFileWithMode(ENV_PATH, envContent, 0o600);
|
|
1100
|
+
writeComposeTemplate();
|
|
1101
|
+
});
|
|
826
1102
|
|
|
827
|
-
|
|
1103
|
+
await ctx.step("Start", async () => {
|
|
1104
|
+
startGatewayStack(ctx.exec);
|
|
1105
|
+
if (config.tlsMode === "letsencrypt") {
|
|
1106
|
+
runLetsEncrypt(config, ctx.exec);
|
|
1107
|
+
}
|
|
1108
|
+
installGatewayService(ctx.exec);
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
await ctx.step("Done", async () => {});
|
|
828
1112
|
|
|
829
1113
|
console.log("\nGateway initialized.");
|
|
830
1114
|
console.log(`WSS URL: wss://${config.domain}`);
|
|
831
1115
|
if (config.turnMode !== "none") {
|
|
832
1116
|
console.log(`TURN credentials URL: https://${config.domain}/turn-credentials`);
|
|
833
1117
|
}
|
|
1118
|
+
if (!ctx.verbose) {
|
|
1119
|
+
console.log(`Install log: ${ctx.logPath}`);
|
|
1120
|
+
}
|
|
834
1121
|
}
|
|
835
1122
|
|
|
836
1123
|
function runSystemctl(args, fallbackComposeArgs) {
|
|
@@ -940,7 +1227,7 @@ function statusCommand() {
|
|
|
940
1227
|
|
|
941
1228
|
console.log("\nConfig summary:");
|
|
942
1229
|
console.log(`DOMAIN=${envMap.DOMAIN || ""}`);
|
|
943
|
-
console.log(`BITCALL_ENV=${envMap.BITCALL_ENV || "
|
|
1230
|
+
console.log(`BITCALL_ENV=${envMap.BITCALL_ENV || "dev"}`);
|
|
944
1231
|
console.log(`ROUTING_MODE=${envMap.ROUTING_MODE || "universal"}`);
|
|
945
1232
|
console.log(`SIP_PROVIDER_URI=${envMap.SIP_PROVIDER_URI || ""}`);
|
|
946
1233
|
console.log(`DEPLOY_MODE=${envMap.DEPLOY_MODE || ""}`);
|
|
@@ -1129,7 +1416,16 @@ function buildProgram() {
|
|
|
1129
1416
|
.description("Install and operate Bitcall WebRTC-to-SIP gateway")
|
|
1130
1417
|
.version(PACKAGE_VERSION);
|
|
1131
1418
|
|
|
1132
|
-
program
|
|
1419
|
+
program
|
|
1420
|
+
.command("init")
|
|
1421
|
+
.description("Run setup wizard and provision gateway")
|
|
1422
|
+
.option("--advanced", "Show advanced configuration prompts")
|
|
1423
|
+
.option("--dev", "Quick dev setup (minimal prompts, permissive defaults)")
|
|
1424
|
+
.option("--production", "Production setup (strict validation enabled)")
|
|
1425
|
+
.option("--domain <domain>", "Gateway domain")
|
|
1426
|
+
.option("--email <email>", "Let's Encrypt email")
|
|
1427
|
+
.option("--verbose", "Stream full installer command output")
|
|
1428
|
+
.action(initCommand);
|
|
1133
1429
|
program.command("up").description("Start gateway services").action(upCommand);
|
|
1134
1430
|
program.command("down").description("Stop gateway services").action(downCommand);
|
|
1135
1431
|
program.command("stop").description("Alias for down").action(downCommand);
|
|
@@ -1179,6 +1475,13 @@ async function main(argv = process.argv) {
|
|
|
1179
1475
|
|
|
1180
1476
|
module.exports = {
|
|
1181
1477
|
main,
|
|
1478
|
+
normalizeInitProfile,
|
|
1479
|
+
validateProductionConfig,
|
|
1480
|
+
buildDevWarnings,
|
|
1481
|
+
buildQuickFlowDefaults,
|
|
1482
|
+
isOriginWildcard,
|
|
1483
|
+
isSingleProviderConfigured,
|
|
1484
|
+
printRequiredPorts,
|
|
1182
1485
|
};
|
|
1183
1486
|
|
|
1184
1487
|
if (require.main === module) {
|