@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,522 @@
1
+ /**
2
+ * Static site management routes.
3
+ *
4
+ * Create/delete/update/verifyDns delegate to @lamalibre/lamaste/server core functions.
5
+ * File management endpoints (list, upload, delete) and HTTP-specific auth checks remain here.
6
+ */
7
+ import { z } from 'zod';
8
+ import {
9
+ createSite,
10
+ deleteSite,
11
+ updateSite,
12
+ verifyDns,
13
+ SiteError,
14
+ } from '@lamalibre/lamaste/server';
15
+ import { getConfig } from '../../lib/config.js';
16
+ import { readSites, writeSites, readTunnels } from '../../lib/state.js';
17
+ import { writeStaticSiteVhost, removeStaticSiteVhost } from '../../lib/nginx.js';
18
+ import { updateAccessControl } from '../../lib/authelia.js';
19
+ import { issueTunnelCert, getCertPath, issueSiteCert } from '../../lib/certbot.js';
20
+ import {
21
+ createSiteDirectory,
22
+ removeSiteDirectory,
23
+ listFiles,
24
+ saveUploadedFile,
25
+ deleteFile,
26
+ getSiteSize,
27
+ validatePath,
28
+ validateFileExtension,
29
+ getSiteRoot,
30
+ } from '../../lib/files.js';
31
+
32
+ const IdParamSchema = z.object({ id: z.string().uuid() });
33
+
34
+ const AliasSchema = z
35
+ .string()
36
+ .max(253, 'Alias must be at most 253 characters')
37
+ .regex(/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/, 'Invalid alias hostname');
38
+
39
+ const CreateSiteSchema = z.object({
40
+ name: z
41
+ .string()
42
+ .min(1, 'Name is required')
43
+ .max(100, 'Name must be at most 100 characters')
44
+ .regex(
45
+ /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/,
46
+ 'Name must be lowercase alphanumeric with optional hyphens, cannot start or end with a hyphen',
47
+ ),
48
+ type: z.enum(['managed', 'custom']),
49
+ customDomain: z
50
+ .string()
51
+ .max(253, 'Domain must be at most 253 characters')
52
+ .regex(/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/, 'Invalid domain format')
53
+ .optional(),
54
+ aliases: z.array(AliasSchema).max(10, 'At most 10 aliases').optional(),
55
+ spaMode: z.boolean().optional().default(false),
56
+ autheliaProtected: z.boolean().optional().default(false),
57
+ });
58
+
59
+ const UpdateSiteSchema = z.object({
60
+ spaMode: z.boolean().optional(),
61
+ autheliaProtected: z.boolean().optional(),
62
+ allowedUsers: z.array(z.string().min(1)).optional(),
63
+ aliases: z.array(AliasSchema).max(10, 'At most 10 aliases').optional(),
64
+ });
65
+
66
+ const DeleteFileSchema = z.object({
67
+ path: z.string().min(1, 'Path is required'),
68
+ });
69
+
70
+ const PathQuerySchema = z.object({
71
+ path: z.string().max(500).optional().default('.'),
72
+ });
73
+
74
+ // `z.coerce.number()` lets browser query strings pass `?limit=50` without
75
+ // requiring callers to JSON-encode their numeric params.
76
+ const ListSitesQuerySchema = z.object({
77
+ limit: z.coerce.number().int().min(1).max(500).optional().default(100),
78
+ offset: z.coerce.number().int().min(0).optional().default(0),
79
+ sort: z.enum(['createdAt', 'name']).optional().default('createdAt'),
80
+ order: z.enum(['asc', 'desc']).optional().default('desc'),
81
+ });
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Dependency adapters for core functions
85
+ // ---------------------------------------------------------------------------
86
+
87
+ function buildNginxDeps() {
88
+ return { writeStaticSiteVhost, removeStaticSiteVhost };
89
+ }
90
+
91
+ function buildCertbotDeps() {
92
+ return { issueTunnelCert, getCertPath, issueSiteCert };
93
+ }
94
+
95
+ function buildFilesDeps() {
96
+ return { createSiteDirectory, removeSiteDirectory, getSiteRoot };
97
+ }
98
+
99
+ function buildSiteStateDeps() {
100
+ return { readSites, writeSites };
101
+ }
102
+
103
+ function buildTunnelReadDeps() {
104
+ return { readTunnels };
105
+ }
106
+
107
+ function buildAutheliaDeps() {
108
+ return { updateAccessControl };
109
+ }
110
+
111
+ /**
112
+ * Map a SiteError code to an HTTP status code.
113
+ */
114
+ function siteErrorStatus(code) {
115
+ switch (code) {
116
+ case 'CUSTOM_DOMAIN_REQUIRED':
117
+ case 'NAME_IN_USE':
118
+ case 'RESERVED_NAME':
119
+ case 'NAME_TUNNEL_COLLISION':
120
+ case 'FQDN_IN_USE':
121
+ case 'FQDN_TUNNEL_COLLISION':
122
+ case 'NOT_CUSTOM':
123
+ case 'DOMAIN_NOT_CONFIGURED':
124
+ case 'INVALID_ALIAS':
125
+ case 'ALIAS_IN_USE':
126
+ case 'DNS_MISMATCH':
127
+ return 400;
128
+ case 'NOT_FOUND':
129
+ return 404;
130
+ case 'ALREADY_VERIFIED':
131
+ return 200;
132
+ default:
133
+ return 500;
134
+ }
135
+ }
136
+
137
+ function assertAgentSiteAccess(request, site) {
138
+ if (request.certRole === 'agent') {
139
+ const allowed = request.certAllowedSites || [];
140
+ if (!allowed.includes(site.name)) {
141
+ const err = new Error('You do not have access to this site');
142
+ err.statusCode = 403;
143
+ throw err;
144
+ }
145
+ }
146
+ }
147
+
148
+ export default async function sitesRoutes(fastify, _opts) {
149
+ const nginxDeps = buildNginxDeps();
150
+ const certbotDeps = buildCertbotDeps();
151
+ const filesDeps = buildFilesDeps();
152
+ const siteStateDeps = buildSiteStateDeps();
153
+ const tunnelReadDeps = buildTunnelReadDeps();
154
+ const autheliaDeps = buildAutheliaDeps();
155
+
156
+ // GET /api/sites
157
+ //
158
+ // Paginated. Defaults (limit=100, offset=0, sort=createdAt desc) preserve
159
+ // the prior unpaginated UX for installations under 100 sites. The
160
+ // `sites` array stays the response envelope's primary field; older
161
+ // clients that ignore `total`/`limit`/`offset` continue to function and
162
+ // simply see the first window.
163
+ //
164
+ // Agent-side filtering happens BEFORE windowing so the `total` an agent
165
+ // sees reflects the agent's visible set, not the global count.
166
+ fastify.get(
167
+ '/sites',
168
+ {
169
+ preHandler: fastify.requireRole(['admin', 'agent'], { capability: 'sites:read' }),
170
+ },
171
+ async (request, _reply) => {
172
+ const { limit, offset, sort, order } = ListSitesQuerySchema.parse(request.query);
173
+ let sites = await readSites();
174
+
175
+ const direction = order === 'asc' ? 1 : -1;
176
+ sites.sort((a, b) => {
177
+ if (sort === 'name') {
178
+ const av = String(a.name ?? '');
179
+ const bv = String(b.name ?? '');
180
+ return av < bv ? -direction : av > bv ? direction : 0;
181
+ }
182
+ const at = new Date(a.createdAt ?? 0).getTime();
183
+ const bt = new Date(b.createdAt ?? 0).getTime();
184
+ return (at - bt) * direction;
185
+ });
186
+
187
+ // Agents only see their allowed sites
188
+ if (request.certRole === 'agent') {
189
+ const allowed = request.certAllowedSites || [];
190
+ sites = sites.filter((s) => allowed.includes(s.name));
191
+ }
192
+
193
+ const total = sites.length;
194
+ const windowed = sites.slice(offset, offset + limit);
195
+
196
+ return { sites: windowed, total, limit, offset };
197
+ },
198
+ );
199
+
200
+ // POST /api/sites
201
+ fastify.post(
202
+ '/sites',
203
+ {
204
+ preHandler: fastify.requireRole(['admin']),
205
+ },
206
+ async (request, reply) => {
207
+ const body = CreateSiteSchema.parse(request.body);
208
+ const config = getConfig();
209
+
210
+ if (!config.domain || !config.email) {
211
+ return reply.code(400).send({
212
+ error: 'Domain and email must be configured before creating sites',
213
+ });
214
+ }
215
+
216
+ try {
217
+ const result = await createSite({
218
+ name: body.name,
219
+ type: body.type,
220
+ customDomain: body.customDomain,
221
+ aliases: body.aliases,
222
+ spaMode: body.spaMode,
223
+ autheliaProtected: body.autheliaProtected,
224
+ domain: config.domain,
225
+ email: config.email,
226
+ nginx: nginxDeps,
227
+ certbot: certbotDeps,
228
+ files: filesDeps,
229
+ siteState: siteStateDeps,
230
+ tunnelState: tunnelReadDeps,
231
+ logger: request.log,
232
+ });
233
+
234
+ const response = { ok: true, site: result.site };
235
+ if (result.message) response.message = result.message;
236
+ return reply.code(201).send(response);
237
+ } catch (err) {
238
+ if (err instanceof SiteError) {
239
+ const status = siteErrorStatus(err.code);
240
+ return reply.code(status).send({
241
+ error: 'Failed to create site',
242
+ details: err.message,
243
+ });
244
+ }
245
+ request.log.error(err, 'Failed to create site');
246
+ return reply.code(500).send({
247
+ error: 'Failed to create site',
248
+ details: err.message,
249
+ });
250
+ }
251
+ },
252
+ );
253
+
254
+ // DELETE /api/sites/:id
255
+ fastify.delete(
256
+ '/sites/:id',
257
+ {
258
+ preHandler: fastify.requireRole(['admin']),
259
+ },
260
+ async (request, reply) => {
261
+ const { id } = IdParamSchema.parse(request.params);
262
+
263
+ try {
264
+ await deleteSite({
265
+ id,
266
+ nginx: nginxDeps,
267
+ files: filesDeps,
268
+ siteState: siteStateDeps,
269
+ logger: request.log,
270
+ });
271
+ return { ok: true };
272
+ } catch (err) {
273
+ if (err instanceof SiteError) {
274
+ const status = siteErrorStatus(err.code);
275
+ return reply.code(status).send({
276
+ error: 'Failed to delete site',
277
+ details: err.message,
278
+ });
279
+ }
280
+ request.log.error(err, 'Failed to delete site');
281
+ return reply.code(500).send({
282
+ error: 'Failed to delete site',
283
+ details: err.message,
284
+ });
285
+ }
286
+ },
287
+ );
288
+
289
+ // PATCH /api/sites/:id — update site settings (spaMode, autheliaProtected, allowedUsers, aliases)
290
+ fastify.patch(
291
+ '/sites/:id',
292
+ {
293
+ preHandler: fastify.requireRole(['admin']),
294
+ },
295
+ async (request, reply) => {
296
+ const { id } = IdParamSchema.parse(request.params);
297
+ const body = UpdateSiteSchema.parse(request.body);
298
+ const config = getConfig();
299
+
300
+ try {
301
+ const result = await updateSite({
302
+ id,
303
+ spaMode: body.spaMode,
304
+ autheliaProtected: body.autheliaProtected,
305
+ allowedUsers: body.allowedUsers,
306
+ aliases: body.aliases,
307
+ domain: config.domain,
308
+ email: config.email,
309
+ serverIp: config.ip,
310
+ nginx: nginxDeps,
311
+ certbot: certbotDeps,
312
+ siteState: siteStateDeps,
313
+ tunnelState: tunnelReadDeps,
314
+ authelia: autheliaDeps,
315
+ logger: request.log,
316
+ });
317
+ return result;
318
+ } catch (err) {
319
+ if (err instanceof SiteError) {
320
+ const status = siteErrorStatus(err.code);
321
+ // Special case: AUTHELIA_FAILED means state was saved but Authelia sync failed
322
+ if (err.code === 'AUTHELIA_FAILED') {
323
+ return reply.code(500).send({
324
+ error: 'Site saved but Authelia configuration failed',
325
+ details: err.message,
326
+ });
327
+ }
328
+ return reply.code(status).send({
329
+ error: 'Failed to update site configuration',
330
+ details: err.message,
331
+ });
332
+ }
333
+ request.log.error(err, 'Failed to update site');
334
+ return reply.code(500).send({
335
+ error: 'Failed to update site configuration',
336
+ details: err.message,
337
+ });
338
+ }
339
+ },
340
+ );
341
+
342
+ // POST /api/sites/:id/verify-dns
343
+ fastify.post(
344
+ '/sites/:id/verify-dns',
345
+ {
346
+ preHandler: fastify.requireRole(['admin']),
347
+ // Moderate tier — triggers DNS lookups and potentially certbot;
348
+ // the work is amplified per request and must be bounded.
349
+ config: { rateLimit: { max: 30, timeWindow: '1 minute' } },
350
+ },
351
+ async (request, reply) => {
352
+ const { id } = IdParamSchema.parse(request.params);
353
+ const config = getConfig();
354
+
355
+ try {
356
+ const result = await verifyDns({
357
+ id,
358
+ serverIp: config.ip,
359
+ domain: config.domain,
360
+ email: config.email,
361
+ nginx: nginxDeps,
362
+ certbot: certbotDeps,
363
+ siteState: siteStateDeps,
364
+ logger: request.log,
365
+ });
366
+ return result;
367
+ } catch (err) {
368
+ if (err instanceof SiteError) {
369
+ const status = siteErrorStatus(err.code);
370
+ return reply.code(status).send({
371
+ error: err.message,
372
+ });
373
+ }
374
+ request.log.error(err, 'Failed to verify DNS');
375
+ return reply.code(500).send({
376
+ error: 'DNS verification failed',
377
+ details: err.message,
378
+ });
379
+ }
380
+ },
381
+ );
382
+
383
+ // GET /api/sites/:id/files
384
+ fastify.get(
385
+ '/sites/:id/files',
386
+ {
387
+ preHandler: fastify.requireRole(['admin', 'agent'], { capability: 'sites:read' }),
388
+ },
389
+ async (request, reply) => {
390
+ const { id } = IdParamSchema.parse(request.params);
391
+ const { path: relativePath } = PathQuerySchema.parse(request.query);
392
+
393
+ const sites = await readSites();
394
+ const site = sites.find((s) => s.id === id);
395
+ if (!site) {
396
+ return reply.code(404).send({ error: 'Site not found' });
397
+ }
398
+
399
+ assertAgentSiteAccess(request, site);
400
+
401
+ try {
402
+ const files = await listFiles(id, relativePath);
403
+ return { files, path: relativePath };
404
+ } catch (err) {
405
+ return reply.code(400).send({ error: err.message });
406
+ }
407
+ },
408
+ );
409
+
410
+ // POST /api/sites/:id/files — multipart file upload
411
+ fastify.post(
412
+ '/sites/:id/files',
413
+ {
414
+ preHandler: fastify.requireRole(['admin', 'agent'], { capability: 'sites:write' }),
415
+ },
416
+ async (request, reply) => {
417
+ const { id } = IdParamSchema.parse(request.params);
418
+ const config = getConfig();
419
+
420
+ const sites = await readSites();
421
+ const site = sites.find((s) => s.id === id);
422
+ if (!site) {
423
+ return reply.code(404).send({ error: 'Site not found' });
424
+ }
425
+
426
+ assertAgentSiteAccess(request, site);
427
+
428
+ const { path: uploadDir } = PathQuerySchema.parse(request.query);
429
+ const uploadedFiles = [];
430
+
431
+ try {
432
+ const parts = request.parts();
433
+ for await (const part of parts) {
434
+ if (part.type !== 'file' || !part.filename) {
435
+ continue;
436
+ }
437
+
438
+ const relativePath = uploadDir === '.' ? part.filename : `${uploadDir}/${part.filename}`;
439
+
440
+ // Validate path and file extension before saving
441
+ validatePath(relativePath);
442
+ validateFileExtension(part.filename);
443
+
444
+ await saveUploadedFile(id, relativePath, part.file);
445
+ uploadedFiles.push(relativePath);
446
+ }
447
+ } catch (err) {
448
+ return reply.code(400).send({ error: `Upload failed: ${err.message}` });
449
+ }
450
+
451
+ // Update site size
452
+ try {
453
+ const totalSize = await getSiteSize(id);
454
+ const siteIndex = sites.findIndex((s) => s.id === id);
455
+ sites[siteIndex].totalSize = totalSize;
456
+
457
+ if (totalSize > config.maxSiteSize) {
458
+ // Don't block but warn
459
+ await writeSites(sites);
460
+ return reply.code(200).send({
461
+ ok: true,
462
+ files: uploadedFiles,
463
+ warning: `Site size (${formatBytes(totalSize)}) exceeds the ${formatBytes(config.maxSiteSize)} limit.`,
464
+ totalSize,
465
+ });
466
+ }
467
+
468
+ await writeSites(sites);
469
+ } catch {
470
+ // Non-critical — don't fail the upload
471
+ }
472
+
473
+ return { ok: true, files: uploadedFiles };
474
+ },
475
+ );
476
+
477
+ // DELETE /api/sites/:id/files
478
+ fastify.delete(
479
+ '/sites/:id/files',
480
+ {
481
+ preHandler: fastify.requireRole(['admin', 'agent'], { capability: 'sites:write' }),
482
+ },
483
+ async (request, reply) => {
484
+ const { id } = IdParamSchema.parse(request.params);
485
+
486
+ const sites = await readSites();
487
+ const site = sites.find((s) => s.id === id);
488
+ if (!site) {
489
+ return reply.code(404).send({ error: 'Site not found' });
490
+ }
491
+
492
+ assertAgentSiteAccess(request, site);
493
+
494
+ const body = DeleteFileSchema.parse(request.body);
495
+
496
+ try {
497
+ await deleteFile(id, body.path);
498
+ } catch (err) {
499
+ return reply.code(400).send({ error: err.message });
500
+ }
501
+
502
+ // Update site size
503
+ try {
504
+ const totalSize = await getSiteSize(id);
505
+ const siteIndex = sites.findIndex((s) => s.id === id);
506
+ sites[siteIndex].totalSize = totalSize;
507
+ await writeSites(sites);
508
+ } catch {
509
+ // Non-critical
510
+ }
511
+
512
+ return { ok: true };
513
+ },
514
+ );
515
+ }
516
+
517
+ function formatBytes(bytes) {
518
+ if (bytes < 1024) return `${bytes} B`;
519
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
520
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
521
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
522
+ }
@@ -0,0 +1,192 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ registerStorageServer,
4
+ removeStorageServer,
5
+ listStorageServers,
6
+ bindPluginStorage,
7
+ unbindPluginStorage,
8
+ listBindings,
9
+ getBinding,
10
+ } from '../../lib/storage.js';
11
+ import { readPlugins } from '../../lib/plugins.js';
12
+
13
+ // --- Zod schemas ---
14
+
15
+ const RegisterServerSchema = z.object({
16
+ id: z.string().uuid(),
17
+ label: z.string().min(1).max(100),
18
+ provider: z.string().min(1).max(50),
19
+ region: z.string().min(1).max(50),
20
+ bucket: z.string().min(1).max(200),
21
+ endpoint: z.string().url(),
22
+ accessKey: z.string().min(1).max(500),
23
+ secretKey: z.string().min(1).max(500),
24
+ });
25
+
26
+ const ServerIdParamSchema = z.object({
27
+ id: z.string().uuid(),
28
+ });
29
+
30
+ const BindingBodySchema = z.object({
31
+ pluginName: z
32
+ .string()
33
+ .min(1)
34
+ .max(100)
35
+ .regex(/^[a-z0-9-]+$/, 'Plugin name must contain only lowercase letters, numbers, and hyphens'),
36
+ storageServerId: z.string().uuid(),
37
+ });
38
+
39
+ const PluginNameParamSchema = z.object({
40
+ pluginName: z
41
+ .string()
42
+ .min(1)
43
+ .max(100)
44
+ .regex(/^[a-z0-9-]+$/, 'Plugin name must contain only lowercase letters, numbers, and hyphens'),
45
+ });
46
+
47
+ export default async function storageRoutes(fastify, _opts) {
48
+ // ===========================================================================
49
+ // Storage server routes
50
+ // ===========================================================================
51
+
52
+ // POST /storage/servers — register a storage server
53
+ fastify.post(
54
+ '/storage/servers',
55
+ {
56
+ preHandler: fastify.requireRole(['admin']),
57
+ },
58
+ async (request, reply) => {
59
+ const body = RegisterServerSchema.parse(request.body);
60
+
61
+ try {
62
+ const entry = await registerStorageServer(body);
63
+ return reply.code(201).send(entry);
64
+ } catch (err) {
65
+ const statusCode = err.statusCode || 500;
66
+ return reply.code(statusCode).send({
67
+ error: err.message || 'Failed to register storage server',
68
+ });
69
+ }
70
+ },
71
+ );
72
+
73
+ // GET /storage/servers — list registered storage servers
74
+ fastify.get(
75
+ '/storage/servers',
76
+ {
77
+ preHandler: fastify.requireRole(['admin']),
78
+ },
79
+ async (_request, _reply) => {
80
+ const servers = await listStorageServers();
81
+ return { servers };
82
+ },
83
+ );
84
+
85
+ // DELETE /storage/servers/:id — remove storage server and bindings
86
+ fastify.delete(
87
+ '/storage/servers/:id',
88
+ {
89
+ preHandler: fastify.requireRole(['admin']),
90
+ },
91
+ async (request, reply) => {
92
+ const params = ServerIdParamSchema.parse(request.params);
93
+
94
+ try {
95
+ const result = await removeStorageServer(params.id);
96
+ return result;
97
+ } catch (err) {
98
+ const statusCode = err.statusCode || 500;
99
+ return reply.code(statusCode).send({
100
+ error: err.message || 'Failed to remove storage server',
101
+ });
102
+ }
103
+ },
104
+ );
105
+
106
+ // ===========================================================================
107
+ // Storage binding routes
108
+ // ===========================================================================
109
+
110
+ // POST /storage/bindings — bind storage to plugin
111
+ fastify.post(
112
+ '/storage/bindings',
113
+ {
114
+ preHandler: fastify.requireRole(['admin']),
115
+ },
116
+ async (request, reply) => {
117
+ const body = BindingBodySchema.parse(request.body);
118
+
119
+ // Validate plugin exists in registry
120
+ const registry = await readPlugins();
121
+ const pluginExists = registry.plugins.some((p) => p.name === body.pluginName);
122
+ if (!pluginExists) {
123
+ return reply.code(404).send({
124
+ error: `Plugin "${body.pluginName}" not found in registry`,
125
+ });
126
+ }
127
+
128
+ try {
129
+ const binding = await bindPluginStorage(body.pluginName, body.storageServerId);
130
+ return reply.code(201).send(binding);
131
+ } catch (err) {
132
+ const statusCode = err.statusCode || 500;
133
+ return reply.code(statusCode).send({
134
+ error: err.message || 'Failed to bind storage to plugin',
135
+ });
136
+ }
137
+ },
138
+ );
139
+
140
+ // GET /storage/bindings — list all bindings
141
+ fastify.get(
142
+ '/storage/bindings',
143
+ {
144
+ preHandler: fastify.requireRole(['admin']),
145
+ },
146
+ async (_request, _reply) => {
147
+ const bindings = await listBindings();
148
+ return { bindings };
149
+ },
150
+ );
151
+
152
+ // GET /storage/bindings/:pluginName — get binding for a specific plugin
153
+ fastify.get(
154
+ '/storage/bindings/:pluginName',
155
+ {
156
+ preHandler: fastify.requireRole(['admin']),
157
+ },
158
+ async (request, reply) => {
159
+ const params = PluginNameParamSchema.parse(request.params);
160
+
161
+ const binding = await getBinding(params.pluginName);
162
+ if (!binding) {
163
+ return reply.code(404).send({
164
+ error: `No storage binding for plugin "${params.pluginName}"`,
165
+ });
166
+ }
167
+
168
+ return binding;
169
+ },
170
+ );
171
+
172
+ // DELETE /storage/bindings/:pluginName — unbind storage from plugin
173
+ fastify.delete(
174
+ '/storage/bindings/:pluginName',
175
+ {
176
+ preHandler: fastify.requireRole(['admin']),
177
+ },
178
+ async (request, reply) => {
179
+ const params = PluginNameParamSchema.parse(request.params);
180
+
181
+ try {
182
+ const result = await unbindPluginStorage(params.pluginName);
183
+ return result;
184
+ } catch (err) {
185
+ const statusCode = err.statusCode || 500;
186
+ return reply.code(statusCode).send({
187
+ error: err.message || 'Failed to unbind storage from plugin',
188
+ });
189
+ }
190
+ },
191
+ );
192
+ }