@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,160 @@
1
+ /**
2
+ * Proxy routes for Gatekeeper API.
3
+ *
4
+ * These routes forward requests from lamaste-serverd's mTLS-protected
5
+ * /api/gatekeeper/* path to the Gatekeeper service on 127.0.0.1:9294.
6
+ * This allows the desktop app (and web panel) to reach Gatekeeper on
7
+ * remote servers through the existing mTLS tunnel — no additional port
8
+ * forwarding needed.
9
+ *
10
+ * Admin-only (lamaste-serverd's mTLS + roleGuard protect these routes).
11
+ */
12
+
13
+ import { gatekeeperRequest } from '../../lib/gatekeeper-client.js';
14
+
15
+ export default async function gatekeeperProxyRoutes(fastify, _opts) {
16
+ // All gatekeeper management routes are admin-only
17
+ fastify.addHook('preHandler', fastify.requireRole(['admin']));
18
+
19
+ /**
20
+ * Forward to gatekeeper via the shared client (which reads the secret
21
+ * from either `gatekeeper-secret.json` or the legacy plain-text file).
22
+ * Normalises the response to the proxy's historical shape.
23
+ */
24
+ const proxy = async (method, gatekeeperPath, body) => {
25
+ const result = await gatekeeperRequest(method, gatekeeperPath, body);
26
+ if (result.ok) {
27
+ return { statusCode: result.statusCode, data: result.data };
28
+ }
29
+ return {
30
+ statusCode: result.statusCode,
31
+ data: { error: result.error },
32
+ };
33
+ };
34
+
35
+ // --- Groups ---
36
+
37
+ fastify.get('/gatekeeper/groups', async (_request, reply) => {
38
+ const { statusCode, data } = await proxy('GET', '/api/groups');
39
+ return reply.code(statusCode).send(data);
40
+ });
41
+
42
+ fastify.post('/gatekeeper/groups', async (request, reply) => {
43
+ const { statusCode, data } = await proxy('POST', '/api/groups', request.body);
44
+ return reply.code(statusCode).send(data);
45
+ });
46
+
47
+ fastify.get('/gatekeeper/groups/:name', async (request, reply) => {
48
+ const { name } = request.params;
49
+ const { statusCode, data } = await proxy('GET', `/api/groups/${encodeURIComponent(name)}`);
50
+ return reply.code(statusCode).send(data);
51
+ });
52
+
53
+ fastify.patch('/gatekeeper/groups/:name', async (request, reply) => {
54
+ const { name } = request.params;
55
+ const { statusCode, data } = await proxy(
56
+ 'PATCH',
57
+ `/api/groups/${encodeURIComponent(name)}`,
58
+ request.body,
59
+ );
60
+ return reply.code(statusCode).send(data);
61
+ });
62
+
63
+ fastify.delete('/gatekeeper/groups/:name', async (request, reply) => {
64
+ const { name } = request.params;
65
+ const { statusCode, data } = await proxy('DELETE', `/api/groups/${encodeURIComponent(name)}`);
66
+ return reply.code(statusCode).send(data);
67
+ });
68
+
69
+ fastify.post('/gatekeeper/groups/:name/members', async (request, reply) => {
70
+ const { name } = request.params;
71
+ const { statusCode, data } = await proxy(
72
+ 'POST',
73
+ `/api/groups/${encodeURIComponent(name)}/members`,
74
+ request.body,
75
+ );
76
+ return reply.code(statusCode).send(data);
77
+ });
78
+
79
+ fastify.delete('/gatekeeper/groups/:name/members/:username', async (request, reply) => {
80
+ const { name, username } = request.params;
81
+ const { statusCode, data } = await proxy(
82
+ 'DELETE',
83
+ `/api/groups/${encodeURIComponent(name)}/members/${encodeURIComponent(username)}`,
84
+ );
85
+ return reply.code(statusCode).send(data);
86
+ });
87
+
88
+ // --- Grants ---
89
+
90
+ fastify.get('/gatekeeper/grants', async (request, reply) => {
91
+ const qs = new URLSearchParams(request.query).toString();
92
+ const grantPath = qs ? `/api/grants?${qs}` : '/api/grants';
93
+ const { statusCode, data } = await proxy('GET', grantPath);
94
+ return reply.code(statusCode).send(data);
95
+ });
96
+
97
+ fastify.post('/gatekeeper/grants', async (request, reply) => {
98
+ const { statusCode, data } = await proxy('POST', '/api/grants', request.body);
99
+ return reply.code(statusCode).send(data);
100
+ });
101
+
102
+ fastify.get('/gatekeeper/grants/:grantId', async (request, reply) => {
103
+ const { grantId } = request.params;
104
+ const { statusCode, data } = await proxy('GET', `/api/grants/${encodeURIComponent(grantId)}`);
105
+ return reply.code(statusCode).send(data);
106
+ });
107
+
108
+ fastify.delete('/gatekeeper/grants/:grantId', async (request, reply) => {
109
+ const { grantId } = request.params;
110
+ const { statusCode, data } = await proxy(
111
+ 'DELETE',
112
+ `/api/grants/${encodeURIComponent(grantId)}`,
113
+ );
114
+ return reply.code(statusCode).send(data);
115
+ });
116
+
117
+ // Bulk-revoke every grant for a principal. Forwarded to gatekeeper as
118
+ // DELETE /api/grants?principalType=...&principalId=...
119
+ fastify.delete('/gatekeeper/grants', async (request, reply) => {
120
+ const qs = new URLSearchParams(request.query).toString();
121
+ const grantPath = qs ? `/api/grants?${qs}` : '/api/grants';
122
+ const { statusCode, data } = await proxy('DELETE', grantPath);
123
+ return reply.code(statusCode).send(data);
124
+ });
125
+
126
+ // --- Diagnostics / Settings / Cache ---
127
+
128
+ fastify.get('/gatekeeper/access/check', async (request, reply) => {
129
+ const qs = new URLSearchParams(request.query).toString();
130
+ const { statusCode, data } = await proxy('GET', `/api/access/check?${qs}`);
131
+ return reply.code(statusCode).send(data);
132
+ });
133
+
134
+ fastify.post('/gatekeeper/cache/bust', async (_request, reply) => {
135
+ const { statusCode, data } = await proxy('POST', '/api/cache/bust');
136
+ return reply.code(statusCode).send(data);
137
+ });
138
+
139
+ fastify.get('/gatekeeper/settings', async (_request, reply) => {
140
+ const { statusCode, data } = await proxy('GET', '/api/settings');
141
+ return reply.code(statusCode).send(data);
142
+ });
143
+
144
+ fastify.patch('/gatekeeper/settings', async (request, reply) => {
145
+ const { statusCode, data } = await proxy('PATCH', '/api/settings', request.body);
146
+ return reply.code(statusCode).send(data);
147
+ });
148
+
149
+ fastify.get('/gatekeeper/access-log', async (request, reply) => {
150
+ const qs = new URLSearchParams(request.query).toString();
151
+ const logPath = qs ? `/api/access-log?${qs}` : '/api/access-log';
152
+ const { statusCode, data } = await proxy('GET', logPath);
153
+ return reply.code(statusCode).send(data);
154
+ });
155
+
156
+ fastify.delete('/gatekeeper/access-log', async (_request, reply) => {
157
+ const { statusCode, data } = await proxy('DELETE', '/api/access-log');
158
+ return reply.code(statusCode).send(data);
159
+ });
160
+ }
@@ -0,0 +1,107 @@
1
+ import { z } from 'zod';
2
+ import { readUsers } from '../../lib/authelia.js';
3
+
4
+ const UsernameParamSchema = z.object({
5
+ username: z
6
+ .string()
7
+ .min(1)
8
+ .max(100)
9
+ .regex(/^[a-zA-Z0-9_.-]+$/),
10
+ });
11
+
12
+ /**
13
+ * Extract a single string value from a header that may be string, string[], or undefined.
14
+ *
15
+ * @param {string | string[] | undefined} value
16
+ * @returns {string | undefined}
17
+ */
18
+ function headerValue(value) {
19
+ if (Array.isArray(value)) return value[0];
20
+ return value;
21
+ }
22
+
23
+ export default async function identityRoutes(fastify, _opts) {
24
+ // GET /api/identity/self — Returns caller's identity from Authelia headers.
25
+ // Only meaningful behind an Authelia-protected vhost. On the mTLS panel vhost,
26
+ // nginx strips Remote-* headers so this will return 400.
27
+ fastify.get(
28
+ '/identity/self',
29
+ { preHandler: fastify.requireRole(['admin']) },
30
+ async (request, reply) => {
31
+ const user = headerValue(request.headers['remote-user']);
32
+ if (!user) {
33
+ return reply.code(400).send({
34
+ error:
35
+ 'Identity headers not present — this endpoint requires an Authelia-protected vhost',
36
+ });
37
+ }
38
+
39
+ const groupsRaw = headerValue(request.headers['remote-groups']) || '';
40
+ const groups = groupsRaw
41
+ .split(',')
42
+ .map((g) => g.trim())
43
+ .filter(Boolean);
44
+ const name = headerValue(request.headers['remote-name']) || '';
45
+ const email = headerValue(request.headers['remote-email']) || '';
46
+
47
+ return { username: user, displayName: name, email, groups };
48
+ },
49
+ );
50
+
51
+ // GET /api/identity/users — List Authelia users
52
+ fastify.get(
53
+ '/identity/users',
54
+ { preHandler: fastify.requireRole(['admin', 'agent'], { capability: 'identity:query' }) },
55
+ async (request, reply) => {
56
+ try {
57
+ const users = await readUsers();
58
+ return { users };
59
+ } catch (err) {
60
+ request.log.error({ err }, 'Failed to read Authelia users');
61
+ return reply.code(500).send({ error: 'Failed to read users' });
62
+ }
63
+ },
64
+ );
65
+
66
+ // GET /api/identity/users/:username — Single user metadata
67
+ fastify.get(
68
+ '/identity/users/:username',
69
+ { preHandler: fastify.requireRole(['admin', 'agent'], { capability: 'identity:query' }) },
70
+ async (request, reply) => {
71
+ const { username } = UsernameParamSchema.parse(request.params);
72
+ try {
73
+ const users = await readUsers();
74
+ const user = users.find((u) => u.username === username);
75
+ if (!user) {
76
+ return reply.code(404).send({ error: 'User not found' });
77
+ }
78
+ return { user };
79
+ } catch (err) {
80
+ request.log.error({ err }, 'Failed to read Authelia users');
81
+ return reply.code(500).send({ error: 'Failed to read users' });
82
+ }
83
+ },
84
+ );
85
+
86
+ // GET /api/identity/groups — List all groups
87
+ fastify.get(
88
+ '/identity/groups',
89
+ { preHandler: fastify.requireRole(['admin', 'agent'], { capability: 'identity:query' }) },
90
+ async (request, reply) => {
91
+ try {
92
+ const users = await readUsers();
93
+ /** @type {Set<string>} */
94
+ const groupSet = new Set();
95
+ for (const u of users) {
96
+ for (const g of u.groups) {
97
+ groupSet.add(g);
98
+ }
99
+ }
100
+ return { groups: [...groupSet].sort() };
101
+ } catch (err) {
102
+ request.log.error({ err }, 'Failed to read Authelia users');
103
+ return reply.code(500).send({ error: 'Failed to read groups' });
104
+ }
105
+ },
106
+ );
107
+ }
@@ -0,0 +1,166 @@
1
+ import crypto from 'node:crypto';
2
+ import { z } from 'zod';
3
+ import { readInvitations, writeInvitations } from '../../lib/state.js';
4
+ import { readUsersRaw } from '../../lib/authelia.js';
5
+ import { getConfig } from '../../lib/config.js';
6
+
7
+ const IdParamSchema = z.object({ id: z.string().uuid() });
8
+
9
+ const CreateInvitationSchema = z.object({
10
+ username: z
11
+ .string()
12
+ .min(2)
13
+ .max(32)
14
+ .regex(
15
+ // Same rule as the users API. A leading "-" would read as a flag to
16
+ // Authelia's CLI when a TOTP secret is stored for the user.
17
+ /^[a-z0-9][a-z0-9_-]*$/,
18
+ 'Username must start with a lowercase letter or digit and may contain underscores or hyphens',
19
+ ),
20
+ email: z.string().email(),
21
+ groups: z.array(z.string()).optional().default([]),
22
+ expiresInDays: z.number().int().min(1).max(30).optional().default(7),
23
+ });
24
+
25
+ export default async function invitationRoutes(fastify, _opts) {
26
+ // GET /api/invitations — list all invitations
27
+ fastify.get(
28
+ '/invitations',
29
+ {
30
+ preHandler: fastify.requireRole(['admin']),
31
+ },
32
+ async (request, reply) => {
33
+ try {
34
+ const invitations = await readInvitations();
35
+ const now = new Date();
36
+ const enriched = invitations.map((inv) => ({
37
+ ...inv,
38
+ token: undefined,
39
+ status: inv.used ? 'accepted' : new Date(inv.expiresAt) < now ? 'expired' : 'pending',
40
+ }));
41
+ return { invitations: enriched };
42
+ } catch (err) {
43
+ request.log.error(err, 'Failed to read invitations');
44
+ return reply.code(500).send({ error: 'Failed to read invitations' });
45
+ }
46
+ },
47
+ );
48
+
49
+ // POST /api/invitations — create a new invitation
50
+ fastify.post(
51
+ '/invitations',
52
+ {
53
+ preHandler: fastify.requireRole(['admin']),
54
+ },
55
+ async (request, reply) => {
56
+ const body = CreateInvitationSchema.parse(request.body);
57
+
58
+ // Check username doesn't already exist in Authelia
59
+ let usersData;
60
+ try {
61
+ usersData = await readUsersRaw();
62
+ } catch {
63
+ usersData = { users: {} };
64
+ }
65
+
66
+ if (usersData.users[body.username]) {
67
+ return reply.code(409).send({ error: 'Username already exists as an Authelia user' });
68
+ }
69
+
70
+ // Check username doesn't already have a pending invitation
71
+ let invitations;
72
+ try {
73
+ invitations = await readInvitations();
74
+ } catch (err) {
75
+ request.log.error(err, 'Failed to read invitations');
76
+ return reply.code(500).send({ error: 'Failed to read invitations' });
77
+ }
78
+
79
+ const now = new Date();
80
+ const existingPending = invitations.find(
81
+ (inv) => inv.username === body.username && !inv.used && new Date(inv.expiresAt) > now,
82
+ );
83
+ if (existingPending) {
84
+ return reply
85
+ .code(409)
86
+ .send({ error: 'A pending invitation already exists for this username' });
87
+ }
88
+
89
+ const config = getConfig();
90
+ const token = crypto.randomBytes(32).toString('hex');
91
+ const expiresAt = new Date(now.getTime() + body.expiresInDays * 24 * 60 * 60 * 1000);
92
+
93
+ const invitation = {
94
+ id: crypto.randomUUID(),
95
+ token,
96
+ username: body.username,
97
+ email: body.email,
98
+ groups: body.groups,
99
+ createdAt: now.toISOString(),
100
+ expiresAt: expiresAt.toISOString(),
101
+ used: false,
102
+ acceptedAt: null,
103
+ };
104
+
105
+ invitations.push(invitation);
106
+
107
+ try {
108
+ await writeInvitations(invitations);
109
+ } catch (err) {
110
+ request.log.error(err, 'Failed to write invitations');
111
+ return reply.code(500).send({ error: 'Failed to save invitation' });
112
+ }
113
+
114
+ const inviteUrl = config.domain ? `https://auth.${config.domain}/invite/${token}` : null;
115
+
116
+ return reply.code(201).send({
117
+ ok: true,
118
+ invitation: {
119
+ id: invitation.id,
120
+ username: invitation.username,
121
+ email: invitation.email,
122
+ groups: invitation.groups,
123
+ createdAt: invitation.createdAt,
124
+ expiresAt: invitation.expiresAt,
125
+ },
126
+ inviteUrl,
127
+ token,
128
+ });
129
+ },
130
+ );
131
+
132
+ // DELETE /api/invitations/:id — revoke an invitation
133
+ fastify.delete(
134
+ '/invitations/:id',
135
+ {
136
+ preHandler: fastify.requireRole(['admin']),
137
+ },
138
+ async (request, reply) => {
139
+ const { id } = IdParamSchema.parse(request.params);
140
+
141
+ let invitations;
142
+ try {
143
+ invitations = await readInvitations();
144
+ } catch (err) {
145
+ request.log.error(err, 'Failed to read invitations');
146
+ return reply.code(500).send({ error: 'Failed to read invitations' });
147
+ }
148
+
149
+ const index = invitations.findIndex((inv) => inv.id === id);
150
+ if (index === -1) {
151
+ return reply.code(404).send({ error: 'Invitation not found' });
152
+ }
153
+
154
+ invitations.splice(index, 1);
155
+
156
+ try {
157
+ await writeInvitations(invitations);
158
+ } catch (err) {
159
+ request.log.error(err, 'Failed to write invitations');
160
+ return reply.code(500).send({ error: 'Failed to revoke invitation' });
161
+ }
162
+
163
+ return { ok: true };
164
+ },
165
+ );
166
+ }
@@ -0,0 +1,218 @@
1
+ import { execa } from 'execa';
2
+ import { ALLOWED_SERVICES } from '../../lib/services.js';
3
+
4
+ // Track active journalctl processes for cleanup on server shutdown
5
+ const activeProcesses = new Set();
6
+
7
+ // Backpressure: max bytes buffered before we pause the child stdout stream
8
+ const MAX_SEND_BUFFER_BYTES = 64 * 1024; // 64 KB
9
+
10
+ export default async function logsRoutes(fastify, _opts) {
11
+ fastify.get(
12
+ '/services/:name/logs',
13
+ {
14
+ websocket: true,
15
+ preHandler: fastify.requireRole(['admin']),
16
+ // The rate-limit only applies to the HTTP upgrade handshake — once
17
+ // the WebSocket is established it streams freely. Empty config
18
+ // inherits the globally-registered limiter (100 connection attempts
19
+ // per minute) and satisfies CodeQL.
20
+ config: { rateLimit: {} },
21
+ },
22
+ (socket, request) => {
23
+ const { name } = request.params;
24
+
25
+ // Validate service name
26
+ if (!ALLOWED_SERVICES.includes(name)) {
27
+ socket.close(1008, 'Unknown service');
28
+ return;
29
+ }
30
+
31
+ let child = null;
32
+ let lineBuffer = '';
33
+ let sendBuffer = [];
34
+ let sendBufferBytes = 0;
35
+ let stdoutPaused = false;
36
+
37
+ try {
38
+ child = execa('journalctl', ['-u', name, '-f', '-n', '100', '--output=short-iso'], {
39
+ reject: false,
40
+ buffer: false,
41
+ stdout: 'pipe',
42
+ stderr: 'pipe',
43
+ });
44
+ } catch (err) {
45
+ request.log.error({ err }, 'Failed to spawn journalctl');
46
+ socket.send(
47
+ JSON.stringify({
48
+ timestamp: new Date().toISOString(),
49
+ message: `[Error: Failed to start log stream for ${name}]`,
50
+ }),
51
+ );
52
+ socket.close(1011, 'Failed to spawn journalctl');
53
+ return;
54
+ }
55
+
56
+ activeProcesses.add(child);
57
+
58
+ /**
59
+ * Attempt to flush buffered messages to the WebSocket.
60
+ * Drops oldest messages if buffer exceeds the size limit.
61
+ * Pauses/resumes child stdout based on buffer pressure.
62
+ */
63
+ function flushSendBuffer() {
64
+ while (sendBuffer.length > 0) {
65
+ if (socket.readyState !== 1 /* OPEN */) {
66
+ // Socket not open — discard everything
67
+ sendBuffer.length = 0;
68
+ sendBufferBytes = 0;
69
+ return;
70
+ }
71
+
72
+ const msg = sendBuffer[0];
73
+ try {
74
+ socket.send(msg);
75
+ sendBufferBytes -= msg.length;
76
+ sendBuffer.shift();
77
+ } catch {
78
+ // Socket may have closed mid-send
79
+ sendBuffer.length = 0;
80
+ sendBufferBytes = 0;
81
+ return;
82
+ }
83
+ }
84
+
85
+ // Buffer drained — resume child stdout if it was paused
86
+ if (stdoutPaused && child?.stdout) {
87
+ stdoutPaused = false;
88
+ child.stdout.resume();
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Enqueue a serialized message for sending.
94
+ * Enforces the buffer size limit by dropping oldest messages.
95
+ */
96
+ function enqueueMessage(serialized) {
97
+ sendBuffer.push(serialized);
98
+ sendBufferBytes += serialized.length;
99
+
100
+ // Drop oldest messages while buffer exceeds the limit
101
+ while (sendBufferBytes > MAX_SEND_BUFFER_BYTES && sendBuffer.length > 1) {
102
+ const dropped = sendBuffer.shift();
103
+ sendBufferBytes -= dropped.length;
104
+ }
105
+
106
+ // Pause child stdout when buffer is full to apply backpressure
107
+ if (sendBufferBytes > MAX_SEND_BUFFER_BYTES && !stdoutPaused && child?.stdout) {
108
+ stdoutPaused = true;
109
+ child.stdout.pause();
110
+ }
111
+ }
112
+
113
+ // Handle WebSocket drain — buffer has been flushed by the network layer
114
+ socket.on('drain', () => {
115
+ flushSendBuffer();
116
+ });
117
+
118
+ // Parse and send lines from journalctl stdout
119
+ if (child.stdout) {
120
+ child.stdout.on('data', (chunk) => {
121
+ lineBuffer += chunk.toString();
122
+ const lines = lineBuffer.split('\n');
123
+ // Keep the last incomplete line in the buffer
124
+ lineBuffer = lines.pop() || '';
125
+
126
+ for (const line of lines) {
127
+ if (!line.trim()) continue;
128
+ const parsed = parseLine(line);
129
+ const serialized = JSON.stringify(parsed);
130
+
131
+ if (socket.readyState === 1 /* OPEN */ && sendBuffer.length === 0) {
132
+ // Fast path: send directly when buffer is empty and socket is open
133
+ try {
134
+ socket.send(serialized);
135
+ } catch {
136
+ // Socket closed — will be cleaned up by close event
137
+ }
138
+ } else {
139
+ enqueueMessage(serialized);
140
+ }
141
+ }
142
+
143
+ // Try to flush any queued messages
144
+ if (sendBuffer.length > 0) {
145
+ flushSendBuffer();
146
+ }
147
+ });
148
+ }
149
+
150
+ // Handle journalctl exit
151
+ child.then(() => {
152
+ activeProcesses.delete(child);
153
+ try {
154
+ if (socket.readyState === 1 /* OPEN */) {
155
+ socket.send(
156
+ JSON.stringify({
157
+ timestamp: new Date().toISOString(),
158
+ message: '[Log stream ended]',
159
+ }),
160
+ );
161
+ socket.close(1000, 'Log stream ended');
162
+ }
163
+ } catch {
164
+ // Socket may already be closed
165
+ }
166
+ });
167
+
168
+ // Clean up on WebSocket close
169
+ socket.on('close', () => {
170
+ sendBuffer.length = 0;
171
+ sendBufferBytes = 0;
172
+ if (child) {
173
+ activeProcesses.delete(child);
174
+ child.kill('SIGTERM');
175
+ }
176
+ });
177
+
178
+ // Clean up on WebSocket error
179
+ socket.on('error', (err) => {
180
+ request.log.error({ err }, 'WebSocket error for log stream');
181
+ sendBuffer.length = 0;
182
+ sendBufferBytes = 0;
183
+ if (child) {
184
+ activeProcesses.delete(child);
185
+ child.kill('SIGTERM');
186
+ }
187
+ });
188
+ },
189
+ );
190
+
191
+ // Clean up all journalctl processes on server shutdown
192
+ fastify.addHook('onClose', async () => {
193
+ for (const proc of activeProcesses) {
194
+ proc.kill('SIGTERM');
195
+ }
196
+ activeProcesses.clear();
197
+ });
198
+ }
199
+
200
+ /**
201
+ * Parse a journalctl --output=short-iso line into { timestamp, message }.
202
+ * Format: 2024-03-15T10:30:45+0000 hostname servicename[pid]: log message
203
+ */
204
+ function parseLine(line) {
205
+ // ISO timestamp is at the beginning, match until first space after the ISO pattern
206
+ const isoMatch = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[^\s]*)\s+(.*)$/);
207
+ if (isoMatch) {
208
+ return {
209
+ timestamp: isoMatch[1],
210
+ message: isoMatch[2],
211
+ };
212
+ }
213
+
214
+ return {
215
+ timestamp: '',
216
+ message: line,
217
+ };
218
+ }