@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,389 @@
1
+ import { z } from 'zod';
2
+ import { getConfig } from '../lib/config.js';
3
+ import { ensureSessionSecret } from '../lib/session.js';
4
+ import { createUserSession } from '../lib/user-access-session.js';
5
+ import {
6
+ createGrant,
7
+ listGrants,
8
+ revokeGrant,
9
+ listGrantsForUser,
10
+ consumeGrant,
11
+ createOTP,
12
+ validateAndConsumeOTP,
13
+ } from '../lib/user-access.js';
14
+ import { createEnrollmentToken } from '../lib/enrollment.js';
15
+ import { readPlugins } from '../lib/plugins.js';
16
+ import { readTunnels } from '../lib/state.js';
17
+
18
+ // --- Zod schemas ---
19
+
20
+ const CreateGrantSchema = z.object({
21
+ username: z
22
+ .string()
23
+ .min(2)
24
+ .max(32)
25
+ .regex(/^[a-z0-9][a-z0-9_-]*$/, 'Invalid username format'),
26
+ pluginName: z
27
+ .string()
28
+ .min(1)
29
+ .max(200)
30
+ .regex(/^@lamalibre\//, 'Plugin must be @lamalibre/ scoped'),
31
+ target: z
32
+ .string()
33
+ .regex(/^(local|agent:[a-z0-9][a-z0-9-]*)$/, 'Target must be "local" or "agent:<label>"')
34
+ .optional()
35
+ .default('local'),
36
+ });
37
+
38
+ const GrantIdParamSchema = z.object({
39
+ grantId: z.string().uuid('Invalid grant ID format'),
40
+ });
41
+
42
+ const ExchangeBodySchema = z.object({
43
+ token: z.string().regex(/^[a-f0-9]{64}$/, 'Invalid token format'),
44
+ // PKCE verifier: 32–64 base64url chars (256–384 bits of entropy). The
45
+ // panel hashes this and timing-safe-compares against the stored S256
46
+ // challenge submitted at /authorize.
47
+ verifier: z.string().regex(/^[A-Za-z0-9_-]{32,64}$/, 'Invalid verifier format'),
48
+ });
49
+
50
+ // PKCE handshake parameters submitted by the desktop at /authorize.
51
+ const AuthorizeQuerySchema = z.object({
52
+ // base64url SHA-256 → 43 chars, no padding (PKCE S256, RFC 7636).
53
+ challenge: z.string().regex(/^[A-Za-z0-9_-]{43}$/, 'Invalid PKCE challenge'),
54
+ // 16 random bytes hex-encoded → 32 chars.
55
+ nonce: z.string().regex(/^[a-f0-9]{32}$/, 'Invalid nonce'),
56
+ });
57
+
58
+ const EnrollBodySchema = z.object({
59
+ grantId: z.string().uuid('Invalid grant ID format'),
60
+ });
61
+
62
+ // In-memory failed-exchange counter (per-process, resets on restart).
63
+ // Defense-in-depth: log a warning every N failures so an operator notices a
64
+ // scripted attacker exhausting the OTP keyspace. Cap is intentionally tiny —
65
+ // the OTP itself is 256 bits of entropy + bound to a verifier.
66
+ const FAILURE_LOG_INTERVAL = 10;
67
+ let exchangeFailureCount = 0;
68
+
69
+ // --- Admin routes (mTLS + roleGuard) ---
70
+
71
+ /**
72
+ * Admin grant management routes.
73
+ * Registered inside the protectedContext (mTLS + roleGuard).
74
+ */
75
+ export async function userAccessAdminRoutes(fastify, _opts) {
76
+ // GET /api/user-access/grants — list all grants
77
+ fastify.get(
78
+ '/user-access/grants',
79
+ {
80
+ preHandler: fastify.requireRole(['admin']),
81
+ },
82
+ async (request, reply) => {
83
+ try {
84
+ const grants = await listGrants();
85
+ return { grants };
86
+ } catch (err) {
87
+ request.log.error(err, 'Failed to list user access grants');
88
+ return reply.code(500).send({ error: 'Failed to list grants' });
89
+ }
90
+ },
91
+ );
92
+
93
+ // POST /api/user-access/grants — create a grant
94
+ fastify.post(
95
+ '/user-access/grants',
96
+ {
97
+ preHandler: fastify.requireRole(['admin']),
98
+ },
99
+ async (request, reply) => {
100
+ let body;
101
+ try {
102
+ body = CreateGrantSchema.parse(request.body);
103
+ } catch (err) {
104
+ return reply.code(400).send({ error: err.errors?.[0]?.message || 'Invalid request body' });
105
+ }
106
+
107
+ try {
108
+ const grant = await createGrant(body.username, body.pluginName, request.log, {
109
+ target: body.target,
110
+ });
111
+
112
+ return { ok: true, grant };
113
+ } catch (err) {
114
+ request.log.error(err, 'Failed to create user access grant');
115
+ const statusCode = err.statusCode || 500;
116
+ return reply.code(statusCode).send({ error: err.message || 'Failed to create grant' });
117
+ }
118
+ },
119
+ );
120
+
121
+ // DELETE /api/user-access/grants/:grantId — revoke a grant
122
+ fastify.delete(
123
+ '/user-access/grants/:grantId',
124
+ {
125
+ preHandler: fastify.requireRole(['admin']),
126
+ },
127
+ async (request, reply) => {
128
+ let params;
129
+ try {
130
+ params = GrantIdParamSchema.parse(request.params);
131
+ } catch (err) {
132
+ return reply.code(400).send({ error: err.errors?.[0]?.message || 'Invalid grant ID' });
133
+ }
134
+
135
+ try {
136
+ await revokeGrant(params.grantId, request.log);
137
+
138
+ return { ok: true };
139
+ } catch (err) {
140
+ const statusCode = err.statusCode || 500;
141
+ return reply.code(statusCode).send({ error: err.message || 'Failed to revoke grant' });
142
+ }
143
+ },
144
+ );
145
+ }
146
+
147
+ // --- Public routes (no mTLS) ---
148
+
149
+ /**
150
+ * Public user-access routes for the OAuth-like auth flow.
151
+ * Registered in the publicContext (no mTLS required).
152
+ */
153
+ export async function userAccessPublicRoutes(fastify, _opts) {
154
+ // GET /authorize — Authelia-protected, generates OTP bound to a PKCE
155
+ // challenge, redirects to deep link with the OTP and nonce in the URL
156
+ // FRAGMENT (#). Fragments are not sent to servers, not stored in most OS
157
+ // URL handler logs, and stripped from third-party referrer chains — they
158
+ // are the right transport for short-lived secrets in a redirect URL.
159
+ fastify.get('/authorize', async (request, reply) => {
160
+ const config = getConfig();
161
+
162
+ // Remote-User header is set by nginx after Authelia forward auth succeeds
163
+ const username = request.headers['remote-user'];
164
+ if (!username) {
165
+ return reply.code(401).send({ error: 'Authentication required' });
166
+ }
167
+
168
+ let query;
169
+ try {
170
+ query = AuthorizeQuerySchema.parse(request.query);
171
+ } catch (err) {
172
+ return reply
173
+ .code(400)
174
+ .send({ error: err.errors?.[0]?.message || 'Invalid authorize parameters' });
175
+ }
176
+
177
+ try {
178
+ const domain = config.domain;
179
+ if (!domain) {
180
+ return reply.code(503).send({ error: 'Server domain not configured' });
181
+ }
182
+
183
+ const { token, nonce } = await createOTP(
184
+ username,
185
+ { challenge: query.challenge, nonce: query.nonce },
186
+ request.log,
187
+ );
188
+
189
+ // Deep-link callback. The OS-registered scheme is `lamalibre://`
190
+ // (ecosystem-level — shared across all Lamalibre products); the
191
+ // `product=lamaste` query param tells the desktop's scheme dispatcher
192
+ // which product the callback is for. Future products (herd, shell,
193
+ // etc.) will reuse the single scheme with their own `product=` value.
194
+ //
195
+ // The OTP token, domain, and nonce stay in the URL fragment (after #)
196
+ // so the auth proxy never sees them. Authelia and any HTTP server in
197
+ // the chain only see path/query; fragments are kept by the user-agent.
198
+ // Putting the token in the query string would leak it to nginx access
199
+ // logs.
200
+ const callbackUrl =
201
+ `lamalibre://callback?product=lamaste` +
202
+ `#token=${encodeURIComponent(token)}` +
203
+ `&domain=${encodeURIComponent(domain)}` +
204
+ `&nonce=${encodeURIComponent(nonce)}`;
205
+ return reply.redirect(callbackUrl, 302);
206
+ } catch (err) {
207
+ const statusCode = err.statusCode || 500;
208
+ const message = statusCode < 500 ? err.message : 'Authorization failed';
209
+ request.log.error(err, 'Failed to create user access OTP');
210
+ return reply.code(statusCode).send({ error: message });
211
+ }
212
+ });
213
+
214
+ // POST /exchange — exchange OTP + PKCE verifier for a user session token.
215
+ // The verifier proves possession of the desktop process that initiated
216
+ // /authorize. A malicious local app that intercepts the deep link has the
217
+ // OTP but NOT the verifier, so this call fails with the same generic error
218
+ // as any other invalid-OTP path (no information leakage).
219
+ fastify.post('/exchange', async (request, reply) => {
220
+ let body;
221
+ try {
222
+ body = ExchangeBodySchema.parse(request.body);
223
+ } catch {
224
+ // Treat shape errors the same as auth failures — no leak about which
225
+ // field was missing or malformed.
226
+ exchangeFailureCount += 1;
227
+ if (exchangeFailureCount % FAILURE_LOG_INTERVAL === 0) {
228
+ request.log.warn(
229
+ { count: exchangeFailureCount },
230
+ 'user-access /exchange repeated failures — possible brute force',
231
+ );
232
+ }
233
+ return reply.code(401).send({ error: 'Invalid or expired token' });
234
+ }
235
+
236
+ try {
237
+ const { username } = await validateAndConsumeOTP(body.token, body.verifier);
238
+ const sessionSecret = await ensureSessionSecret();
239
+ const session = createUserSession(sessionSecret, username);
240
+
241
+ return {
242
+ ok: true,
243
+ sessionToken: session.value,
244
+ username,
245
+ expiresAt: session.expiresAt,
246
+ };
247
+ } catch (err) {
248
+ exchangeFailureCount += 1;
249
+ if (exchangeFailureCount % FAILURE_LOG_INTERVAL === 0) {
250
+ request.log.warn(
251
+ { count: exchangeFailureCount },
252
+ 'user-access /exchange repeated failures — possible brute force',
253
+ );
254
+ }
255
+ // Use generic error message (no information leakage)
256
+ const statusCode = err.statusCode || 401;
257
+ return reply.code(statusCode).send({ error: 'Invalid or expired token' });
258
+ }
259
+ });
260
+ }
261
+
262
+ // --- User-session-protected routes ---
263
+
264
+ /**
265
+ * Routes accessible to authenticated Authelia users via Bearer session token.
266
+ * Registered with user-access-session middleware.
267
+ */
268
+ export async function userAccessProtectedRoutes(fastify, _opts) {
269
+ // GET /plugins — list granted plugins for the authenticated user
270
+ fastify.get('/plugins', async (request, reply) => {
271
+ const username = request.userAccessUsername;
272
+
273
+ try {
274
+ const grants = await listGrantsForUser(username);
275
+
276
+ // Enrich with plugin metadata where available
277
+ let plugins = [];
278
+ try {
279
+ const registry = await readPlugins();
280
+ plugins = registry.plugins || [];
281
+ } catch {
282
+ // Plugin registry may not exist — proceed with grants only
283
+ }
284
+
285
+ const pluginMap = new Map();
286
+ for (const p of plugins) {
287
+ pluginMap.set(p.packageName, p);
288
+ }
289
+
290
+ // Load tunnels for agent-side grant enrichment
291
+ let tunnels = [];
292
+ try {
293
+ tunnels = await readTunnels();
294
+ } catch {
295
+ // Tunnel state may not exist — proceed without tunnel info
296
+ }
297
+
298
+ const enrichedGrants = grants.map((g) => {
299
+ const plugin = pluginMap.get(g.pluginName);
300
+ const pluginMeta = plugin
301
+ ? {
302
+ name: plugin.name,
303
+ displayName: plugin.displayName || plugin.name,
304
+ description: plugin.description,
305
+ version: plugin.version,
306
+ }
307
+ : null;
308
+
309
+ const target = g.target || 'local';
310
+
311
+ if (target.startsWith('agent:')) {
312
+ const agentLabel = target.slice('agent:'.length);
313
+ const tunnel = tunnels.find(
314
+ (t) =>
315
+ t.type === 'plugin' && t.agentLabel === agentLabel && t.pluginName === g.pluginName,
316
+ );
317
+ return {
318
+ ...g,
319
+ target,
320
+ agentLabel,
321
+ tunnelUrl: tunnel ? `https://${tunnel.fqdn}` : null,
322
+ tunnelEnabled: tunnel?.enabled ?? false,
323
+ plugin: pluginMeta,
324
+ };
325
+ }
326
+
327
+ return {
328
+ ...g,
329
+ target,
330
+ plugin: pluginMeta,
331
+ };
332
+ });
333
+
334
+ return { grants: enrichedGrants };
335
+ } catch (err) {
336
+ request.log.error(err, 'Failed to list user plugins');
337
+ return reply.code(500).send({ error: 'Failed to list plugins' });
338
+ }
339
+ });
340
+
341
+ // POST /enroll — consume a grant and generate an enrollment token
342
+ fastify.post('/enroll', async (request, reply) => {
343
+ const username = request.userAccessUsername;
344
+
345
+ let body;
346
+ try {
347
+ body = EnrollBodySchema.parse(request.body);
348
+ } catch {
349
+ return reply.code(400).send({ error: 'Invalid request' });
350
+ }
351
+
352
+ try {
353
+ // Agent-side grants do not require enrollment — they provide browser access
354
+ const allGrants = await listGrantsForUser(username);
355
+ const targetGrant = allGrants.find((g) => g.grantId === body.grantId);
356
+ if (targetGrant && (targetGrant.target || 'local').startsWith('agent:')) {
357
+ return reply.code(400).send({ error: 'Agent-side grants do not require enrollment' });
358
+ }
359
+
360
+ // Consume the grant (validates ownership and single-use)
361
+ const grant = await consumeGrant(body.grantId, username, request.log);
362
+
363
+ // Generate an enrollment token for a new agent
364
+ // Label is auto-generated from username + plugin short name.
365
+ // Underscores in usernames are replaced with hyphens to match agent label format
366
+ // (signCSR validates labels against /^[a-z0-9][a-z0-9-]*$/).
367
+ const pluginShortName = grant.pluginName.replace(/^@lamalibre\//, '').replace(/-server$/, '');
368
+ const sanitizedUsername = username.replace(/_/g, '-');
369
+ const label = `${sanitizedUsername}-${pluginShortName}`;
370
+
371
+ // Default capabilities for user-enrolled agents
372
+ const capabilities = ['tunnels:read', 'services:read', 'system:read'];
373
+
374
+ const tokenData = await createEnrollmentToken(label, capabilities, [], request.log);
375
+
376
+ return {
377
+ ok: true,
378
+ enrollmentToken: tokenData.token,
379
+ label: tokenData.label,
380
+ expiresAt: tokenData.expiresAt,
381
+ pluginName: grant.pluginName,
382
+ };
383
+ } catch (err) {
384
+ const statusCode = err.statusCode || 500;
385
+ const message = statusCode < 500 ? err.message : 'Enrollment failed';
386
+ return reply.code(statusCode).send({ error: message });
387
+ }
388
+ });
389
+ }