@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,390 @@
1
+ import { readFile, writeFile, rename, open } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { loadAgentRegistry, saveAgentRegistry, withRegistryLock } from './mtls.js';
5
+ import { isIpAllowed } from './ip-utils.js';
6
+
7
+ const STATE_DIR = process.env.LAMALIBRE_LAMASTE_STATE_DIR || '/etc/lamalibre/lamaste';
8
+
9
+ // Promise-chain mutex to serialize push install modifications
10
+ let pushInstallLock = Promise.resolve();
11
+ function withPushInstallLock(fn) {
12
+ const prev = pushInstallLock;
13
+ let resolve;
14
+ pushInstallLock = new Promise((r) => {
15
+ resolve = r;
16
+ });
17
+ return prev.then(fn).finally(resolve);
18
+ }
19
+
20
+ // --- Push install config ---
21
+
22
+ function pushInstallConfigPath() {
23
+ return path.join(STATE_DIR, 'push-install-config.json');
24
+ }
25
+
26
+ const DEFAULT_POLICY = {
27
+ id: 'default',
28
+ name: 'Default',
29
+ description: 'Standard push install policy',
30
+ allowedIps: [],
31
+ deniedIps: [],
32
+ allowedPlugins: [],
33
+ allowedActions: ['install', 'update', 'check-prerequisites'],
34
+ };
35
+
36
+ const DEFAULT_PUSH_INSTALL_CONFIG = {
37
+ enabled: false,
38
+ policies: [structuredClone(DEFAULT_POLICY)],
39
+ defaultPolicy: 'default',
40
+ };
41
+
42
+ /**
43
+ * Read push install configuration from disk.
44
+ * Returns defaults if the file does not exist.
45
+ */
46
+ export async function readPushInstallConfig() {
47
+ try {
48
+ const raw = await readFile(pushInstallConfigPath(), 'utf-8');
49
+ const parsed = JSON.parse(raw);
50
+
51
+ return {
52
+ enabled: parsed.enabled ?? false,
53
+ policies: Array.isArray(parsed.policies)
54
+ ? parsed.policies
55
+ : [structuredClone(DEFAULT_POLICY)],
56
+ defaultPolicy: parsed.defaultPolicy || 'default',
57
+ };
58
+ } catch (err) {
59
+ if (err.code === 'ENOENT') {
60
+ return structuredClone(DEFAULT_PUSH_INSTALL_CONFIG);
61
+ }
62
+ throw new Error(`Failed to read push install config: ${err.message}`);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Write push install configuration to disk atomically.
68
+ */
69
+ export async function writePushInstallConfig(config) {
70
+ const filePath = pushInstallConfigPath();
71
+ const tmpPath = `${filePath}.tmp`;
72
+
73
+ const content = JSON.stringify(config, null, 2) + '\n';
74
+ await writeFile(tmpPath, content, { encoding: 'utf-8', mode: 0o600 });
75
+
76
+ const fd = await open(tmpPath, 'r');
77
+ await fd.sync();
78
+ await fd.close();
79
+
80
+ await rename(tmpPath, filePath);
81
+ }
82
+
83
+ // --- Push install sessions audit log ---
84
+
85
+ function pushInstallSessionsPath() {
86
+ return path.join(STATE_DIR, 'push-install-sessions.json');
87
+ }
88
+
89
+ /**
90
+ * Read the push install sessions audit log.
91
+ */
92
+ export async function readPushInstallSessions() {
93
+ try {
94
+ const raw = await readFile(pushInstallSessionsPath(), 'utf-8');
95
+ const parsed = JSON.parse(raw);
96
+ return Array.isArray(parsed) ? parsed : [];
97
+ } catch (err) {
98
+ if (err.code === 'ENOENT') {
99
+ return [];
100
+ }
101
+ throw new Error(`Failed to read push install sessions: ${err.message}`);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Write the push install sessions audit log atomically.
107
+ */
108
+ async function writePushInstallSessions(sessions) {
109
+ const filePath = pushInstallSessionsPath();
110
+ const tmpPath = `${filePath}.tmp`;
111
+
112
+ const content = JSON.stringify(sessions, null, 2) + '\n';
113
+ await writeFile(tmpPath, content, { encoding: 'utf-8', mode: 0o600 });
114
+
115
+ const fd = await open(tmpPath, 'r');
116
+ await fd.sync();
117
+ await fd.close();
118
+
119
+ await rename(tmpPath, filePath);
120
+ }
121
+
122
+ /**
123
+ * Add a session entry to the push install audit log.
124
+ * Wrapped in withPushInstallLock to prevent concurrent read-modify-write races.
125
+ */
126
+ export function logPushInstallSession(entry) {
127
+ return withPushInstallLock(async () => {
128
+ const sessions = await readPushInstallSessions();
129
+ sessions.push({
130
+ id: randomUUID(),
131
+ ...entry,
132
+ timestamp: new Date().toISOString(),
133
+ });
134
+ // Keep last 500 entries
135
+ if (sessions.length > 500) {
136
+ sessions.splice(0, sessions.length - 500);
137
+ }
138
+ await writePushInstallSessions(sessions);
139
+ return sessions[sessions.length - 1];
140
+ });
141
+ }
142
+
143
+ /**
144
+ * Atomically update push install configuration fields.
145
+ * Serialized through the lock to prevent lost updates.
146
+ *
147
+ * @param {{ enabled?: boolean, defaultPolicy?: string }} updates
148
+ * @returns {Promise<object>} The updated config
149
+ */
150
+ export function updatePushInstallConfigFields(updates) {
151
+ return withPushInstallLock(async () => {
152
+ const config = await readPushInstallConfig();
153
+
154
+ if (updates.enabled !== undefined) {
155
+ config.enabled = updates.enabled;
156
+ }
157
+
158
+ if (updates.defaultPolicy !== undefined) {
159
+ const policy = config.policies.find((p) => p.id === updates.defaultPolicy);
160
+ if (!policy) {
161
+ throw Object.assign(new Error(`Policy "${updates.defaultPolicy}" not found`), {
162
+ statusCode: 400,
163
+ });
164
+ }
165
+ config.defaultPolicy = updates.defaultPolicy;
166
+ }
167
+
168
+ await writePushInstallConfig(config);
169
+ return config;
170
+ });
171
+ }
172
+
173
+ // --- Policy CRUD ---
174
+
175
+ /**
176
+ * Slugify a policy name to generate an ID.
177
+ */
178
+ function slugifyPolicyName(name) {
179
+ return name
180
+ .toLowerCase()
181
+ .replace(/[^a-z0-9]+/g, '-')
182
+ .replace(/^-|-$/g, '');
183
+ }
184
+
185
+ /**
186
+ * Create a new push install policy.
187
+ */
188
+ export function createPushInstallPolicy(policyData) {
189
+ return withPushInstallLock(async () => {
190
+ const config = await readPushInstallConfig();
191
+ const id = policyData.id || slugifyPolicyName(policyData.name);
192
+
193
+ if (!id) {
194
+ throw Object.assign(new Error('Policy name cannot be empty'), { statusCode: 400 });
195
+ }
196
+
197
+ const existing = config.policies.find((p) => p.id === id);
198
+ if (existing) {
199
+ throw Object.assign(new Error(`Policy "${id}" already exists`), { statusCode: 409 });
200
+ }
201
+
202
+ const policy = {
203
+ ...structuredClone(DEFAULT_POLICY),
204
+ ...policyData,
205
+ id,
206
+ };
207
+
208
+ config.policies.push(policy);
209
+ await writePushInstallConfig(config);
210
+
211
+ return policy;
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Update an existing push install policy.
217
+ */
218
+ export function updatePushInstallPolicy(id, updates) {
219
+ return withPushInstallLock(async () => {
220
+ const config = await readPushInstallConfig();
221
+ const policy = config.policies.find((p) => p.id === id);
222
+
223
+ if (!policy) {
224
+ throw Object.assign(new Error(`Policy "${id}" not found`), { statusCode: 404 });
225
+ }
226
+
227
+ // Apply updates (only defined fields)
228
+ if (updates.name !== undefined) policy.name = updates.name;
229
+ if (updates.description !== undefined) policy.description = updates.description;
230
+ if (updates.allowedIps !== undefined) policy.allowedIps = updates.allowedIps;
231
+ if (updates.deniedIps !== undefined) policy.deniedIps = updates.deniedIps;
232
+ if (updates.allowedPlugins !== undefined) policy.allowedPlugins = updates.allowedPlugins;
233
+ if (updates.allowedActions !== undefined) policy.allowedActions = updates.allowedActions;
234
+
235
+ await writePushInstallConfig(config);
236
+ return policy;
237
+ });
238
+ }
239
+
240
+ /**
241
+ * Delete a push install policy.
242
+ */
243
+ export function deletePushInstallPolicy(id) {
244
+ return withPushInstallLock(async () => {
245
+ const config = await readPushInstallConfig();
246
+
247
+ if (id === 'default') {
248
+ throw Object.assign(new Error('Cannot delete the default policy'), { statusCode: 400 });
249
+ }
250
+
251
+ const index = config.policies.findIndex((p) => p.id === id);
252
+ if (index === -1) {
253
+ throw Object.assign(new Error(`Policy "${id}" not found`), { statusCode: 404 });
254
+ }
255
+
256
+ config.policies.splice(index, 1);
257
+
258
+ // If defaultPolicy was the deleted one, reset to 'default'
259
+ if (config.defaultPolicy === id) {
260
+ config.defaultPolicy = 'default';
261
+ }
262
+
263
+ await writePushInstallConfig(config);
264
+ return { ok: true, id };
265
+ });
266
+ }
267
+
268
+ // --- Agent push install access management ---
269
+
270
+ /**
271
+ * Enable push install access for an agent certificate.
272
+ * Sets `pushInstallEnabledUntil` and `pushInstallPolicy` on the agent registry entry.
273
+ *
274
+ * @param {string} label - Agent label
275
+ * @param {number} durationMinutes - Session window length in minutes
276
+ * @param {string} [policyId] - Policy ID to assign (defaults to config's defaultPolicy)
277
+ */
278
+ export async function enableAgentPushInstall(label, durationMinutes, policyId) {
279
+ // Read push-install config under its own lock
280
+ const config = await withPushInstallLock(() => readPushInstallConfig());
281
+
282
+ if (!config.enabled) {
283
+ throw Object.assign(new Error('Push install is not enabled globally'), { statusCode: 400 });
284
+ }
285
+
286
+ // Resolve which policy to assign
287
+ const resolvedPolicyId = policyId || config.defaultPolicy;
288
+ const policy = config.policies.find((p) => p.id === resolvedPolicyId);
289
+ if (!policy) {
290
+ throw Object.assign(new Error(`Policy "${resolvedPolicyId}" not found`), { statusCode: 404 });
291
+ }
292
+
293
+ // Modify agent registry under the registry lock (shared with mtls.js)
294
+ return withRegistryLock(async () => {
295
+ const registry = await loadAgentRegistry();
296
+ const agent = registry.agents.find((a) => a.label === label && !a.revoked);
297
+
298
+ if (!agent) {
299
+ throw Object.assign(new Error(`Agent certificate "${label}" not found`), { statusCode: 404 });
300
+ }
301
+
302
+ const until = new Date(Date.now() + durationMinutes * 60 * 1000);
303
+ agent.pushInstallEnabledUntil = until.toISOString();
304
+ agent.pushInstallPolicy = resolvedPolicyId;
305
+ await saveAgentRegistry(registry);
306
+
307
+ return {
308
+ ok: true,
309
+ label,
310
+ pushInstallEnabledUntil: agent.pushInstallEnabledUntil,
311
+ pushInstallPolicy: agent.pushInstallPolicy,
312
+ };
313
+ });
314
+ }
315
+
316
+ /**
317
+ * Disable push install access for an agent certificate.
318
+ */
319
+ export async function disableAgentPushInstall(label) {
320
+ // Modify agent registry under the registry lock (shared with mtls.js)
321
+ return withRegistryLock(async () => {
322
+ const registry = await loadAgentRegistry();
323
+ const agent = registry.agents.find((a) => a.label === label && !a.revoked);
324
+
325
+ if (!agent) {
326
+ throw Object.assign(new Error(`Agent certificate "${label}" not found`), { statusCode: 404 });
327
+ }
328
+
329
+ delete agent.pushInstallEnabledUntil;
330
+ delete agent.pushInstallPolicy;
331
+ await saveAgentRegistry(registry);
332
+
333
+ return { ok: true, label };
334
+ });
335
+ }
336
+
337
+ /**
338
+ * Run the 5-gate auth check for push install access to an agent.
339
+ *
340
+ * 1. Global push install enabled
341
+ * 2. Agent cert exists and is not revoked
342
+ * 3. Agent pushInstallEnabledUntil is in the future
343
+ * 4. Source IP passes the agent's assigned policy allow/deny lists
344
+ * 5. (Caller is admin — enforced by route preHandler, not checked here)
345
+ *
346
+ * @param {string} label - Agent label
347
+ * @param {string} sourceIp - Requesting client's IP address
348
+ * @returns {Promise<{ ok: true, agent: object, config: object, policy: object } | { ok: false, error: string, statusCode: number }>}
349
+ */
350
+ export async function validatePushInstallAccess(label, sourceIp) {
351
+ // Gate 1: Global push install enabled
352
+ const config = await readPushInstallConfig();
353
+ if (!config.enabled) {
354
+ return { ok: false, error: 'Push install is not enabled globally', statusCode: 400 };
355
+ }
356
+
357
+ // Gate 2: Agent cert exists and is not revoked
358
+ const registry = await loadAgentRegistry();
359
+ const agent = registry.agents.find((a) => a.label === label && !a.revoked);
360
+ if (!agent) {
361
+ return { ok: false, error: `Agent certificate "${label}" not found`, statusCode: 404 };
362
+ }
363
+
364
+ // Gate 3: Agent pushInstallEnabledUntil is in the future
365
+ if (!agent.pushInstallEnabledUntil || new Date(agent.pushInstallEnabledUntil) <= new Date()) {
366
+ return {
367
+ ok: false,
368
+ error: `Push install access not enabled for agent "${label}"`,
369
+ statusCode: 403,
370
+ };
371
+ }
372
+
373
+ // Resolve the agent's assigned policy
374
+ const policyId = agent.pushInstallPolicy || config.defaultPolicy;
375
+ const policy = config.policies.find((p) => p.id === policyId);
376
+ if (!policy) {
377
+ return {
378
+ ok: false,
379
+ error: `Policy "${policyId}" not found in push install configuration`,
380
+ statusCode: 500,
381
+ };
382
+ }
383
+
384
+ // Gate 4: Source IP passes the policy's allow/deny lists
385
+ if (!isIpAllowed(sourceIp, policy.allowedIps, policy.deniedIps)) {
386
+ return { ok: false, error: 'Source IP is not allowed', statusCode: 403 };
387
+ }
388
+
389
+ return { ok: true, agent, config, policy };
390
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Certificate revocation list (CRL) management.
3
+ *
4
+ * Backed by SQLite (`state.db`, table `revoked_certs`) per
5
+ * docs/decisions/sqlite-migration.md §3d. The companion `agents` table
6
+ * created by `0004_agents.sql` is owned by `lib/mtls.js`.
7
+ *
8
+ * Hot path: the mTLS middleware calls `isRevoked(serial)` on every
9
+ * authenticated request — a single indexed `SELECT serial FROM revoked_certs`
10
+ * materialising the `Set<serial>` the caller expects. No in-memory cache.
11
+ */
12
+ import { getStateDb } from './state-db.js';
13
+
14
+ // --- SQLite prepared-statement bundle (lazy init) ---
15
+
16
+ let stmts = null;
17
+
18
+ async function getStmts() {
19
+ if (stmts) return stmts;
20
+
21
+ const db = await getStateDb();
22
+
23
+ stmts = {
24
+ db,
25
+
26
+ selectAll: db.prepare('SELECT * FROM revoked_certs'),
27
+ selectSerials: db.prepare('SELECT serial FROM revoked_certs'),
28
+ selectBySerial: db.prepare('SELECT serial FROM revoked_certs WHERE serial = ?'),
29
+ insert: db.prepare('INSERT INTO revoked_certs (serial, label, revoked_at) VALUES (?, ?, ?)'),
30
+ deleteBySerial: db.prepare('DELETE FROM revoked_certs WHERE serial = ?'),
31
+
32
+ begin: db.prepare('BEGIN IMMEDIATE'),
33
+ commit: db.prepare('COMMIT'),
34
+ rollback: db.prepare('ROLLBACK'),
35
+ };
36
+
37
+ return stmts;
38
+ }
39
+
40
+ /**
41
+ * Load the revocation list and return a Set of revoked serial numbers.
42
+ *
43
+ * The Set<serial> shape is what the mTLS middleware's hot-path lookup
44
+ * expects.
45
+ *
46
+ * @returns {Promise<Set<string>>} Set of revoked certificate serial numbers.
47
+ */
48
+ export async function loadRevocationList() {
49
+ const s = await getStmts();
50
+ const rows = s.selectSerials.all();
51
+ return new Set(rows.map((r) => r.serial));
52
+ }
53
+
54
+ /**
55
+ * Add a certificate serial to the revocation list.
56
+ *
57
+ * Duplicate serials are silently skipped (idempotent). The check runs with
58
+ * a SELECT inside the BEGIN IMMEDIATE so concurrent callers cannot race
59
+ * past it.
60
+ *
61
+ * @param {string} serial - The certificate serial number to revoke.
62
+ * @param {string} label - A human-readable label for the revoked certificate.
63
+ */
64
+ export async function addToRevocationList(serial, label) {
65
+ const s = await getStmts();
66
+ const revokedAt = new Date().toISOString();
67
+
68
+ s.begin.run();
69
+ try {
70
+ const existing = s.selectBySerial.get(serial);
71
+ if (!existing) {
72
+ s.insert.run(serial, label, revokedAt);
73
+ }
74
+ s.commit.run();
75
+ } catch (err) {
76
+ s.rollback.run();
77
+ throw err;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Remove a certificate serial from the revocation list.
83
+ *
84
+ * @param {string} serial - The certificate serial number to un-revoke.
85
+ */
86
+ export async function removeFromRevocationList(serial) {
87
+ const s = await getStmts();
88
+
89
+ s.begin.run();
90
+ try {
91
+ s.deleteBySerial.run(serial);
92
+ s.commit.run();
93
+ } catch (err) {
94
+ s.rollback.run();
95
+ throw err;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Check if a certificate serial is in the revocation list.
101
+ *
102
+ * Hot-path call from the mTLS middleware on every authenticated request.
103
+ * Uses an indexed primary-key lookup — O(log N) on the SQLite B-tree.
104
+ *
105
+ * @param {string} serial - The certificate serial number to check.
106
+ * @returns {Promise<boolean>} True if the serial is revoked.
107
+ */
108
+ export async function isRevoked(serial) {
109
+ const s = await getStmts();
110
+ const row = s.selectBySerial.get(serial);
111
+ return row !== undefined;
112
+ }
@@ -0,0 +1,131 @@
1
+ import { execa } from 'execa';
2
+
3
+ export const ALLOWED_SERVICES = ['nginx', 'chisel', 'authelia', 'lamalibre-lamaste-serverd'];
4
+
5
+ const ALLOWED_ACTIONS = ['start', 'stop', 'restart', 'reload'];
6
+
7
+ /** Services whose unit supports `reload` (and has a sudoers rule for it). */
8
+ const RELOADABLE_SERVICES = ['nginx', 'authelia'];
9
+
10
+ /**
11
+ * Format a duration in milliseconds to a human-readable string like "2d 5h 30m".
12
+ */
13
+ function formatDuration(ms) {
14
+ if (ms <= 0) return '0s';
15
+
16
+ const seconds = Math.floor(ms / 1000);
17
+ const days = Math.floor(seconds / 86400);
18
+ const hours = Math.floor((seconds % 86400) / 3600);
19
+ const minutes = Math.floor((seconds % 3600) / 60);
20
+ const secs = seconds % 60;
21
+
22
+ const parts = [];
23
+ if (days > 0) parts.push(`${days}d`);
24
+ if (hours > 0) parts.push(`${hours}h`);
25
+ if (minutes > 0) parts.push(`${minutes}m`);
26
+ if (parts.length === 0) parts.push(`${secs}s`);
27
+
28
+ return parts.join(' ');
29
+ }
30
+
31
+ /**
32
+ * Query the status and uptime of a single service.
33
+ */
34
+ async function getServiceStatus(name) {
35
+ try {
36
+ const { stdout: statusOut } = await execa('systemctl', ['is-active', name]);
37
+ const status = statusOut.trim();
38
+
39
+ let uptime = null;
40
+ if (status === 'active') {
41
+ try {
42
+ const { stdout: timestampOut } = await execa('systemctl', [
43
+ 'show',
44
+ name,
45
+ '--property=ActiveEnterTimestamp',
46
+ ]);
47
+ // Output looks like: ActiveEnterTimestamp=Thu 2024-03-14 10:30:45 UTC
48
+ const match = timestampOut.match(/=(.+)$/);
49
+ if (match && match[1]) {
50
+ const activeEnter = new Date(match[1]);
51
+ if (!Number.isNaN(activeEnter.getTime())) {
52
+ uptime = formatDuration(Date.now() - activeEnter.getTime());
53
+ }
54
+ }
55
+ } catch {
56
+ // If we can't get uptime, that's not fatal
57
+ }
58
+ }
59
+
60
+ return { name, status, uptime };
61
+ } catch {
62
+ // Service not installed or systemctl failed for this service
63
+ // Check the exit code to distinguish inactive vs failed vs unknown
64
+ try {
65
+ const { stdout } = await execa('systemctl', ['is-active', name], {
66
+ reject: false,
67
+ });
68
+ const status = stdout.trim();
69
+ if (status === 'inactive' || status === 'failed') {
70
+ return { name, status, uptime: null };
71
+ }
72
+ } catch {
73
+ // Fall through to unknown
74
+ }
75
+ return { name, status: 'unknown', uptime: null };
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Retrieve status and uptime of all managed services in parallel.
81
+ */
82
+ export async function getAllServiceStatuses() {
83
+ const results = await Promise.all(ALLOWED_SERVICES.map(getServiceStatus));
84
+ return results;
85
+ }
86
+
87
+ /**
88
+ * Validate a service name against the allowlist.
89
+ */
90
+ export function isAllowedService(name) {
91
+ return ALLOWED_SERVICES.includes(name);
92
+ }
93
+
94
+ /**
95
+ * Validate an action against the allowlist.
96
+ */
97
+ export function isAllowedAction(action) {
98
+ return ALLOWED_ACTIONS.includes(action);
99
+ }
100
+
101
+ /**
102
+ * Execute a systemctl action on a managed service.
103
+ * Returns { ok: true, name, action } on success.
104
+ * Throws with a descriptive error on failure.
105
+ */
106
+ export async function executeServiceAction(name, action) {
107
+ // Safety: cannot stop the panel from the UI
108
+ if (name === 'lamalibre-lamaste-serverd' && action === 'stop') {
109
+ const err = new Error(
110
+ 'Cannot stop the panel service from the UI — it would terminate this session',
111
+ );
112
+ err.statusCode = 400;
113
+ throw err;
114
+ }
115
+ if (action === 'reload' && !RELOADABLE_SERVICES.includes(name)) {
116
+ const err = new Error(`${name} does not support reload — use restart`);
117
+ err.statusCode = 400;
118
+ throw err;
119
+ }
120
+
121
+ try {
122
+ await execa('sudo', ['systemctl', action, name], { timeout: 30_000 });
123
+ return { ok: true, name, action };
124
+ } catch (err) {
125
+ const details = err.stderr || err.message;
126
+ const error = new Error(`Failed to ${action} ${name}`);
127
+ error.statusCode = 500;
128
+ error.details = details;
129
+ throw error;
130
+ }
131
+ }