@lamalibre/create-lamaste 4.0.0

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.
Files changed (270) hide show
  1. package/LICENSE.md +118 -0
  2. package/README.md +114 -0
  3. package/bin/create-lamaste.js +15 -0
  4. package/package.json +54 -0
  5. package/scripts/bundle-vendor.js +110 -0
  6. package/scripts/lamaste-cert-info +62 -0
  7. package/scripts/lamaste-certbot +96 -0
  8. package/scripts/lamaste-priv +693 -0
  9. package/src/index.js +646 -0
  10. package/src/lib/accounts.js +86 -0
  11. package/src/lib/binaries.js +137 -0
  12. package/src/lib/cert-help-page.js +160 -0
  13. package/src/lib/env.js +132 -0
  14. package/src/lib/http-redirect.js +38 -0
  15. package/src/lib/ownership.js +231 -0
  16. package/src/lib/relay-services.js +103 -0
  17. package/src/lib/secrets.js +19 -0
  18. package/src/lib/service-config.js +311 -0
  19. package/src/lib/summary.js +131 -0
  20. package/src/lib/wrappers.js +63 -0
  21. package/src/tasks/gatekeeper.js +233 -0
  22. package/src/tasks/harden.js +293 -0
  23. package/src/tasks/mtls.js +193 -0
  24. package/src/tasks/nginx.js +265 -0
  25. package/src/tasks/node.js +110 -0
  26. package/src/tasks/panel.js +408 -0
  27. package/src/tasks/redeploy.js +438 -0
  28. package/vendor/docs/00-introduction/how-it-works.md +447 -0
  29. package/vendor/docs/00-introduction/quickstart.md +472 -0
  30. package/vendor/docs/00-introduction/what-is-lamaste.md +112 -0
  31. package/vendor/docs/01-concepts/authentication.md +464 -0
  32. package/vendor/docs/01-concepts/authorization.md +242 -0
  33. package/vendor/docs/01-concepts/certificates.md +452 -0
  34. package/vendor/docs/01-concepts/dns-and-domains.md +343 -0
  35. package/vendor/docs/01-concepts/mtls.md +490 -0
  36. package/vendor/docs/01-concepts/nginx-reverse-proxy.md +540 -0
  37. package/vendor/docs/01-concepts/security-model.md +600 -0
  38. package/vendor/docs/01-concepts/tickets.md +414 -0
  39. package/vendor/docs/01-concepts/tunneling.md +367 -0
  40. package/vendor/docs/02-guides/agent-setup.md +194 -0
  41. package/vendor/docs/02-guides/certificate-management.md +341 -0
  42. package/vendor/docs/02-guides/cloud-provisioning.md +406 -0
  43. package/vendor/docs/02-guides/desktop-app-setup.md +226 -0
  44. package/vendor/docs/02-guides/disaster-recovery.md +464 -0
  45. package/vendor/docs/02-guides/first-tunnel.md +230 -0
  46. package/vendor/docs/02-guides/installation.md +393 -0
  47. package/vendor/docs/02-guides/managing-users.md +255 -0
  48. package/vendor/docs/02-guides/onboarding.md +244 -0
  49. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +375 -0
  50. package/vendor/docs/02-guides/static-sites.md +364 -0
  51. package/vendor/docs/03-architecture/e2e-three-vm-sequences.md +457 -0
  52. package/vendor/docs/03-architecture/installer-flow.md +151 -0
  53. package/vendor/docs/03-architecture/installer.md +445 -0
  54. package/vendor/docs/03-architecture/management-flow.md +288 -0
  55. package/vendor/docs/03-architecture/nginx-configuration.md +830 -0
  56. package/vendor/docs/03-architecture/onboarding-flow.md +164 -0
  57. package/vendor/docs/03-architecture/overview.md +541 -0
  58. package/vendor/docs/03-architecture/panel-client.md +416 -0
  59. package/vendor/docs/03-architecture/panel-server.md +695 -0
  60. package/vendor/docs/03-architecture/state-management.md +529 -0
  61. package/vendor/docs/03-architecture/system-overview.md +167 -0
  62. package/vendor/docs/04-api-reference/certificates.md +1141 -0
  63. package/vendor/docs/04-api-reference/gatekeeper.md +592 -0
  64. package/vendor/docs/04-api-reference/onboarding.md +473 -0
  65. package/vendor/docs/04-api-reference/overview.md +404 -0
  66. package/vendor/docs/04-api-reference/services.md +267 -0
  67. package/vendor/docs/04-api-reference/settings.md +343 -0
  68. package/vendor/docs/04-api-reference/sites.md +716 -0
  69. package/vendor/docs/04-api-reference/system.md +268 -0
  70. package/vendor/docs/04-api-reference/tickets.md +618 -0
  71. package/vendor/docs/04-api-reference/tunnels.md +417 -0
  72. package/vendor/docs/04-api-reference/users.md +333 -0
  73. package/vendor/docs/05-operations/backup-and-restore.md +342 -0
  74. package/vendor/docs/05-operations/monitoring.md +285 -0
  75. package/vendor/docs/05-operations/uninstalling.md +344 -0
  76. package/vendor/docs/05-operations/upgrades.md +270 -0
  77. package/vendor/docs/06-reference/config-files.md +1080 -0
  78. package/vendor/docs/06-reference/glossary.md +528 -0
  79. package/vendor/docs/06-reference/installer-flags.md +260 -0
  80. package/vendor/docs/06-reference/ports-and-services.md +244 -0
  81. package/vendor/docs/06-reference/troubleshooting.md +665 -0
  82. package/vendor/docs/_index.json +233 -0
  83. package/vendor/docs/gatekeeper-design.md +953 -0
  84. package/vendor/docs/package.json +15 -0
  85. package/vendor/docs/todo/lib-cli-desktop-refactor.md +641 -0
  86. package/vendor/gatekeeper/dist/boot.d.ts +1 -0
  87. package/vendor/gatekeeper/dist/boot.d.ts.map +1 -0
  88. package/vendor/gatekeeper/dist/boot.js +17 -0
  89. package/vendor/gatekeeper/dist/boot.js.map +1 -0
  90. package/vendor/gatekeeper/dist/cli/commands/access.d.ts +2 -0
  91. package/vendor/gatekeeper/dist/cli/commands/access.d.ts.map +1 -0
  92. package/vendor/gatekeeper/dist/cli/commands/access.js +33 -0
  93. package/vendor/gatekeeper/dist/cli/commands/access.js.map +1 -0
  94. package/vendor/gatekeeper/dist/cli/commands/grants.d.ts +2 -0
  95. package/vendor/gatekeeper/dist/cli/commands/grants.d.ts.map +1 -0
  96. package/vendor/gatekeeper/dist/cli/commands/grants.js +110 -0
  97. package/vendor/gatekeeper/dist/cli/commands/grants.js.map +1 -0
  98. package/vendor/gatekeeper/dist/cli/commands/groups.d.ts +2 -0
  99. package/vendor/gatekeeper/dist/cli/commands/groups.d.ts.map +1 -0
  100. package/vendor/gatekeeper/dist/cli/commands/groups.js +98 -0
  101. package/vendor/gatekeeper/dist/cli/commands/groups.js.map +1 -0
  102. package/vendor/gatekeeper/dist/cli/index.d.ts +3 -0
  103. package/vendor/gatekeeper/dist/cli/index.d.ts.map +1 -0
  104. package/vendor/gatekeeper/dist/cli/index.js +45 -0
  105. package/vendor/gatekeeper/dist/cli/index.js.map +1 -0
  106. package/vendor/gatekeeper/dist/index.d.ts +9 -0
  107. package/vendor/gatekeeper/dist/index.d.ts.map +1 -0
  108. package/vendor/gatekeeper/dist/index.js +13 -0
  109. package/vendor/gatekeeper/dist/index.js.map +1 -0
  110. package/vendor/gatekeeper/dist/lib/authz.d.ts +16 -0
  111. package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -0
  112. package/vendor/gatekeeper/dist/lib/authz.js +42 -0
  113. package/vendor/gatekeeper/dist/lib/authz.js.map +1 -0
  114. package/vendor/gatekeeper/dist/lib/cache-bust.d.ts +15 -0
  115. package/vendor/gatekeeper/dist/lib/cache-bust.d.ts.map +1 -0
  116. package/vendor/gatekeeper/dist/lib/cache-bust.js +19 -0
  117. package/vendor/gatekeeper/dist/lib/cache-bust.js.map +1 -0
  118. package/vendor/gatekeeper/dist/lib/constants.d.ts +82 -0
  119. package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -0
  120. package/vendor/gatekeeper/dist/lib/constants.js +79 -0
  121. package/vendor/gatekeeper/dist/lib/constants.js.map +1 -0
  122. package/vendor/gatekeeper/dist/lib/grants.d.ts +32 -0
  123. package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -0
  124. package/vendor/gatekeeper/dist/lib/grants.js +314 -0
  125. package/vendor/gatekeeper/dist/lib/grants.js.map +1 -0
  126. package/vendor/gatekeeper/dist/lib/groups.d.ts +10 -0
  127. package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -0
  128. package/vendor/gatekeeper/dist/lib/groups.js +247 -0
  129. package/vendor/gatekeeper/dist/lib/groups.js.map +1 -0
  130. package/vendor/gatekeeper/dist/lib/migration.d.ts +17 -0
  131. package/vendor/gatekeeper/dist/lib/migration.d.ts.map +1 -0
  132. package/vendor/gatekeeper/dist/lib/migration.js +79 -0
  133. package/vendor/gatekeeper/dist/lib/migration.js.map +1 -0
  134. package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0001_groups.sql +7 -0
  135. package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0002_access_grants.sql +14 -0
  136. package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0003_access_request_log.sql +9 -0
  137. package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +7 -0
  138. package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +14 -0
  139. package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +9 -0
  140. package/vendor/gatekeeper/dist/lib/state-db.d.ts +16 -0
  141. package/vendor/gatekeeper/dist/lib/state-db.d.ts.map +1 -0
  142. package/vendor/gatekeeper/dist/lib/state-db.js +154 -0
  143. package/vendor/gatekeeper/dist/lib/state-db.js.map +1 -0
  144. package/vendor/gatekeeper/dist/lib/templates.d.ts +21 -0
  145. package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -0
  146. package/vendor/gatekeeper/dist/lib/templates.js +151 -0
  147. package/vendor/gatekeeper/dist/lib/templates.js.map +1 -0
  148. package/vendor/gatekeeper/dist/lib/types.d.ts +178 -0
  149. package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -0
  150. package/vendor/gatekeeper/dist/lib/types.js +2 -0
  151. package/vendor/gatekeeper/dist/lib/types.js.map +1 -0
  152. package/vendor/gatekeeper/dist/server/index.d.ts +15 -0
  153. package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -0
  154. package/vendor/gatekeeper/dist/server/index.js +251 -0
  155. package/vendor/gatekeeper/dist/server/index.js.map +1 -0
  156. package/vendor/gatekeeper/dist/server/routes/access-request.d.ts +7 -0
  157. package/vendor/gatekeeper/dist/server/routes/access-request.d.ts.map +1 -0
  158. package/vendor/gatekeeper/dist/server/routes/access-request.js +17 -0
  159. package/vendor/gatekeeper/dist/server/routes/access-request.js.map +1 -0
  160. package/vendor/gatekeeper/dist/server/routes/authz.d.ts +3 -0
  161. package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -0
  162. package/vendor/gatekeeper/dist/server/routes/authz.js +279 -0
  163. package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -0
  164. package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts +13 -0
  165. package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -0
  166. package/vendor/gatekeeper/dist/server/routes/diagnostic.js +249 -0
  167. package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -0
  168. package/vendor/gatekeeper/dist/server/routes/grants.d.ts +3 -0
  169. package/vendor/gatekeeper/dist/server/routes/grants.d.ts.map +1 -0
  170. package/vendor/gatekeeper/dist/server/routes/grants.js +112 -0
  171. package/vendor/gatekeeper/dist/server/routes/grants.js.map +1 -0
  172. package/vendor/gatekeeper/dist/server/routes/groups.d.ts +3 -0
  173. package/vendor/gatekeeper/dist/server/routes/groups.d.ts.map +1 -0
  174. package/vendor/gatekeeper/dist/server/routes/groups.js +139 -0
  175. package/vendor/gatekeeper/dist/server/routes/groups.js.map +1 -0
  176. package/vendor/gatekeeper/package.json +49 -0
  177. package/vendor/server/bin/lamaste-server.js +204 -0
  178. package/vendor/server/package.json +31 -0
  179. package/vendor/server/src/commands/certs.js +224 -0
  180. package/vendor/server/src/commands/chisel.js +132 -0
  181. package/vendor/server/src/commands/logs.js +49 -0
  182. package/vendor/server/src/commands/plugins.js +321 -0
  183. package/vendor/server/src/commands/reset-admin.js +478 -0
  184. package/vendor/server/src/commands/restart.js +94 -0
  185. package/vendor/server/src/commands/sites.js +257 -0
  186. package/vendor/server/src/commands/status.js +158 -0
  187. package/vendor/server/src/commands/tunnels.js +365 -0
  188. package/vendor/server/src/commands/uninstall.js +326 -0
  189. package/vendor/server/src/config.js +38 -0
  190. package/vendor/server/src/exec.js +26 -0
  191. package/vendor/server/src/logger.js +39 -0
  192. package/vendor/server/src/ndjson.js +56 -0
  193. package/vendor/server/src/panel-api.js +85 -0
  194. package/vendor/server/src/state.js +45 -0
  195. package/vendor/server-ui/dist/assets/index-B0nWuUyL.js +257 -0
  196. package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +1 -0
  197. package/vendor/server-ui/dist/index.html +13 -0
  198. package/vendor/serverd/package.json +44 -0
  199. package/vendor/serverd/src/boot.js +13 -0
  200. package/vendor/serverd/src/index.js +191 -0
  201. package/vendor/serverd/src/lib/app-error.js +14 -0
  202. package/vendor/serverd/src/lib/authelia.js +85 -0
  203. package/vendor/serverd/src/lib/certbot.js +74 -0
  204. package/vendor/serverd/src/lib/chisel-args.js +6 -0
  205. package/vendor/serverd/src/lib/chisel-reconcile.js +232 -0
  206. package/vendor/serverd/src/lib/chisel-runtime.js +46 -0
  207. package/vendor/serverd/src/lib/chisel-users.js +109 -0
  208. package/vendor/serverd/src/lib/chisel.js +81 -0
  209. package/vendor/serverd/src/lib/config.js +190 -0
  210. package/vendor/serverd/src/lib/constants.js +8 -0
  211. package/vendor/serverd/src/lib/csr-signing.js +331 -0
  212. package/vendor/serverd/src/lib/enrollment.js +302 -0
  213. package/vendor/serverd/src/lib/files.js +19 -0
  214. package/vendor/serverd/src/lib/gatekeeper-client.js +135 -0
  215. package/vendor/serverd/src/lib/invite-page.js +281 -0
  216. package/vendor/serverd/src/lib/ip-utils.js +79 -0
  217. package/vendor/serverd/src/lib/migrations/state/0001_enrollment_tokens.sql +17 -0
  218. package/vendor/serverd/src/lib/migrations/state/0002_tickets.sql +70 -0
  219. package/vendor/serverd/src/lib/migrations/state/0003_plugins.sql +15 -0
  220. package/vendor/serverd/src/lib/migrations/state/0004_agents.sql +19 -0
  221. package/vendor/serverd/src/lib/mtls.js +795 -0
  222. package/vendor/serverd/src/lib/nginx.js +1090 -0
  223. package/vendor/serverd/src/lib/pki-paths.js +41 -0
  224. package/vendor/serverd/src/lib/pki-sign.js +103 -0
  225. package/vendor/serverd/src/lib/plugins.js +538 -0
  226. package/vendor/serverd/src/lib/push-install.js +390 -0
  227. package/vendor/serverd/src/lib/revocation.js +112 -0
  228. package/vendor/serverd/src/lib/services.js +131 -0
  229. package/vendor/serverd/src/lib/session.js +151 -0
  230. package/vendor/serverd/src/lib/state-db.js +130 -0
  231. package/vendor/serverd/src/lib/state.js +140 -0
  232. package/vendor/serverd/src/lib/storage.js +63 -0
  233. package/vendor/serverd/src/lib/system-stats.js +58 -0
  234. package/vendor/serverd/src/lib/tickets.js +1556 -0
  235. package/vendor/serverd/src/lib/totp.js +76 -0
  236. package/vendor/serverd/src/lib/tunnel-deps.js +41 -0
  237. package/vendor/serverd/src/lib/user-access-session.js +142 -0
  238. package/vendor/serverd/src/lib/user-access.js +52 -0
  239. package/vendor/serverd/src/middleware/errors.js +63 -0
  240. package/vendor/serverd/src/middleware/mtls.js +120 -0
  241. package/vendor/serverd/src/middleware/onboarding-guard.js +30 -0
  242. package/vendor/serverd/src/middleware/role-guard.js +58 -0
  243. package/vendor/serverd/src/middleware/twofa-session.js +65 -0
  244. package/vendor/serverd/src/middleware/user-access-session.js +52 -0
  245. package/vendor/serverd/src/routes/enrollment.js +120 -0
  246. package/vendor/serverd/src/routes/health.js +22 -0
  247. package/vendor/serverd/src/routes/invite.js +163 -0
  248. package/vendor/serverd/src/routes/management/agents.js +182 -0
  249. package/vendor/serverd/src/routes/management/certs.js +911 -0
  250. package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +160 -0
  251. package/vendor/serverd/src/routes/management/identity.js +107 -0
  252. package/vendor/serverd/src/routes/management/invitations.js +166 -0
  253. package/vendor/serverd/src/routes/management/logs.js +218 -0
  254. package/vendor/serverd/src/routes/management/plugins.js +560 -0
  255. package/vendor/serverd/src/routes/management/services.js +63 -0
  256. package/vendor/serverd/src/routes/management/settings.js +325 -0
  257. package/vendor/serverd/src/routes/management/sites.js +522 -0
  258. package/vendor/serverd/src/routes/management/storage.js +192 -0
  259. package/vendor/serverd/src/routes/management/system.js +201 -0
  260. package/vendor/serverd/src/routes/management/tickets.js +457 -0
  261. package/vendor/serverd/src/routes/management/tunnels.js +709 -0
  262. package/vendor/serverd/src/routes/management/users.js +377 -0
  263. package/vendor/serverd/src/routes/management.js +36 -0
  264. package/vendor/serverd/src/routes/onboarding/dns.js +101 -0
  265. package/vendor/serverd/src/routes/onboarding/domain.js +42 -0
  266. package/vendor/serverd/src/routes/onboarding/index.js +18 -0
  267. package/vendor/serverd/src/routes/onboarding/provision.js +327 -0
  268. package/vendor/serverd/src/routes/onboarding/status.js +12 -0
  269. package/vendor/serverd/src/routes/plugin-router.js +79 -0
  270. package/vendor/serverd/src/routes/user-access.js +389 -0
