@lamalibre/create-portlama 1.0.35 → 1.0.37
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 +20 -0
- package/package.json +3 -2
- package/src/index.js +146 -52
- package/vendor/panel-client/dist/assets/{index-BKznW-YZ.js → index-BU662GVt.js} +21 -21
- package/vendor/panel-client/dist/docs/00-introduction/what-is-portlama.md +2 -0
- package/vendor/panel-client/dist/docs/01-concepts/mtls.md +1 -0
- package/vendor/panel-client/dist/docs/01-concepts/security-model.md +1 -0
- package/vendor/panel-client/dist/docs/01-concepts/tunneling.md +12 -0
- package/vendor/panel-client/dist/docs/02-guides/desktop-app-setup.md +12 -10
- package/vendor/panel-client/dist/docs/02-guides/first-tunnel.md +2 -0
- package/vendor/panel-client/dist/docs/03-architecture/management-flow.md +3 -0
- package/vendor/panel-client/dist/docs/03-architecture/overview.md +2 -0
- package/vendor/panel-client/dist/docs/03-architecture/panel-client.md +4 -1
- package/vendor/panel-client/dist/docs/03-architecture/panel-server.md +4 -1
- package/vendor/panel-client/dist/docs/03-architecture/system-overview.md +1 -0
- package/vendor/panel-client/dist/docs/04-api-reference/certificates.md +1 -1
- package/vendor/panel-client/dist/docs/04-api-reference/tunnels.md +9 -0
- package/vendor/panel-client/dist/docs/06-reference/config-files.md +49 -2
- package/vendor/panel-client/dist/index.html +1 -1
- package/vendor/panel-server/package.json +1 -1
- package/vendor/panel-server/src/lib/mtls.js +1 -0
- package/vendor/panel-server/src/lib/nginx.js +164 -0
- package/vendor/panel-server/src/routes/management/tunnels.js +277 -13
package/README.md
CHANGED
|
@@ -17,6 +17,26 @@ the full Portlama stack with zero prompts. When finished it prints:
|
|
|
17
17
|
|
|
18
18
|
Import the certificate into your browser and open the URL to begin onboarding.
|
|
19
19
|
|
|
20
|
+
### JSON Mode
|
|
21
|
+
|
|
22
|
+
For programmatic use (e.g. from the Portlama desktop app), pass `--json` to
|
|
23
|
+
replace the interactive Listr2 terminal UI with NDJSON progress lines on stdout:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx @lamalibre/create-portlama --json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The `--json` flag implies `--yes` (no prompts) and `--dev` (accept private IPs).
|
|
30
|
+
Each line is a JSON object with one of these shapes:
|
|
31
|
+
|
|
32
|
+
```jsonl
|
|
33
|
+
{"event":"step","step":"check_environment","status":"running"}
|
|
34
|
+
{"event":"step","step":"harden_system","status":"complete"}
|
|
35
|
+
{"event":"step","step":"install_node","status":"skipped"}
|
|
36
|
+
{"event":"complete","server":{"ip":"...","panelUrl":"...","p12Path":"...","p12PasswordPath":"..."}}
|
|
37
|
+
{"event":"error","message":"...","recoverable":false}
|
|
38
|
+
```
|
|
39
|
+
|
|
20
40
|
## What It Provisions
|
|
21
41
|
|
|
22
42
|
- **OS hardening** — swap, UFW, fail2ban, SSH lockdown
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lamalibre/create-portlama",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.37",
|
|
4
4
|
"description": "One-command setup for secure reverse tunnels with a management dashboard",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
"vendor"
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
|
+
"build": "node scripts/bundle-vendor.js",
|
|
18
19
|
"lint": "eslint .",
|
|
19
|
-
"prepublishOnly": "npm run lint &&
|
|
20
|
+
"prepublishOnly": "npm run lint && npm run build"
|
|
20
21
|
},
|
|
21
22
|
"repository": {
|
|
22
23
|
"type": "git",
|
package/src/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { redeployTasks } from './tasks/redeploy.js';
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Parse minimal CLI flags from process.argv.
|
|
18
|
-
* @returns {{ skipHarden: boolean, uninstall: boolean, dev: boolean, help: boolean, yes: boolean }}
|
|
18
|
+
* @returns {{ skipHarden: boolean, uninstall: boolean, dev: boolean, help: boolean, yes: boolean, json: boolean, forceFull: boolean }}
|
|
19
19
|
*/
|
|
20
20
|
function parseFlags() {
|
|
21
21
|
const args = process.argv.slice(2);
|
|
@@ -26,9 +26,19 @@ function parseFlags() {
|
|
|
26
26
|
help: args.includes('--help') || args.includes('-h'),
|
|
27
27
|
yes: args.includes('--yes') || args.includes('-y'),
|
|
28
28
|
forceFull: args.includes('--force-full'),
|
|
29
|
+
json: args.includes('--json'),
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Emit a single NDJSON line to stdout. Used when --json mode is active
|
|
35
|
+
* so that callers (e.g. Tauri desktop app) can parse structured progress.
|
|
36
|
+
* @param {Record<string, unknown>} obj
|
|
37
|
+
*/
|
|
38
|
+
function emitJson(obj) {
|
|
39
|
+
process.stdout.write(JSON.stringify(obj) + '\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
/**
|
|
33
43
|
* Print help message describing Portlama and what the installer does,
|
|
34
44
|
* then exit with code 0.
|
|
@@ -61,7 +71,7 @@ ${b('SYSTEM MODIFICATIONS')}
|
|
|
61
71
|
• Creates a 1GB swap file
|
|
62
72
|
|
|
63
73
|
${y('Firewall & Security')}
|
|
64
|
-
• Resets UFW firewall (allows only ports 22, 443, 9292)
|
|
74
|
+
• Resets UFW firewall (allows only ports 22, 80, 443, 9292)
|
|
65
75
|
• Installs fail2ban with SSH and nginx jails
|
|
66
76
|
• Hardens SSH (disables password authentication)
|
|
67
77
|
|
|
@@ -94,6 +104,7 @@ ${b('FLAGS')}
|
|
|
94
104
|
${c('--yes')}, ${c('-y')} Skip the confirmation prompt
|
|
95
105
|
${c('--skip-harden')} Skip OS hardening (swap, UFW, fail2ban, SSH)
|
|
96
106
|
${c('--dev')} Allow private/non-routable IP addresses
|
|
107
|
+
${c('--json')} Output NDJSON progress lines instead of terminal UI
|
|
97
108
|
${c('--force-full')} Run full installation even on existing installs
|
|
98
109
|
${c('--uninstall')} Show manual removal guide for Portlama
|
|
99
110
|
`);
|
|
@@ -371,9 +382,37 @@ ${chalk.cyan.bold('└───────────────────
|
|
|
371
382
|
});
|
|
372
383
|
}
|
|
373
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Run a single Listr task step in JSON mode: emit running/complete/skipped
|
|
387
|
+
* NDJSON events around each step so the caller gets per-step progress.
|
|
388
|
+
* @param {Record<string, unknown>} ctx - Shared installer context.
|
|
389
|
+
* @param {{ key: string, title: string, fn: Function, skip?: () => boolean }} step
|
|
390
|
+
*/
|
|
391
|
+
async function runJsonStep(ctx, step) {
|
|
392
|
+
if (step.skip && step.skip()) {
|
|
393
|
+
emitJson({ event: 'step', step: step.key, status: 'skipped' });
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
emitJson({ event: 'step', step: step.key, status: 'running' });
|
|
397
|
+
const taskList = new Listr(
|
|
398
|
+
[{ title: step.title, task: (_c, t) => step.fn(ctx, t) }],
|
|
399
|
+
{ renderer: 'silent', exitOnError: true },
|
|
400
|
+
);
|
|
401
|
+
try {
|
|
402
|
+
await taskList.run();
|
|
403
|
+
} catch (error) {
|
|
404
|
+
emitJson({ event: 'step', step: step.key, status: 'failed' });
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
emitJson({ event: 'step', step: step.key, status: 'complete' });
|
|
408
|
+
}
|
|
409
|
+
|
|
374
410
|
/**
|
|
375
411
|
* Main installer orchestrator. Creates a shared context, runs all installation
|
|
376
412
|
* tasks through Listr2, and prints a summary on completion.
|
|
413
|
+
*
|
|
414
|
+
* When --json is active, outputs NDJSON progress lines instead of the
|
|
415
|
+
* interactive Listr2 terminal UI. The --json flag implies --yes and --dev.
|
|
377
416
|
*/
|
|
378
417
|
export async function main() {
|
|
379
418
|
const flags = parseFlags();
|
|
@@ -386,6 +425,14 @@ export async function main() {
|
|
|
386
425
|
printUninstallGuide();
|
|
387
426
|
}
|
|
388
427
|
|
|
428
|
+
// --json implies --yes (no interactive prompts) and --dev (accept private IPs)
|
|
429
|
+
if (flags.json) {
|
|
430
|
+
flags.yes = true;
|
|
431
|
+
flags.dev = true;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const renderer = flags.json ? 'silent' : 'default';
|
|
435
|
+
|
|
389
436
|
const ctx = {
|
|
390
437
|
ip: null,
|
|
391
438
|
osRelease: null,
|
|
@@ -437,44 +484,7 @@ export async function main() {
|
|
|
437
484
|
},
|
|
438
485
|
],
|
|
439
486
|
{
|
|
440
|
-
renderer
|
|
441
|
-
rendererOptions: { collapseSubtasks: false },
|
|
442
|
-
exitOnError: true,
|
|
443
|
-
},
|
|
444
|
-
);
|
|
445
|
-
|
|
446
|
-
// Phase 2: Installation tasks
|
|
447
|
-
const installTasks = new Listr(
|
|
448
|
-
[
|
|
449
|
-
{
|
|
450
|
-
title: 'Hardening operating system',
|
|
451
|
-
task: (_ctx, task) => hardenTasks(ctx, task),
|
|
452
|
-
},
|
|
453
|
-
{
|
|
454
|
-
title: 'Installing Node.js 20',
|
|
455
|
-
task: (_ctx, task) => nodeTasks(ctx, task),
|
|
456
|
-
},
|
|
457
|
-
{
|
|
458
|
-
title: 'Generating mTLS certificates',
|
|
459
|
-
task: (_ctx, task) => mtlsTasks(ctx, task),
|
|
460
|
-
},
|
|
461
|
-
{
|
|
462
|
-
title: 'Configuring nginx',
|
|
463
|
-
task: (_ctx, task) => nginxTasks(ctx, task),
|
|
464
|
-
},
|
|
465
|
-
{
|
|
466
|
-
title: 'Deploying Portlama panel',
|
|
467
|
-
task: (_ctx, task) => panelTasks(ctx, task),
|
|
468
|
-
},
|
|
469
|
-
{
|
|
470
|
-
title: 'Installation complete',
|
|
471
|
-
task: async () => {
|
|
472
|
-
// Summary will be printed after Listr finishes
|
|
473
|
-
},
|
|
474
|
-
},
|
|
475
|
-
],
|
|
476
|
-
{
|
|
477
|
-
renderer: 'default',
|
|
487
|
+
renderer,
|
|
478
488
|
rendererOptions: { collapseSubtasks: false },
|
|
479
489
|
exitOnError: true,
|
|
480
490
|
},
|
|
@@ -482,7 +492,13 @@ export async function main() {
|
|
|
482
492
|
|
|
483
493
|
try {
|
|
484
494
|
// Run environment checks first
|
|
495
|
+
if (flags.json) {
|
|
496
|
+
emitJson({ event: 'step', step: 'check_environment', status: 'running' });
|
|
497
|
+
}
|
|
485
498
|
await envTasks.run();
|
|
499
|
+
if (flags.json) {
|
|
500
|
+
emitJson({ event: 'step', step: 'check_environment', status: 'complete' });
|
|
501
|
+
}
|
|
486
502
|
|
|
487
503
|
// Detect existing system state for the confirmation banner
|
|
488
504
|
const existingState = await detectExistingState();
|
|
@@ -490,30 +506,96 @@ export async function main() {
|
|
|
490
506
|
// Determine mode: redeploy (fast update) or full install
|
|
491
507
|
const isRedeploy = existingState.portlamaExists && !flags.forceFull;
|
|
492
508
|
|
|
493
|
-
// Show confirmation banner and wait for user input
|
|
494
|
-
|
|
509
|
+
// Show confirmation banner and wait for user input (skipped in JSON mode)
|
|
510
|
+
if (!flags.json) {
|
|
511
|
+
await confirmInstallation(flags, isRedeploy, existingState);
|
|
512
|
+
}
|
|
495
513
|
|
|
496
514
|
if (isRedeploy) {
|
|
497
|
-
|
|
498
|
-
|
|
515
|
+
if (flags.json) {
|
|
516
|
+
// JSON mode: emit step-level progress for redeploy
|
|
517
|
+
await runJsonStep(ctx, {
|
|
518
|
+
key: 'redeploy_panel',
|
|
519
|
+
title: 'Redeploying Portlama panel',
|
|
520
|
+
fn: redeployTasks,
|
|
521
|
+
});
|
|
522
|
+
} else {
|
|
523
|
+
// Fast path: only update panel files and restart
|
|
524
|
+
const redeployTaskList = new Listr(
|
|
525
|
+
[
|
|
526
|
+
{
|
|
527
|
+
title: 'Redeploying Portlama panel',
|
|
528
|
+
task: (_ctx, task) => redeployTasks(ctx, task),
|
|
529
|
+
},
|
|
530
|
+
],
|
|
531
|
+
{
|
|
532
|
+
renderer,
|
|
533
|
+
rendererOptions: { collapseSubtasks: false },
|
|
534
|
+
exitOnError: true,
|
|
535
|
+
},
|
|
536
|
+
);
|
|
537
|
+
await redeployTaskList.run();
|
|
538
|
+
}
|
|
539
|
+
} else if (flags.json) {
|
|
540
|
+
// JSON mode: run each install step individually with NDJSON progress
|
|
541
|
+
const installSteps = [
|
|
542
|
+
{ key: 'harden_system', title: 'Hardening operating system', fn: hardenTasks, skip: () => ctx.skipHarden },
|
|
543
|
+
{ key: 'install_node', title: 'Installing Node.js 20', fn: nodeTasks },
|
|
544
|
+
{ key: 'generate_certs', title: 'Generating mTLS certificates', fn: mtlsTasks },
|
|
545
|
+
{ key: 'configure_nginx', title: 'Configuring nginx', fn: nginxTasks },
|
|
546
|
+
{ key: 'deploy_panel', title: 'Deploying Portlama panel', fn: panelTasks },
|
|
547
|
+
];
|
|
548
|
+
for (const step of installSteps) {
|
|
549
|
+
await runJsonStep(ctx, step);
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
// Full install path with interactive Listr2 rendering
|
|
553
|
+
const installTasks = new Listr(
|
|
499
554
|
[
|
|
500
555
|
{
|
|
501
|
-
title: '
|
|
502
|
-
task: (_ctx, task) =>
|
|
556
|
+
title: 'Hardening operating system',
|
|
557
|
+
task: (_ctx, task) => hardenTasks(ctx, task),
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
title: 'Installing Node.js 20',
|
|
561
|
+
task: (_ctx, task) => nodeTasks(ctx, task),
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
title: 'Generating mTLS certificates',
|
|
565
|
+
task: (_ctx, task) => mtlsTasks(ctx, task),
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
title: 'Configuring nginx',
|
|
569
|
+
task: (_ctx, task) => nginxTasks(ctx, task),
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
title: 'Deploying Portlama panel',
|
|
573
|
+
task: (_ctx, task) => panelTasks(ctx, task),
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
title: 'Installation complete',
|
|
577
|
+
task: async () => {
|
|
578
|
+
// Summary will be printed after Listr finishes
|
|
579
|
+
},
|
|
503
580
|
},
|
|
504
581
|
],
|
|
505
582
|
{
|
|
506
|
-
renderer
|
|
583
|
+
renderer,
|
|
507
584
|
rendererOptions: { collapseSubtasks: false },
|
|
508
585
|
exitOnError: true,
|
|
509
586
|
},
|
|
510
587
|
);
|
|
511
|
-
await redeployTaskList.run();
|
|
512
|
-
} else {
|
|
513
|
-
// Full install path
|
|
514
588
|
await installTasks.run();
|
|
515
589
|
}
|
|
516
590
|
} catch (error) {
|
|
591
|
+
if (flags.json) {
|
|
592
|
+
emitJson({
|
|
593
|
+
event: 'error',
|
|
594
|
+
message: error.message || 'Unknown error',
|
|
595
|
+
recoverable: false,
|
|
596
|
+
});
|
|
597
|
+
process.exit(1);
|
|
598
|
+
}
|
|
517
599
|
console.error('\n');
|
|
518
600
|
console.error(' ┌─────────────────────────────────────────────┐');
|
|
519
601
|
console.error(' │ Portlama installation failed. │');
|
|
@@ -526,6 +608,18 @@ export async function main() {
|
|
|
526
608
|
process.exit(1);
|
|
527
609
|
}
|
|
528
610
|
|
|
529
|
-
|
|
530
|
-
|
|
611
|
+
if (flags.json) {
|
|
612
|
+
emitJson({
|
|
613
|
+
event: 'complete',
|
|
614
|
+
server: {
|
|
615
|
+
ip: ctx.ip,
|
|
616
|
+
panelUrl: `https://${ctx.ip}:9292`,
|
|
617
|
+
p12Path: `${ctx.pkiDir}/client.p12`,
|
|
618
|
+
p12PasswordPath: `${ctx.pkiDir}/.p12-password`,
|
|
619
|
+
},
|
|
620
|
+
});
|
|
621
|
+
} else {
|
|
622
|
+
// Print summary after Listr2 finishes rendering
|
|
623
|
+
await printSummary(ctx);
|
|
624
|
+
}
|
|
531
625
|
}
|