@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,201 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { readFile, writeFile, rename, open, mkdir } from 'node:fs/promises';
4
+ import { join, dirname } from 'node:path';
5
+ import { z } from 'zod';
6
+ import { getSystemStats } from '../../lib/system-stats.js';
7
+ import { getConfig } from '../../lib/config.js';
8
+ import { setPluginCapabilities } from '../../lib/mtls.js';
9
+ import { getPluginCapabilities } from '../../lib/plugins.js';
10
+ import { PLUGIN_CAPABILITY_REGEX } from '@lamalibre/lamaste';
11
+
12
+ const execFileAsync = promisify(execFile);
13
+
14
+ /** Root helper that starts create-lamaste in a transient unit (see lamaste-priv). */
15
+ const PRIV = '/usr/local/sbin/lamaste-priv';
16
+
17
+ // Persist agent-reported capabilities so they survive server restarts
18
+ const AGENT_CAPS_FILE = () => join(getConfig().dataDir, 'agent-plugin-caps.json');
19
+
20
+ async function loadAgentReportedCaps() {
21
+ try {
22
+ const raw = await readFile(AGENT_CAPS_FILE(), 'utf-8');
23
+ const parsed = JSON.parse(raw);
24
+ return Array.isArray(parsed.capabilities) ? parsed.capabilities : [];
25
+ } catch (err) {
26
+ if (err.code === 'ENOENT') return [];
27
+ return [];
28
+ }
29
+ }
30
+
31
+ async function saveAgentReportedCaps(caps) {
32
+ const filePath = AGENT_CAPS_FILE();
33
+ const tmpPath = `${filePath}.tmp`;
34
+ await mkdir(dirname(filePath), { recursive: true });
35
+ const content = JSON.stringify({ capabilities: caps }, null, 2) + '\n';
36
+ await writeFile(tmpPath, content, { encoding: 'utf-8', mode: 0o600 });
37
+ const fd = await open(tmpPath, 'r');
38
+ await fd.sync();
39
+ await fd.close();
40
+ await rename(tmpPath, filePath);
41
+ }
42
+
43
+ const UpdateBodySchema = z.object({
44
+ version: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver (e.g. 1.0.43)'),
45
+ });
46
+
47
+ export default async function systemRoutes(fastify, _opts) {
48
+ // Load agent-reported capabilities from disk on startup
49
+ try {
50
+ const persistedCaps = await loadAgentReportedCaps();
51
+ if (persistedCaps.length > 0) {
52
+ const serverCaps = await getPluginCapabilities();
53
+ const merged = [...new Set([...serverCaps, ...persistedCaps])];
54
+ setPluginCapabilities(merged);
55
+ fastify.log.info(
56
+ { count: persistedCaps.length },
57
+ 'Loaded persisted agent plugin capabilities',
58
+ );
59
+ }
60
+ } catch (err) {
61
+ fastify.log.warn({ err: err.message }, 'Failed to load agent-reported capabilities');
62
+ }
63
+ fastify.get(
64
+ '/system/stats',
65
+ {
66
+ preHandler: fastify.requireRole(['admin', 'agent'], { capability: 'system:read' }),
67
+ },
68
+ async (request, reply) => {
69
+ try {
70
+ const stats = await getSystemStats(request.log);
71
+ return stats;
72
+ } catch {
73
+ return reply.code(500).send({ error: 'Failed to retrieve system stats' });
74
+ }
75
+ },
76
+ );
77
+
78
+ // ------------------------------------------------------------------
79
+ // POST /system/update — trigger a panel server update
80
+ //
81
+ // The root helper `lamaste-priv self-update <version>` starts
82
+ // `create-lamaste@<version> --yes` (the redeploy path) in a transient
83
+ // systemd unit, two seconds from now. A transient unit is critical: the
84
+ // panel runs as the lamalibre-lamaste-serverd service, and systemd's
85
+ // default KillMode (control-group) kills ALL processes in the cgroup
86
+ // when the installer stops the service. The helper takes only the
87
+ // version — the panel never supplies a script for root to run.
88
+ //
89
+ // Returns 202 immediately — caller should poll /api/health until
90
+ // the server comes back with the new version.
91
+ // ------------------------------------------------------------------
92
+ fastify.post(
93
+ '/system/update',
94
+ {
95
+ preHandler: fastify.requireRole(['admin']),
96
+ // Moderate tier — starts a privileged update via lamaste-priv;
97
+ // the global 100/min default is too generous for an operation that
98
+ // restarts the panel daemon.
99
+ config: { rateLimit: { max: 30, timeWindow: '1 minute' } },
100
+ },
101
+ async (request, reply) => {
102
+ const body = UpdateBodySchema.parse(request.body);
103
+ const { version } = body;
104
+
105
+ let unitName;
106
+ try {
107
+ const { stdout } = await execFileAsync('sudo', [PRIV, 'self-update', version], {
108
+ timeout: 30_000,
109
+ });
110
+ unitName = stdout.trim();
111
+ } catch (err) {
112
+ request.log.error({ err, version }, 'Failed to launch update unit');
113
+ return reply.code(500).send({
114
+ error: `Could not start the update: ${err.stderr?.trim() || err.message}`,
115
+ });
116
+ }
117
+
118
+ request.log.info({ version, unit: unitName }, 'Panel update initiated via systemd-run');
119
+
120
+ return reply.code(202).send({
121
+ ok: true,
122
+ message: `Update to create-lamaste@${version} initiated. The panel will restart shortly.`,
123
+ });
124
+ },
125
+ );
126
+
127
+ // ------------------------------------------------------------------
128
+ // POST /agents/plugins/report — accept agent plugin capability report
129
+ //
130
+ // Agents report their enabled plugins during `lamaste-agent update`.
131
+ // The server merges the reported plugin capabilities into the valid
132
+ // capabilities set so they can be assigned to agents.
133
+ // ------------------------------------------------------------------
134
+
135
+ // Capability strings must be in the namespaced plugin form
136
+ // `plugin:<short-name>:<action>` (see PLUGIN_CAPABILITY_REGEX). The
137
+ // route-side prefix-scoping below additionally requires the `<short-name>`
138
+ // segment to match the reporting plugin's own derived route.
139
+ const PluginReportSchema = z.object({
140
+ plugins: z.array(
141
+ z.object({
142
+ name: z.string().regex(/^[a-z0-9-]+$/),
143
+ version: z.string(),
144
+ capabilities: z.array(z.string().regex(PLUGIN_CAPABILITY_REGEX)).default([]),
145
+ }),
146
+ ),
147
+ });
148
+
149
+ fastify.post(
150
+ '/agents/plugins/report',
151
+ {
152
+ preHandler: fastify.requireRole(['admin', 'agent']),
153
+ },
154
+ async (request, reply) => {
155
+ const result = PluginReportSchema.safeParse(request.body);
156
+ if (!result.success) {
157
+ return reply
158
+ .code(400)
159
+ .send({ error: 'Invalid plugin report', details: result.error.issues });
160
+ }
161
+ const { plugins } = result.data;
162
+
163
+ // Collect capabilities, scoped to the reporting plugin's namespace.
164
+ // Only accept caps whose `<short-name>` segment matches the plugin's
165
+ // own name — this stops a compromised plugin from contributing
166
+ // capabilities that another plugin has claimed (or from inventing new
167
+ // top-level namespaces; the regex above already blocks anything
168
+ // outside `plugin:*`).
169
+ const reportedCaps = new Set();
170
+ for (const plugin of plugins) {
171
+ const expectedPrefix = `plugin:${plugin.name}:`;
172
+ for (const cap of plugin.capabilities) {
173
+ if (cap.startsWith(expectedPrefix)) {
174
+ reportedCaps.add(cap);
175
+ }
176
+ }
177
+ }
178
+
179
+ if (reportedCaps.size === 0) {
180
+ return { ok: true, merged: 0 };
181
+ }
182
+
183
+ // Merge with existing server-side plugin capabilities
184
+ const serverCaps = await getPluginCapabilities();
185
+ const allPluginCaps = [...new Set([...serverCaps, ...reportedCaps])];
186
+ setPluginCapabilities(allPluginCaps);
187
+
188
+ // Persist agent-reported capabilities so they survive server restarts
189
+ const existingAgentCaps = await loadAgentReportedCaps();
190
+ const allAgentCaps = [...new Set([...existingAgentCaps, ...reportedCaps])];
191
+ await saveAgentReportedCaps(allAgentCaps);
192
+
193
+ request.log.info(
194
+ { agentPlugins: plugins.length, newCaps: reportedCaps.size },
195
+ 'Agent plugin capabilities reported',
196
+ );
197
+
198
+ return { ok: true, merged: reportedCaps.size };
199
+ },
200
+ );
201
+ }
@@ -0,0 +1,457 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ RegisterScopeSchema,
4
+ RegisterInstanceSchema,
5
+ RequestTicketSchema,
6
+ ValidateTicketSchema,
7
+ CreateSessionSchema,
8
+ UpdateSessionSchema,
9
+ AssignmentSchema,
10
+ TicketScopeNameSchema,
11
+ registerScope,
12
+ listScopes,
13
+ unregisterScope,
14
+ registerInstance,
15
+ deregisterInstance,
16
+ instanceHeartbeat,
17
+ createAssignment,
18
+ removeAssignment,
19
+ listAssignments,
20
+ requestTicket,
21
+ getTicketInbox,
22
+ validateTicket,
23
+ listTickets,
24
+ revokeTicket,
25
+ createSession,
26
+ sessionHeartbeat,
27
+ updateSession,
28
+ killSession,
29
+ listSessions,
30
+ } from '../../lib/tickets.js';
31
+
32
+ // Param validation schemas
33
+ const HexIdSchema = z
34
+ .string()
35
+ .min(1)
36
+ .max(128)
37
+ .regex(/^[a-f0-9]+$/);
38
+ const SessionIdSchema = z
39
+ .string()
40
+ .min(1)
41
+ .max(128)
42
+ .regex(/^[a-zA-Z0-9_-]+$/);
43
+ // Accepts regular agent labels (e.g., "macbook-pro") and plugin-agent labels
44
+ // (e.g., "plugin-agent:macbook-pro:raspi-sync")
45
+ const AgentLabelSchema = z
46
+ .string()
47
+ .min(1)
48
+ .max(150)
49
+ .regex(/^[a-zA-Z0-9_:-]+$/);
50
+ const InstanceScopeSchema = z
51
+ .string()
52
+ .min(1)
53
+ .max(200)
54
+ .regex(/^plugin:[a-z0-9-]+:[a-z0-9-]+:[a-f0-9]+$/);
55
+
56
+ export default async function ticketRoutes(fastify, _opts) {
57
+ // -----------------------------------------------------------------------
58
+ // Ticket scope management (admin only)
59
+ // -----------------------------------------------------------------------
60
+
61
+ // POST /api/tickets/scopes — Register ticket scopes
62
+ fastify.post(
63
+ '/tickets/scopes',
64
+ { preHandler: fastify.requireRole(['admin']) },
65
+ async (request, reply) => {
66
+ const body = RegisterScopeSchema.parse(request.body);
67
+ try {
68
+ const result = await registerScope(body, request.log);
69
+ return reply.code(201).send(result);
70
+ } catch (err) {
71
+ const statusCode = err.statusCode || 500;
72
+ return reply.code(statusCode).send({ error: err.message });
73
+ }
74
+ },
75
+ );
76
+
77
+ // GET /api/tickets/scopes — List registered scopes
78
+ fastify.get(
79
+ '/tickets/scopes',
80
+ { preHandler: fastify.requireRole(['admin']) },
81
+ async (request, reply) => {
82
+ try {
83
+ const result = await listScopes();
84
+ return result;
85
+ } catch (err) {
86
+ const statusCode = err.statusCode || 500;
87
+ return reply.code(statusCode).send({ error: err.message });
88
+ }
89
+ },
90
+ );
91
+
92
+ // DELETE /api/tickets/scopes/:name — Unregister ticket scopes
93
+ fastify.delete(
94
+ '/tickets/scopes/:name',
95
+ { preHandler: fastify.requireRole(['admin']) },
96
+ async (request, reply) => {
97
+ const name = TicketScopeNameSchema.parse(request.params.name);
98
+ try {
99
+ const result = await unregisterScope(name, request.log);
100
+ return result;
101
+ } catch (err) {
102
+ const statusCode = err.statusCode || 500;
103
+ return reply.code(statusCode).send({ error: err.message });
104
+ }
105
+ },
106
+ );
107
+
108
+ // -----------------------------------------------------------------------
109
+ // Instance registration (agent, scoped)
110
+ // -----------------------------------------------------------------------
111
+
112
+ // POST /api/tickets/instances — Register or re-register as scope host
113
+ fastify.post(
114
+ '/tickets/instances',
115
+ { preHandler: fastify.requireRole(['admin', 'agent']) },
116
+ async (request, reply) => {
117
+ const body = RegisterInstanceSchema.parse(request.body);
118
+ try {
119
+ // Verify caller has the base scope capability
120
+ if (request.certRole === 'agent') {
121
+ const caps = request.certCapabilities || [];
122
+ if (!caps.includes(body.scope)) {
123
+ return reply.code(403).send({ error: 'Insufficient capability' });
124
+ }
125
+ }
126
+
127
+ const agentLabel = request.certLabel;
128
+ if (!agentLabel) {
129
+ return reply.code(400).send({ error: 'Agent label required' });
130
+ }
131
+ const result = await registerInstance(body.scope, body.transport, agentLabel, request.log);
132
+ return reply.code(result.isReregistration ? 200 : 201).send({
133
+ ok: result.ok,
134
+ instanceId: result.instanceId,
135
+ instanceScope: result.instanceScope,
136
+ });
137
+ } catch (err) {
138
+ const statusCode = err.statusCode || 500;
139
+ return reply.code(statusCode).send({ error: err.message });
140
+ }
141
+ },
142
+ );
143
+
144
+ // DELETE /api/tickets/instances/:instanceId — Deregister instance
145
+ fastify.delete(
146
+ '/tickets/instances/:instanceId',
147
+ { preHandler: fastify.requireRole(['admin', 'agent']) },
148
+ async (request, reply) => {
149
+ const instanceId = HexIdSchema.parse(request.params.instanceId);
150
+ try {
151
+ const result = await deregisterInstance(
152
+ instanceId,
153
+ request.certLabel,
154
+ request.certRole,
155
+ request.log,
156
+ );
157
+ return result;
158
+ } catch (err) {
159
+ const statusCode = err.statusCode || 500;
160
+ return reply.code(statusCode).send({ error: err.message });
161
+ }
162
+ },
163
+ );
164
+
165
+ // POST /api/tickets/instances/:instanceId/heartbeat — Instance liveness
166
+ fastify.post(
167
+ '/tickets/instances/:instanceId/heartbeat',
168
+ { preHandler: fastify.requireRole(['admin', 'agent']) },
169
+ async (request, reply) => {
170
+ const instanceId = HexIdSchema.parse(request.params.instanceId);
171
+ try {
172
+ const agentLabel = request.certLabel;
173
+ if (!agentLabel) {
174
+ return reply.code(400).send({ error: 'Agent label required' });
175
+ }
176
+ // Defense-in-depth: reject agents with no capabilities before acquiring
177
+ // the lock. The library's instanceHeartbeat verifies the specific scope.
178
+ if (request.certRole === 'agent') {
179
+ const caps = request.certCapabilities || [];
180
+ if (caps.length === 0) {
181
+ return reply.code(404).send({ error: 'Not found' });
182
+ }
183
+ }
184
+ const result = await instanceHeartbeat(instanceId, agentLabel);
185
+ return result;
186
+ } catch (err) {
187
+ const statusCode = err.statusCode || 500;
188
+ return reply.code(statusCode).send({ error: err.message });
189
+ }
190
+ },
191
+ );
192
+
193
+ // -----------------------------------------------------------------------
194
+ // Instance assignment (admin only)
195
+ // -----------------------------------------------------------------------
196
+
197
+ // POST /api/tickets/assignments — Assign agent to instance
198
+ fastify.post(
199
+ '/tickets/assignments',
200
+ { preHandler: fastify.requireRole(['admin']) },
201
+ async (request, reply) => {
202
+ const body = AssignmentSchema.parse(request.body);
203
+ try {
204
+ const { isExisting, ...response } = await createAssignment(
205
+ body.agentLabel,
206
+ body.instanceScope,
207
+ request.log,
208
+ );
209
+ return reply.code(isExisting ? 200 : 201).send(response);
210
+ } catch (err) {
211
+ const statusCode = err.statusCode || 500;
212
+ return reply.code(statusCode).send({ error: err.message });
213
+ }
214
+ },
215
+ );
216
+
217
+ // DELETE /api/tickets/assignments/:agentLabel/:instanceScope — Remove assignment
218
+ fastify.delete(
219
+ '/tickets/assignments/:agentLabel/:instanceScope',
220
+ { preHandler: fastify.requireRole(['admin']) },
221
+ async (request, reply) => {
222
+ const agentLabel = AgentLabelSchema.parse(request.params.agentLabel);
223
+ const instanceScope = InstanceScopeSchema.parse(request.params.instanceScope);
224
+ try {
225
+ const result = await removeAssignment(agentLabel, instanceScope, request.log);
226
+ return result;
227
+ } catch (err) {
228
+ const statusCode = err.statusCode || 500;
229
+ return reply.code(statusCode).send({ error: err.message });
230
+ }
231
+ },
232
+ );
233
+
234
+ // GET /api/tickets/assignments — List all assignments
235
+ fastify.get(
236
+ '/tickets/assignments',
237
+ { preHandler: fastify.requireRole(['admin']) },
238
+ async (request, reply) => {
239
+ const filters = {};
240
+ if (request.query.agentLabel)
241
+ filters.agentLabel = AgentLabelSchema.parse(request.query.agentLabel);
242
+ if (request.query.instanceScope)
243
+ filters.instanceScope = InstanceScopeSchema.parse(request.query.instanceScope);
244
+ try {
245
+ const result = await listAssignments(filters);
246
+ return result;
247
+ } catch (err) {
248
+ const statusCode = err.statusCode || 500;
249
+ return reply.code(statusCode).send({ error: err.message });
250
+ }
251
+ },
252
+ );
253
+
254
+ // -----------------------------------------------------------------------
255
+ // Ticket operations (agent, scoped)
256
+ // -----------------------------------------------------------------------
257
+
258
+ // POST /api/tickets — Request a ticket
259
+ fastify.post(
260
+ '/tickets',
261
+ { preHandler: fastify.requireRole(['admin', 'agent']) },
262
+ async (request, reply) => {
263
+ const body = RequestTicketSchema.parse(request.body);
264
+ try {
265
+ // Verify caller has the base scope capability
266
+ if (request.certRole === 'agent') {
267
+ const caps = request.certCapabilities || [];
268
+ if (!caps.includes(body.scope)) {
269
+ return reply.code(404).send({ error: 'Not found' });
270
+ }
271
+ }
272
+
273
+ const sourceLabel = request.certLabel;
274
+ if (!sourceLabel) {
275
+ return reply.code(400).send({ error: 'Agent label required' });
276
+ }
277
+ const result = await requestTicket(
278
+ body.scope,
279
+ body.instanceId,
280
+ body.target,
281
+ sourceLabel,
282
+ request.log,
283
+ );
284
+ return reply.code(201).send(result);
285
+ } catch (err) {
286
+ const statusCode = err.statusCode || 500;
287
+ return reply.code(statusCode).send({ error: err.message });
288
+ }
289
+ },
290
+ );
291
+
292
+ // GET /api/tickets/inbox — Retrieve pending tickets
293
+ fastify.get(
294
+ '/tickets/inbox',
295
+ { preHandler: fastify.requireRole(['admin', 'agent']) },
296
+ async (request, reply) => {
297
+ try {
298
+ const agentLabel = request.certLabel;
299
+ if (!agentLabel) {
300
+ return reply.code(400).send({ error: 'Agent label required' });
301
+ }
302
+ const result = await getTicketInbox(agentLabel);
303
+ return result;
304
+ } catch (err) {
305
+ const statusCode = err.statusCode || 500;
306
+ return reply.code(statusCode).send({ error: err.message });
307
+ }
308
+ },
309
+ );
310
+
311
+ // POST /api/tickets/validate — Validate a ticket
312
+ fastify.post(
313
+ '/tickets/validate',
314
+ { preHandler: fastify.requireRole(['admin', 'agent']) },
315
+ async (request, reply) => {
316
+ const body = ValidateTicketSchema.parse(request.body);
317
+ try {
318
+ const callerLabel = request.certLabel;
319
+ if (!callerLabel) {
320
+ return reply.code(401).send({ error: 'Invalid ticket' });
321
+ }
322
+ const result = await validateTicket(body.ticketId, callerLabel, request.log);
323
+ return result;
324
+ } catch (err) {
325
+ const statusCode = err.statusCode || 500;
326
+ return reply.code(statusCode).send({ error: err.message });
327
+ }
328
+ },
329
+ );
330
+
331
+ // GET /api/tickets — List active tickets (admin)
332
+ fastify.get(
333
+ '/tickets',
334
+ { preHandler: fastify.requireRole(['admin']) },
335
+ async (request, reply) => {
336
+ try {
337
+ const result = await listTickets();
338
+ return result;
339
+ } catch (err) {
340
+ const statusCode = err.statusCode || 500;
341
+ return reply.code(statusCode).send({ error: err.message });
342
+ }
343
+ },
344
+ );
345
+
346
+ // DELETE /api/tickets/:ticketId — Revoke a ticket (admin)
347
+ fastify.delete(
348
+ '/tickets/:ticketId',
349
+ { preHandler: fastify.requireRole(['admin']) },
350
+ async (request, reply) => {
351
+ const ticketId = HexIdSchema.parse(request.params.ticketId);
352
+ try {
353
+ const result = await revokeTicket(ticketId, request.log);
354
+ return result;
355
+ } catch (err) {
356
+ const statusCode = err.statusCode || 500;
357
+ return reply.code(statusCode).send({ error: err.message });
358
+ }
359
+ },
360
+ );
361
+
362
+ // -----------------------------------------------------------------------
363
+ // Session management (agent + admin)
364
+ // -----------------------------------------------------------------------
365
+
366
+ // POST /api/tickets/sessions — Report session creation
367
+ fastify.post(
368
+ '/tickets/sessions',
369
+ { preHandler: fastify.requireRole(['admin', 'agent']) },
370
+ async (request, reply) => {
371
+ const body = CreateSessionSchema.parse(request.body);
372
+ try {
373
+ const callerLabel = request.certLabel;
374
+ if (!callerLabel) {
375
+ return reply.code(400).send({ error: 'Agent label required' });
376
+ }
377
+ const result = await createSession(body.ticketId, callerLabel, request.log);
378
+ return reply.code(201).send(result);
379
+ } catch (err) {
380
+ const statusCode = err.statusCode || 500;
381
+ return reply.code(statusCode).send({ error: err.message });
382
+ }
383
+ },
384
+ );
385
+
386
+ // POST /api/tickets/sessions/:sessionId/heartbeat — Session heartbeat
387
+ fastify.post(
388
+ '/tickets/sessions/:sessionId/heartbeat',
389
+ { preHandler: fastify.requireRole(['admin', 'agent']) },
390
+ async (request, reply) => {
391
+ const sessionId = SessionIdSchema.parse(request.params.sessionId);
392
+ try {
393
+ const callerLabel = request.certLabel;
394
+ if (!callerLabel) {
395
+ return reply.code(400).send({ error: 'Agent label required' });
396
+ }
397
+ const result = await sessionHeartbeat(sessionId, callerLabel);
398
+ return result;
399
+ } catch (err) {
400
+ const statusCode = err.statusCode || 500;
401
+ return reply.code(statusCode).send({ error: err.message });
402
+ }
403
+ },
404
+ );
405
+
406
+ // PATCH /api/tickets/sessions/:sessionId — Update session status
407
+ fastify.patch(
408
+ '/tickets/sessions/:sessionId',
409
+ { preHandler: fastify.requireRole(['admin', 'agent']) },
410
+ async (request, reply) => {
411
+ const sessionId = SessionIdSchema.parse(request.params.sessionId);
412
+ const body = UpdateSessionSchema.parse(request.body);
413
+ try {
414
+ const callerLabel = request.certLabel;
415
+ if (!callerLabel) {
416
+ return reply.code(400).send({ error: 'Agent label required' });
417
+ }
418
+ const result = await updateSession(sessionId, body.status, callerLabel);
419
+ return result;
420
+ } catch (err) {
421
+ const statusCode = err.statusCode || 500;
422
+ return reply.code(statusCode).send({ error: err.message });
423
+ }
424
+ },
425
+ );
426
+
427
+ // DELETE /api/tickets/sessions/:sessionId — Admin kill session
428
+ fastify.delete(
429
+ '/tickets/sessions/:sessionId',
430
+ { preHandler: fastify.requireRole(['admin']) },
431
+ async (request, reply) => {
432
+ const sessionId = SessionIdSchema.parse(request.params.sessionId);
433
+ try {
434
+ const result = await killSession(sessionId, request.log);
435
+ return result;
436
+ } catch (err) {
437
+ const statusCode = err.statusCode || 500;
438
+ return reply.code(statusCode).send({ error: err.message });
439
+ }
440
+ },
441
+ );
442
+
443
+ // GET /api/tickets/sessions — List active sessions (admin)
444
+ fastify.get(
445
+ '/tickets/sessions',
446
+ { preHandler: fastify.requireRole(['admin']) },
447
+ async (request, reply) => {
448
+ try {
449
+ const result = await listSessions();
450
+ return result;
451
+ } catch (err) {
452
+ const statusCode = err.statusCode || 500;
453
+ return reply.code(statusCode).send({ error: err.message });
454
+ }
455
+ },
456
+ );
457
+ }