@@ -0,0 +1,365 @@
1
+ /**
2
+ * lamaste-server tunnels — Manage server tunnels.
3
+ *
4
+ * Subcommands:
5
+ * list List active tunnels
6
+ * create --subdomain <s> --port <p> --agent <l> Create a tunnel carried by an agent
7
+ * delete <id> Delete a tunnel by ID
8
+ * toggle <id> --enable|--disable Enable or disable a tunnel
9
+ * assign <id> --agent <label> Move a tunnel to another agent
10
+ * configure <id> [--access-mode <m>] [--max-body-mb <n>]
11
+ * Change access mode / body limit
12
+ *
13
+ * List reads directly from the state file.
14
+ * Create/delete/toggle use the panel REST API (which has the full
15
+ * nginx/certbot/chisel dependency stack).
16
+ */
17
+
18
+ import chalk from 'chalk';
19
+ import { readTunnels } from '../state.js';
20
+ import { panelRequest } from '../panel-api.js';
21
+ import { emit, emitError, emitComplete } from '../ndjson.js';
22
+
23
+ /**
24
+ * @param {string[]} args
25
+ * @param {{ json: boolean }} options
26
+ */
27
+ export async function runTunnels(args, { json }) {
28
+ const sub = args[0];
29
+
30
+ switch (sub) {
31
+ case 'list':
32
+ return listTunnels({ json });
33
+ case 'create':
34
+ return createTunnel(args.slice(1), { json });
35
+ case 'delete':
36
+ return deleteTunnel(args[1], { json });
37
+ case 'toggle':
38
+ return toggleTunnel(args.slice(1), { json });
39
+ case 'assign':
40
+ return assignTunnel(args.slice(1), { json });
41
+ case 'configure':
42
+ return configureTunnel(args.slice(1), { json });
43
+ default:
44
+ printTunnelUsage();
45
+ process.exit(sub ? 1 : 0);
46
+ }
47
+ }
48
+
49
+ function printTunnelUsage() {
50
+ const b = chalk.bold;
51
+ const c = chalk.cyan;
52
+ console.log(`
53
+ ${b('Usage:')} lamaste-server tunnels <subcommand>
54
+
55
+ ${b('Subcommands:')}
56
+ ${c('list')} List active tunnels
57
+ ${c('create')} --subdomain <s> --port <p> --agent <l> [opts] Create a tunnel
58
+ ${c('delete')} <id> Delete a tunnel by ID
59
+ ${c('toggle')} <id> --enable|--disable Enable or disable a tunnel
60
+ ${c('assign')} <id> --agent <label> Move a tunnel to another agent
61
+ ${c('configure')} <id> [--access-mode <m>] [--max-body-mb <n>] Change access mode / body limit
62
+
63
+ ${b('Create options:')}
64
+ --subdomain <name> Tunnel subdomain (required)
65
+ --port <number> Local port to tunnel (required)
66
+ --agent <label> Agent that carries the tunnel (required)
67
+ --description <text> Optional description
68
+ --access-mode <mode> Access mode: public, authenticated, restricted (default: restricted)
69
+ --max-body-mb <n> Largest request body in MiB, 1-10240 (default: 10)
70
+ `);
71
+ }
72
+
73
+ /**
74
+ * @param {{ json: boolean }} options
75
+ */
76
+ async function listTunnels({ json }) {
77
+ const tunnels = await readTunnels();
78
+
79
+ if (json) {
80
+ emit({ tunnels });
81
+ return;
82
+ }
83
+
84
+ if (tunnels.length === 0) {
85
+ console.log('\n No tunnels configured.\n');
86
+ return;
87
+ }
88
+
89
+ const b = chalk.bold;
90
+ const c = chalk.cyan;
91
+ const g = chalk.green;
92
+ const r = chalk.red;
93
+ const d = chalk.dim;
94
+
95
+ console.log('');
96
+ console.log(b(' Tunnels'));
97
+ console.log(d(' ' + '\u2500'.repeat(50)));
98
+
99
+ for (const t of tunnels) {
100
+ const enabled = t.enabled !== false ? g('enabled') : r('disabled');
101
+ const accessMode = t.accessMode ? d(`[${t.accessMode}]`) : '';
102
+ const type = t.type !== 'app' ? d(`(${t.type})`) : '';
103
+ const carrier = t.agentLabel ? d(`via ${t.agentLabel}`) : r('no agent');
104
+ const bodyLimit = t.type !== 'panel' ? d(`${t.maxBodySizeMb ?? 1} MiB`) : '';
105
+ console.log(
106
+ ` ${c(t.subdomain)} \u2192 :${t.port} ${enabled} ${carrier} ${accessMode} ${bodyLimit} ${type}`.trimEnd(),
107
+ );
108
+ if (t.description) {
109
+ console.log(` ${d(t.description)}`);
110
+ }
111
+ console.log(` ${d(`id: ${t.id} fqdn: ${t.fqdn}`)}`);
112
+ }
113
+ console.log('');
114
+ }
115
+
116
+ /**
117
+ * @param {string[]} args
118
+ * @param {{ json: boolean }} options
119
+ */
120
+ async function createTunnel(args, { json }) {
121
+ const subdomain = getArg(args, 'subdomain');
122
+ const portStr = getArg(args, 'port');
123
+ const description = getArg(args, 'description') || undefined;
124
+ const accessMode = getArg(args, 'access-mode') || undefined;
125
+ const agentLabel = getArg(args, 'agent');
126
+ const maxBodyStr = getArg(args, 'max-body-mb');
127
+
128
+ if (!subdomain || !portStr || !agentLabel) {
129
+ const msg = 'Error: --subdomain, --port and --agent are required';
130
+ if (json) emitError(msg);
131
+ else console.error(`\n ${msg}\n`);
132
+ process.exit(1);
133
+ }
134
+
135
+ const port = parseInt(portStr, 10);
136
+ if (isNaN(port) || port < 1 || port > 65535) {
137
+ const msg = 'Error: --port must be a valid port number (1-65535)';
138
+ if (json) emitError(msg);
139
+ else console.error(`\n ${msg}\n`);
140
+ process.exit(1);
141
+ }
142
+
143
+ if (accessMode && !['public', 'authenticated', 'restricted'].includes(accessMode)) {
144
+ const msg = 'Error: --access-mode must be public, authenticated, or restricted';
145
+ if (json) emitError(msg);
146
+ else console.error(`\n ${msg}\n`);
147
+ process.exit(1);
148
+ }
149
+
150
+ const maxBodySizeMb = maxBodyStr === null ? undefined : parseMaxBody(maxBodyStr, json);
151
+
152
+ /** @type {Record<string, unknown>} */
153
+ const body = { subdomain, port, agentLabel };
154
+ if (description) body.description = description;
155
+ if (accessMode) body.accessMode = accessMode;
156
+ if (maxBodySizeMb !== undefined) body.maxBodySizeMb = maxBodySizeMb;
157
+
158
+ if (!json) process.stderr.write(` Creating tunnel ${chalk.cyan(subdomain)} \u2192 :${port}...`);
159
+
160
+ try {
161
+ const { tunnel } = await panelRequest('POST', '/api/tunnels', body);
162
+
163
+ if (json) {
164
+ emitComplete({ tunnel });
165
+ } else {
166
+ console.log(` ${chalk.green('ok')}`);
167
+ console.log(` FQDN: ${chalk.cyan(tunnel.fqdn)}`);
168
+ console.log(` ID: ${chalk.dim(tunnel.id)}`);
169
+ console.log(` Agent: ${tunnel.agentLabel}`);
170
+ console.log(chalk.dim(` ${tunnel.agentLabel} starts carrying it within 30 seconds.`));
171
+ console.log('');
172
+ }
173
+ } catch (err) {
174
+ if (json) emitError(err.message);
175
+ else {
176
+ console.log(` ${chalk.red('failed')}`);
177
+ console.error(` ${chalk.red(err.message)}\n`);
178
+ }
179
+ process.exit(1);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * @param {string | undefined} id
185
+ * @param {{ json: boolean }} options
186
+ */
187
+ async function deleteTunnel(id, { json }) {
188
+ if (!id) {
189
+ const msg = 'Usage: lamaste-server tunnels delete <id>';
190
+ if (json) emitError(msg);
191
+ else console.error(`\n ${msg}\n`);
192
+ process.exit(1);
193
+ }
194
+
195
+ if (!json) process.stderr.write(` Deleting tunnel ${chalk.dim(id)}...`);
196
+
197
+ try {
198
+ await panelRequest('DELETE', `/api/tunnels/${id}`);
199
+ if (json) {
200
+ emitComplete({ id, deleted: true });
201
+ } else {
202
+ console.log(` ${chalk.green('ok')}\n`);
203
+ }
204
+ } catch (err) {
205
+ if (json) emitError(err.message);
206
+ else {
207
+ console.log(` ${chalk.red('failed')}`);
208
+ console.error(` ${chalk.red(err.message)}\n`);
209
+ }
210
+ process.exit(1);
211
+ }
212
+ }
213
+
214
+ /**
215
+ * @param {string[]} args
216
+ * @param {{ json: boolean }} options
217
+ */
218
+ async function toggleTunnel(args, { json }) {
219
+ const id = args[0];
220
+ const enable = args.includes('--enable');
221
+ const disable = args.includes('--disable');
222
+
223
+ if (!id || (!enable && !disable)) {
224
+ const msg = 'Usage: lamaste-server tunnels toggle <id> --enable|--disable';
225
+ if (json) emitError(msg);
226
+ else console.error(`\n ${msg}\n`);
227
+ process.exit(1);
228
+ }
229
+
230
+ const enabled = enable;
231
+
232
+ if (!json) {
233
+ const action = enabled ? 'Enabling' : 'Disabling';
234
+ process.stderr.write(` ${action} tunnel ${chalk.dim(id)}...`);
235
+ }
236
+
237
+ try {
238
+ const { tunnel } = await panelRequest('PATCH', `/api/tunnels/${id}`, { enabled });
239
+ if (json) {
240
+ emitComplete({ id, enabled, tunnel });
241
+ } else {
242
+ console.log(` ${chalk.green('ok')}\n`);
243
+ }
244
+ } catch (err) {
245
+ if (json) emitError(err.message);
246
+ else {
247
+ console.log(` ${chalk.red('failed')}`);
248
+ console.error(` ${chalk.red(err.message)}\n`);
249
+ }
250
+ process.exit(1);
251
+ }
252
+ }
253
+
254
+ /**
255
+ * @param {string[]} args
256
+ * @param {{ json: boolean }} options
257
+ */
258
+ async function assignTunnel(args, { json }) {
259
+ const id = args[0];
260
+ const agentLabel = getArg(args, 'agent');
261
+
262
+ if (!id || !agentLabel) {
263
+ const msg = 'Usage: lamaste-server tunnels assign <id> --agent <label>';
264
+ if (json) emitError(msg);
265
+ else console.error(`\n ${msg}\n`);
266
+ process.exit(1);
267
+ }
268
+
269
+ if (!json) process.stderr.write(` Assigning tunnel ${chalk.dim(id)} to ${agentLabel}...`);
270
+
271
+ try {
272
+ const { tunnel } = await panelRequest('PATCH', `/api/tunnels/${id}`, { agentLabel });
273
+ if (json) {
274
+ emitComplete({ id, tunnel });
275
+ } else {
276
+ console.log(` ${chalk.green('ok')}`);
277
+ console.log(
278
+ chalk.dim(` ${agentLabel} and the previous agent pick up the change within 30 seconds.\n`),
279
+ );
280
+ }
281
+ } catch (err) {
282
+ if (json) emitError(err.message);
283
+ else {
284
+ console.log(` ${chalk.red('failed')}`);
285
+ console.error(` ${chalk.red(err.message)}\n`);
286
+ }
287
+ process.exit(1);
288
+ }
289
+ }
290
+
291
+ /**
292
+ * @param {string[]} args
293
+ * @param {{ json: boolean }} options
294
+ */
295
+ async function configureTunnel(args, { json }) {
296
+ const id = args[0];
297
+ const accessMode = getArg(args, 'access-mode');
298
+ const maxBodyStr = getArg(args, 'max-body-mb');
299
+
300
+ if (!id || (accessMode === null && maxBodyStr === null)) {
301
+ const msg =
302
+ 'Usage: lamaste-server tunnels configure <id> [--access-mode <mode>] [--max-body-mb <n>]';
303
+ if (json) emitError(msg);
304
+ else console.error(`\n ${msg}\n`);
305
+ process.exit(1);
306
+ }
307
+ if (accessMode !== null && !['public', 'authenticated', 'restricted'].includes(accessMode)) {
308
+ const msg = 'Error: --access-mode must be public, authenticated, or restricted';
309
+ if (json) emitError(msg);
310
+ else console.error(`\n ${msg}\n`);
311
+ process.exit(1);
312
+ }
313
+
314
+ /** @type {Record<string, unknown>} */
315
+ const body = {};
316
+ if (accessMode !== null) body.accessMode = accessMode;
317
+ if (maxBodyStr !== null) body.maxBodySizeMb = parseMaxBody(maxBodyStr, json);
318
+
319
+ if (!json) process.stderr.write(` Reconfiguring tunnel ${chalk.dim(id)}...`);
320
+
321
+ try {
322
+ const { tunnel } = await panelRequest('PATCH', `/api/tunnels/${id}`, body);
323
+ if (json) {
324
+ emitComplete({ id, tunnel });
325
+ } else {
326
+ console.log(` ${chalk.green('ok')}`);
327
+ console.log(` Access: ${tunnel.accessMode}`);
328
+ console.log(` Body limit: ${tunnel.maxBodySizeMb} MiB\n`);
329
+ }
330
+ } catch (err) {
331
+ if (json) emitError(err.message);
332
+ else {
333
+ console.log(` ${chalk.red('failed')}`);
334
+ console.error(` ${chalk.red(err.message)}\n`);
335
+ }
336
+ process.exit(1);
337
+ }
338
+ }
339
+
340
+ /**
341
+ * @param {string} value
342
+ * @param {boolean} json
343
+ * @returns {number}
344
+ */
345
+ function parseMaxBody(value, json) {
346
+ const n = Number(value);
347
+ if (!Number.isInteger(n) || n < 1 || n > 10240) {
348
+ const msg = 'Error: --max-body-mb must be an integer from 1 to 10240';
349
+ if (json) emitError(msg);
350
+ else console.error(`\n ${msg}\n`);
351
+ process.exit(1);
352
+ }
353
+ return n;
354
+ }
355
+
356
+ /**
357
+ * @param {string[]} args
358
+ * @param {string} name
359
+ * @returns {string | null}
360
+ */
361
+ function getArg(args, name) {
362
+ const idx = args.indexOf(`--${name}`);
363
+ if (idx === -1 || idx + 1 >= args.length) return null;
364
+ return args[idx + 1];
365
+ }
@@ -0,0 +1,326 @@
1
+ /**
2
+ * lamaste-server uninstall — Full server teardown.
3
+ *
4
+ * Cleanup order (reverse of installation):
5
+ *
6
+ * 1. Stop and disable services (panel, gatekeeper)
7
+ * 2. Remove systemd service files
8
+ * 3. Remove nginx configurations (vhosts, sites)
9
+ * 4. Reload nginx (falls back to default config)
10
+ * 5. Remove certbot certificates
11
+ * 6. Remove Authelia configuration
12
+ * 7. Remove Lamaste data directory (/etc/lamalibre/lamaste)
13
+ * 8. Remove installed npm packages
14
+ * 9. Remove lamaste system user
15
+ *
16
+ * Requires root access. Interactive confirmation unless --force is passed.
17
+ */
18
+
19
+ import { readFile } from 'node:fs/promises';
20
+ import { createInterface } from 'node:readline';
21
+ import chalk from 'chalk';
22
+ import { execa } from 'execa';
23
+ import { CONFIG_PATH, STATE_DIR, PANEL_SERVICE, GATEKEEPER_SERVICE } from '../config.js';
24
+ import { emitStep, emitError, emitComplete } from '../ndjson.js';
25
+
26
+ /**
27
+ * @param {string[]} args
28
+ * @param {{ json: boolean }} options
29
+ */
30
+ export async function runUninstall(args, { json }) {
31
+ // Verify running as root
32
+ if (process.getuid && process.getuid() !== 0) {
33
+ const msg = 'lamaste-server uninstall must be run as root.';
34
+ if (json) emitError(msg);
35
+ else {
36
+ console.error(`\n Error: ${msg}`);
37
+ console.error(' Usage: sudo lamaste-server uninstall\n');
38
+ }
39
+ process.exit(1);
40
+ }
41
+
42
+ const force = args.includes('--force');
43
+
44
+ // Read config to get domain info for display
45
+ let config = null;
46
+ try {
47
+ const raw = await readFile(CONFIG_PATH, 'utf-8');
48
+ config = JSON.parse(raw);
49
+ } catch {
50
+ // Config may not exist
51
+ }
52
+
53
+ const domain = config?.domain || 'unknown';
54
+
55
+ if (!force && !json) {
56
+ console.log('');
57
+ console.log(chalk.bold.red(' WARNING: This will completely remove Lamaste from this server.'));
58
+ console.log('');
59
+ console.log(` Domain: ${chalk.cyan(String(domain))}`);
60
+ console.log(` Data dir: ${chalk.dim(STATE_DIR)}`);
61
+ console.log('');
62
+ console.log(' This action will:');
63
+ console.log(' - Stop and remove all Lamaste services');
64
+ console.log(' - Remove all nginx tunnel/site configurations');
65
+ console.log(" - Remove all certificates (mTLS and Let's Encrypt)");
66
+ console.log(' - Remove Authelia configuration');
67
+ console.log(' - Delete all Lamaste data');
68
+ console.log('');
69
+
70
+ const confirmed = await promptConfirm(' Type "uninstall" to confirm: ', 'uninstall');
71
+ if (!confirmed) {
72
+ console.log('\n Aborted.\n');
73
+ process.exit(0);
74
+ }
75
+ console.log('');
76
+ }
77
+
78
+ const steps = [
79
+ { name: 'stop-services', label: 'Stopping services', fn: stopServices },
80
+ { name: 'remove-services', label: 'Removing service files', fn: removeServices },
81
+ { name: 'remove-nginx', label: 'Removing nginx configurations', fn: removeNginxConfigs },
82
+ { name: 'reload-nginx', label: 'Reloading nginx', fn: reloadNginx },
83
+ { name: 'remove-certbot', label: 'Removing certbot certificates', fn: removeCertbotCerts },
84
+ { name: 'remove-authelia', label: 'Removing Authelia', fn: removeAuthelia },
85
+ { name: 'remove-data', label: 'Removing Lamaste data', fn: removeData },
86
+ { name: 'remove-packages', label: 'Removing npm packages', fn: removePackages },
87
+ { name: 'remove-user', label: 'Removing lamaste user', fn: removeUser },
88
+ {
89
+ name: 'remove-authelia-user',
90
+ label: 'Removing lamaste-authelia user',
91
+ fn: removeAutheliaUser,
92
+ },
93
+ ];
94
+
95
+ for (const step of steps) {
96
+ if (json) emitStep(step.name, 'running', step.label);
97
+ else process.stderr.write(` ${step.label}...`);
98
+
99
+ try {
100
+ await step.fn(config);
101
+ if (json) emitStep(step.name, 'complete');
102
+ else console.log(` ${chalk.green('ok')}`);
103
+ } catch (err) {
104
+ // Non-fatal: log and continue with remaining steps
105
+ const msg = err.message || String(err);
106
+ if (json) emitStep(step.name, 'failed', msg);
107
+ else console.log(` ${chalk.yellow('skipped')} ${chalk.dim(`(${msg})`)}`);
108
+ }
109
+ }
110
+
111
+ if (json) {
112
+ emitComplete({ uninstalled: true });
113
+ } else {
114
+ console.log('');
115
+ console.log(chalk.green(' Lamaste has been removed from this server.'));
116
+ console.log('');
117
+ }
118
+ }
119
+
120
+ async function stopServices() {
121
+ for (const service of [PANEL_SERVICE, GATEKEEPER_SERVICE, 'chisel']) {
122
+ try {
123
+ await execa('systemctl', ['stop', service], { timeout: 15000 });
124
+ } catch {
125
+ // Service may not be running
126
+ }
127
+ try {
128
+ await execa('systemctl', ['disable', service], { timeout: 10000 });
129
+ } catch {
130
+ // Service may not be enabled
131
+ }
132
+ }
133
+ }
134
+
135
+ async function removeServices() {
136
+ const serviceFiles = [
137
+ `/etc/systemd/system/${PANEL_SERVICE}.service`,
138
+ `/etc/systemd/system/${GATEKEEPER_SERVICE}.service`,
139
+ '/etc/systemd/system/chisel.service',
140
+ '/etc/systemd/system/authelia.service',
141
+ ];
142
+
143
+ for (const file of serviceFiles) {
144
+ await execa('rm', ['-f', file]).catch(() => {});
145
+ }
146
+
147
+ await execa('systemctl', ['daemon-reload']).catch(() => {});
148
+
149
+ // Remove sudoers file and the root-owned wrappers it referenced
150
+ await execa('rm', ['-f', '/etc/sudoers.d/lamaste']).catch(() => {});
151
+ await execa('rm', [
152
+ '-f',
153
+ '/usr/local/sbin/lamaste-priv',
154
+ '/usr/local/sbin/lamaste-certbot',
155
+ '/usr/local/sbin/lamaste-cert-info',
156
+ // installed by versions before 4.0
157
+ '/usr/local/sbin/lamaste-sign-csr',
158
+ '/usr/local/sbin/lamaste-pki-rename',
159
+ ]).catch(() => {});
160
+ }
161
+
162
+ /**
163
+ * @param {Record<string, unknown> | null} _config
164
+ */
165
+ async function removeNginxConfigs(_config) {
166
+ // Remove lamaste-specific sites
167
+ const patterns = [
168
+ '/etc/nginx/sites-enabled/lamalibre-lamaste-*',
169
+ '/etc/nginx/sites-available/lamalibre-lamaste-*',
170
+ '/etc/nginx/snippets/lamalibre-lamaste-*',
171
+ ];
172
+
173
+ for (const pattern of patterns) {
174
+ // Use shell glob expansion safely via rm
175
+ await execa('sh', ['-c', `rm -f ${pattern}`]).catch(() => {});
176
+ }
177
+
178
+ // Remove any tunnel/site vhosts (they use the subdomain as filename)
179
+ // Read tunnels and sites to identify their vhost files
180
+ try {
181
+ const tunnelsRaw = await readFile(`${STATE_DIR}/tunnels.json`, 'utf-8');
182
+ const tunnels = JSON.parse(tunnelsRaw);
183
+ if (Array.isArray(tunnels)) {
184
+ for (const t of tunnels) {
185
+ if (t.subdomain) {
186
+ await execa('rm', [
187
+ '-f',
188
+ `/etc/nginx/sites-enabled/${t.subdomain}`,
189
+ `/etc/nginx/sites-available/${t.subdomain}`,
190
+ ]).catch(() => {});
191
+ }
192
+ }
193
+ }
194
+ } catch {
195
+ // No tunnels file
196
+ }
197
+
198
+ try {
199
+ const sitesRaw = await readFile(`${STATE_DIR}/sites.json`, 'utf-8');
200
+ const sites = JSON.parse(sitesRaw);
201
+ if (Array.isArray(sites)) {
202
+ for (const s of sites) {
203
+ if (s.id) {
204
+ await execa('rm', [
205
+ '-f',
206
+ `/etc/nginx/sites-enabled/site-${s.id}`,
207
+ `/etc/nginx/sites-available/site-${s.id}`,
208
+ ]).catch(() => {});
209
+ }
210
+ }
211
+ }
212
+ } catch {
213
+ // No sites file
214
+ }
215
+ }
216
+
217
+ async function reloadNginx() {
218
+ await execa('nginx', ['-t'], { timeout: 10000 });
219
+ await execa('systemctl', ['reload', 'nginx'], { timeout: 10000 });
220
+ }
221
+
222
+ async function removeCertbotCerts() {
223
+ // List all certbot certificates and delete lamaste-related ones
224
+ try {
225
+ const { stdout } = await execa('certbot', ['certificates', '--non-interactive'], {
226
+ timeout: 30000,
227
+ });
228
+
229
+ const certNames = [];
230
+ for (const match of stdout.matchAll(/Certificate Name:\s+(.+)/g)) {
231
+ certNames.push(match[1].trim());
232
+ }
233
+
234
+ for (const name of certNames) {
235
+ await execa('certbot', ['delete', '--non-interactive', '--cert-name', name], {
236
+ timeout: 15000,
237
+ }).catch(() => {});
238
+ }
239
+ } catch {
240
+ // certbot may not be installed
241
+ }
242
+ }
243
+
244
+ async function removeAuthelia() {
245
+ // Stop and disable authelia
246
+ await execa('systemctl', ['stop', 'authelia']).catch(() => {});
247
+ await execa('systemctl', ['disable', 'authelia']).catch(() => {});
248
+
249
+ // Remove its configuration, database and log, and the binary
250
+ // create-lamaste installed (the pinned release, at /usr/local/bin).
251
+ await execa('rm', ['-rf', '/etc/authelia', '/var/log/authelia']).catch(() => {});
252
+ await execa('rm', ['-f', '/usr/local/bin/authelia']).catch(() => {});
253
+ }
254
+
255
+ async function removeData() {
256
+ // Remove the entire lamaste data directory
257
+ await execa('rm', ['-rf', STATE_DIR]);
258
+
259
+ // Remove site content directories
260
+ await execa('rm', ['-rf', '/var/www/lamaste']).catch(() => {});
261
+
262
+ // Remove chisel binary
263
+ await execa('rm', ['-f', '/usr/local/bin/chisel']).catch(() => {});
264
+
265
+ // Remove the lamaste-reset-admin symlink (legacy)
266
+ await execa('rm', ['-f', '/usr/local/bin/lamaste-reset-admin']).catch(() => {});
267
+ }
268
+
269
+ async function removePackages() {
270
+ // Locate and remove globally installed lamaste packages
271
+ try {
272
+ const { stdout } = await execa('npm', ['ls', '-g', '--depth=0', '--json'], {
273
+ timeout: 15000,
274
+ });
275
+ const globalPkgs = JSON.parse(stdout);
276
+ const deps = globalPkgs.dependencies || {};
277
+
278
+ for (const [name] of Object.entries(deps)) {
279
+ if (name.startsWith('@lamalibre/')) {
280
+ await execa('npm', ['uninstall', '-g', name], { timeout: 30000 }).catch(() => {});
281
+ }
282
+ }
283
+ } catch {
284
+ // Global packages listing may fail
285
+ }
286
+
287
+ // Remove the panel server installation directory
288
+ await execa('rm', ['-rf', '/opt/lamalibre/lamaste']).catch(() => {});
289
+ }
290
+
291
+ async function removeUser() {
292
+ // Check if lamaste user exists
293
+ try {
294
+ await execa('id', ['lamaste']);
295
+ } catch {
296
+ return; // User does not exist
297
+ }
298
+
299
+ // Remove the user and its home directory
300
+ await execa('userdel', ['-r', 'lamaste']).catch(() => {});
301
+
302
+ // Remove groups if they still exist
303
+ await execa('groupdel', ['lamaste']).catch(() => {});
304
+ await execa('groupdel', ['lamaste-chisel']).catch(() => {});
305
+ }
306
+
307
+ async function removeAutheliaUser() {
308
+ await execa('userdel', ['lamaste-authelia']).catch(() => {});
309
+ await execa('groupdel', ['lamaste-authelia']).catch(() => {});
310
+ }
311
+
312
+ /**
313
+ * Prompt the user for confirmation.
314
+ * @param {string} prompt
315
+ * @param {string} expected The exact text the user must type
316
+ * @returns {Promise<boolean>}
317
+ */
318
+ function promptConfirm(prompt, expected) {
319
+ return new Promise((resolve) => {
320
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
321
+ rl.question(prompt, (answer) => {
322
+ rl.close();
323
+ resolve(answer.trim() === expected);
324
+ });
325
+ });
326
+ }