@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,716 @@
1
+ # Static Sites API
2
+
3
+ > Host static websites through Lamaste — upload files, manage domains, and serve content directly from nginx without a tunnel.
4
+
5
+ ## In Plain English
6
+
7
+ While tunnels forward requests to an app running on your local machine, static sites are served directly from the droplet. You upload HTML, CSS, JavaScript, and image files through the management panel, and nginx serves them at a subdomain of your domain (or a custom domain you own).
8
+
9
+ This is useful for landing pages, documentation sites, or any content that does not need a backend server.
10
+
11
+ There are two types of sites:
12
+
13
+ - **Managed** — uses a subdomain of your Lamaste domain (like `docs.example.com`). The certificate and DNS are handled automatically.
14
+ - **Custom** — uses a domain you own (like `myblog.net`). You need to point its DNS to your droplet, then verify DNS through the API before the site goes live.
15
+
16
+ ## Authentication
17
+
18
+ All site endpoints require a valid mTLS client certificate and a completed onboarding. See the [API Overview](./overview.md) for details.
19
+
20
+ If onboarding is not complete, all endpoints return `503 Service Unavailable`.
21
+
22
+ ### Role-Based Access
23
+
24
+ Site endpoints use a two-level access model: capabilities control which operations are permitted, and `allowedSites` controls which sites an agent can interact with.
25
+
26
+ **Admin-only endpoints (require admin certificate):**
27
+
28
+ | Endpoint | Description |
29
+ | -------------------------------- | ---------------------------------- |
30
+ | `POST /api/sites` | Create a new site |
31
+ | `PATCH /api/sites/:id` | Update site settings |
32
+ | `DELETE /api/sites/:id` | Delete a site |
33
+ | `POST /api/sites/:id/verify-dns` | Verify DNS for custom domain sites |
34
+
35
+ **Agent-accessible endpoints (require capability + site in allowedSites):**
36
+
37
+ | Capability | Grants Access To | Site Scoping |
38
+ | ------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
39
+ | `sites:read` | `GET /api/sites` (list sites), `GET /api/sites/:id/files` (list files) | Agent sees only sites in its `allowedSites` list |
40
+ | `sites:write` | `POST /api/sites/:id/files` (upload files), `DELETE /api/sites/:id/files` (delete files) | Agent can only modify files on sites in its `allowedSites` list |
41
+
42
+ Admin certificates have full access to all endpoints and see all sites regardless of `allowedSites`. Agent certificates must have both the relevant capability and the site name in their `allowedSites` list. The admin assigns sites to agents from **Panel** > **Certificates** > **Agent Certificates** > edit agent > **Site Access**.
43
+
44
+ ## Endpoints
45
+
46
+ ### `GET /api/sites`
47
+
48
+ Returns all configured static sites, sorted by creation date (newest first).
49
+
50
+ **Request:**
51
+
52
+ No request body.
53
+
54
+ ```bash
55
+ curl -s --cert client.p12:password \
56
+ https://203.0.113.42:9292/api/sites | jq
57
+ ```
58
+
59
+ **Response (200):**
60
+
61
+ ```json
62
+ {
63
+ "sites": [
64
+ {
65
+ "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
66
+ "name": "docs",
67
+ "fqdn": "docs.example.com",
68
+ "type": "managed",
69
+ "spaMode": false,
70
+ "autheliaProtected": false,
71
+ "allowedUsers": [],
72
+ "dnsVerified": true,
73
+ "certIssued": true,
74
+ "rootPath": "/var/www/lamaste/c3d4e5f6-a7b8-9012-cdef-123456789012",
75
+ "createdAt": "2026-03-13T14:30:00.000Z",
76
+ "totalSize": 524288
77
+ },
78
+ {
79
+ "id": "d4e5f6a7-b8c9-0123-defa-234567890123",
80
+ "name": "blog",
81
+ "fqdn": "myblog.net",
82
+ "type": "custom",
83
+ "spaMode": true,
84
+ "autheliaProtected": false,
85
+ "allowedUsers": [],
86
+ "dnsVerified": false,
87
+ "certIssued": false,
88
+ "rootPath": "/var/www/lamaste/d4e5f6a7-b8c9-0123-defa-234567890123",
89
+ "createdAt": "2026-03-12T10:15:00.000Z",
90
+ "totalSize": 0
91
+ }
92
+ ]
93
+ }
94
+ ```
95
+
96
+ | Field | Type | Description |
97
+ | ------------------- | ---------- | --------------------------------------------------------------------------------------------------------- |
98
+ | `id` | `string` | UUID v4 identifier |
99
+ | `name` | `string` | Site name (used as subdomain for managed sites) |
100
+ | `fqdn` | `string` | Fully qualified domain name |
101
+ | `type` | `string` | `"managed"` (subdomain) or `"custom"` (your own domain) |
102
+ | `spaMode` | `boolean` | If `true`, all routes serve `index.html` (for single-page apps) |
103
+ | `autheliaProtected` | `boolean` | If `true`, Authelia authentication is required to access the site |
104
+ | `allowedUsers` | `string[]` | List of Authelia usernames allowed to access the site (empty array means all authenticated users) |
105
+ | `dnsVerified` | `boolean` | Whether DNS has been verified (always `true` for managed sites) |
106
+ | `certIssued` | `boolean` | Whether a TLS certificate has been issued |
107
+ | `rootPath` | `string` | Filesystem path where uploaded files are stored |
108
+ | `createdAt` | `string` | ISO 8601 timestamp |
109
+ | `totalSize` | `number` | Total size of uploaded files in bytes |
110
+ | `aliases` | `string[]` | Custom-domain sites only, optional: hostnames answered with a 301 to `https://<fqdn>` (e.g. `www.<fqdn>`) |
111
+
112
+ ---
113
+
114
+ ### `POST /api/sites`
115
+
116
+ Creates a new static site.
117
+
118
+ For **managed** sites, the full provisioning happens immediately: TLS certificate issuance, nginx vhost creation, and directory setup. The site is live as soon as the endpoint returns.
119
+
120
+ For **custom** domain sites, only the directory and state are created. You must then add a DNS A record pointing your domain to the droplet and call `POST /api/sites/:id/verify-dns` to issue the certificate and configure nginx.
121
+
122
+ **Request:**
123
+
124
+ ```json
125
+ {
126
+ "name": "docs",
127
+ "type": "managed",
128
+ "spaMode": false,
129
+ "autheliaProtected": false
130
+ }
131
+ ```
132
+
133
+ For a custom domain:
134
+
135
+ ```json
136
+ {
137
+ "name": "blog",
138
+ "type": "custom",
139
+ "customDomain": "myblog.net",
140
+ "spaMode": true,
141
+ "autheliaProtected": false
142
+ }
143
+ ```
144
+
145
+ | Field | Type | Validation | Description |
146
+ | ------------------- | ---------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
147
+ | `name` | `string` | 1-100 chars, lowercase alphanumeric + hyphens, cannot start/end with hyphen | Site name (used as subdomain for managed type) |
148
+ | `type` | `string` | `"managed"` or `"custom"` | Site type |
149
+ | `customDomain` | `string` | Max 253 chars, lowercase alphanumeric + dots + hyphens; required for `custom` type | Your own domain |
150
+ | `aliases` | `string[]` | Custom sites only, optional, at most 10 hostnames | Hostnames that 301 to the site, e.g. `["www.myblog.net"]`. Each needs its own A record; one certificate covers them all |
151
+ | `spaMode` | `boolean` | Optional, defaults to `false` | Serve `index.html` for all routes |
152
+ | `autheliaProtected` | `boolean` | Optional, defaults to `false` | Require Authelia login |
153
+
154
+ **Name regex:**
155
+
156
+ ```
157
+ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$
158
+ ```
159
+
160
+ **Custom domain regex:**
161
+
162
+ ```
163
+ ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$
164
+ ```
165
+
166
+ ```bash
167
+ # Create managed site
168
+ curl -s --cert client.p12:password \
169
+ -X POST \
170
+ -H "Content-Type: application/json" \
171
+ -d '{"name":"docs","type":"managed"}' \
172
+ https://203.0.113.42:9292/api/sites | jq
173
+
174
+ # Create custom domain site
175
+ curl -s --cert client.p12:password \
176
+ -X POST \
177
+ -H "Content-Type: application/json" \
178
+ -d '{"name":"blog","type":"custom","customDomain":"myblog.net","spaMode":true}' \
179
+ https://203.0.113.42:9292/api/sites | jq
180
+ ```
181
+
182
+ **Response (201) — managed site:**
183
+
184
+ ```json
185
+ {
186
+ "ok": true,
187
+ "site": {
188
+ "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
189
+ "name": "docs",
190
+ "fqdn": "docs.example.com",
191
+ "type": "managed",
192
+ "spaMode": false,
193
+ "autheliaProtected": false,
194
+ "dnsVerified": true,
195
+ "certIssued": true,
196
+ "rootPath": "/var/www/lamaste/c3d4e5f6-a7b8-9012-cdef-123456789012",
197
+ "createdAt": "2026-03-13T14:30:00.000Z",
198
+ "totalSize": 0
199
+ }
200
+ }
201
+ ```
202
+
203
+ **Response (201) — custom domain site:**
204
+
205
+ ```json
206
+ {
207
+ "ok": true,
208
+ "site": {
209
+ "id": "d4e5f6a7-b8c9-0123-defa-234567890123",
210
+ "name": "blog",
211
+ "fqdn": "myblog.net",
212
+ "type": "custom",
213
+ "spaMode": true,
214
+ "autheliaProtected": false,
215
+ "dnsVerified": false,
216
+ "certIssued": false,
217
+ "rootPath": "/var/www/lamaste/d4e5f6a7-b8c9-0123-defa-234567890123",
218
+ "createdAt": "2026-03-13T14:30:00.000Z",
219
+ "totalSize": 0
220
+ },
221
+ "message": "Site created. Add an A record for your domain, then verify DNS."
222
+ }
223
+ ```
224
+
225
+ **Errors:**
226
+
227
+ | Status | Body | When |
228
+ | ------ | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
229
+ | 400 | `{"error":"Validation failed","details":{"issues":[...]}}` | Invalid name format, type, or custom domain |
230
+ | 400 | `{"error":"Custom domain is required for custom type sites"}` | `type` is `custom` but `customDomain` is missing |
231
+ | 400 | `{"error":"Site name 'docs' is already in use"}` | Another site uses this name |
232
+ | 400 | `{"error":"Name 'panel' is reserved"}` | Name collides with a reserved subdomain |
233
+ | 400 | `{"error":"Name 'app' is already in use by a tunnel"}` | Name collides with an existing tunnel subdomain |
234
+ | 400 | `{"error":"Domain 'docs.example.com' is already in use by another site"}` | FQDN collision with another site |
235
+ | 400 | `{"error":"Failed to create site","details":"Invalid alias hostname '...'"}` | Malformed alias, the site's own domain, or a Lamaste hostname (`panel.`, `auth.`, `tunnel.`, the base domain) |
236
+ | 400 | `{"error":"Failed to create site","details":"'x' is already served by site 'y'"}` | Alias collides with another site's domain or alias, or a tunnel |
237
+ | 400 | `{"error":"Failed to create site","details":"Aliases are only supported on custom-domain sites"}` | Aliases on a managed site |
238
+ | 400 | `{"error":"Domain 'app.example.com' is already in use by a tunnel"}` | FQDN collision with an existing tunnel |
239
+ | 400 | `{"error":"Domain and email must be configured before creating sites"}` | Domain not set in config |
240
+ | 500 | `{"error":"Failed to create site","details":"Certificate issuance failed: ..."}` | certbot failed (managed sites) |
241
+ | 500 | `{"error":"Failed to create site","details":"Nginx configuration failed: ..."}` | nginx vhost failed (managed sites) |
242
+ | 500 | `{"error":"Failed to create site","details":"Directory creation failed: ..."}` | Could not create site directory |
243
+ | 500 | `{"error":"Failed to create site","details":"State persistence failed: ..."}` | Could not write sites.json |
244
+
245
+ **Reserved names** (for managed type):
246
+
247
+ `panel`, `auth`, `tunnel`, `www`, `mail`, `ftp`, `api`
248
+
249
+ ---
250
+
251
+ ### `DELETE /api/sites/:id`
252
+
253
+ Deletes a static site by its UUID. Removes the nginx vhost (if a certificate was issued), the site directory and all uploaded files, and the site from the state file.
254
+
255
+ **Request:**
256
+
257
+ No request body.
258
+
259
+ ```bash
260
+ curl -s --cert client.p12:password \
261
+ -X DELETE \
262
+ https://203.0.113.42:9292/api/sites/c3d4e5f6-a7b8-9012-cdef-123456789012 | jq
263
+ ```
264
+
265
+ **Response (200):**
266
+
267
+ ```json
268
+ {
269
+ "ok": true
270
+ }
271
+ ```
272
+
273
+ **Errors:**
274
+
275
+ | Status | Body | When |
276
+ | ------ | --------------------------------------------------- | ------------------------------------------- |
277
+ | 404 | `{"error":"Site not found"}` | No site with the given UUID |
278
+ | 500 | `{"error":"Failed to delete site","details":"..."}` | nginx, directory, or state operation failed |
279
+
280
+ ---
281
+
282
+ ### `PATCH /api/sites/:id`
283
+
284
+ Updates a site's settings. Any combination of fields can be sent; only provided fields are changed. If nginx-affecting settings (`spaMode`, `autheliaProtected`) change and the site has a live certificate, the nginx vhost is regenerated. If `autheliaProtected` or `allowedUsers` change, the Authelia access control configuration is updated and reloaded.
285
+
286
+ **Request:**
287
+
288
+ ```json
289
+ {
290
+ "spaMode": true,
291
+ "autheliaProtected": true,
292
+ "allowedUsers": ["alice", "bob"]
293
+ }
294
+ ```
295
+
296
+ | Field | Type | Validation | Description |
297
+ | ------------------- | ---------- | ---------------------------------------------------------------- | ---------------------------------------------- |
298
+ | `spaMode` | `boolean` | Optional | Serve `index.html` for all routes |
299
+ | `autheliaProtected` | `boolean` | Optional | Require Authelia login |
300
+ | `allowedUsers` | `string[]` | Optional, each element min 1 char | Authelia usernames allowed to access this site |
301
+ | `aliases` | `string[]` | Optional, custom sites only; the complete new list (`[]` clears) | Redirect aliases |
302
+
303
+ **Changing aliases on a live site** takes effect immediately, in an order where the site serves correctly at every moment:
304
+
305
+ 1. Every newly added alias must already resolve to this server — otherwise `400` with `DNS_MISMATCH` details and nothing changes.
306
+ 2. The certificate lineage is issued for the **union** of the old and new names, so it is valid for whichever vhost nginx is serving.
307
+ 3. The vhost is rewritten (tested by nginx; the previous file is restored on failure), then the new list is saved.
308
+ 4. The certificate is re-issued for **exactly** the new set, so a later renewal never has to validate a name the site no longer serves (its DNS may already point elsewhere).
309
+
310
+ A failure in steps 1–3 leaves the saved site unchanged. A failure in step 4 leaves a working site whose certificate still names the removed aliases: the response is still `200`, with a `warning` explaining that renewal will fail once those names stop resolving here; the next alias update retries it. On a site still awaiting DNS verification the list is simply saved; `verify-dns` then checks every name.
311
+
312
+ ```bash
313
+ curl -s --cert client.p12:password \
314
+ -X PATCH \
315
+ -H "Content-Type: application/json" \
316
+ -d '{"autheliaProtected":true,"allowedUsers":["alice"]}' \
317
+ https://203.0.113.42:9292/api/sites/c3d4e5f6-a7b8-9012-cdef-123456789012 | jq
318
+ ```
319
+
320
+ **Response (200):**
321
+
322
+ ```json
323
+ {
324
+ "ok": true,
325
+ "site": {
326
+ "id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
327
+ "name": "docs",
328
+ "fqdn": "docs.example.com",
329
+ "type": "managed",
330
+ "spaMode": false,
331
+ "autheliaProtected": true,
332
+ "allowedUsers": ["alice"],
333
+ "dnsVerified": true,
334
+ "certIssued": true,
335
+ "rootPath": "/var/www/lamaste/c3d4e5f6-a7b8-9012-cdef-123456789012",
336
+ "createdAt": "2026-03-13T14:30:00.000Z",
337
+ "totalSize": 524288
338
+ }
339
+ }
340
+ ```
341
+
342
+ **Response (200) — aliases changed, certificate not narrowed:**
343
+
344
+ ```json
345
+ {
346
+ "ok": true,
347
+ "site": { ... },
348
+ "warning": "The site now serves the new alias list, but its certificate still names removed aliases, and renewing it will fail once they stop resolving here: ..."
349
+ }
350
+ ```
351
+
352
+ **Response (200) — no changes:**
353
+
354
+ ```json
355
+ {
356
+ "ok": true,
357
+ "site": { ... },
358
+ "message": "No changes"
359
+ }
360
+ ```
361
+
362
+ **Errors:**
363
+
364
+ | Status | Body | When |
365
+ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
366
+ | 400 | `{"error":"Failed to update site configuration","details":"Add A records pointing www.myblog.net to 203.0.113.42 before adding them as aliases"}` | A newly added alias on a live site does not resolve here (`DNS_MISMATCH`); nothing changed |
367
+ | 400 | `{"error":"Failed to update site configuration","details":"'x' is already served by site 'y'"}` | Invalid or colliding alias (same rules as creation), or aliases on a managed site |
368
+ | 404 | `{"error":"Site not found"}` | No site with the given UUID |
369
+ | 500 | `{"error":"Failed to update site configuration","details":"Certificate issuance failed: ..."}` | Re-issuing the certificate for the new alias set failed |
370
+ | 500 | `{"error":"Failed to update site configuration","details":"Nginx configuration failed: ..."}` | nginx vhost regeneration failed |
371
+ | 500 | `{"error":"Site saved but Authelia configuration failed","details":"..."}` | Authelia access control update failed |
372
+
373
+ ---
374
+
375
+ ### `POST /api/sites/:id/verify-dns`
376
+
377
+ Verifies that a custom domain's DNS A record — and the A record of every alias — points to the droplet's IP address. On success, issues one Let's Encrypt certificate lineage (named after the site's domain) covering the domain and every alias, and configures the nginx vhost with a redirect block for the aliases. The site becomes live immediately.
378
+
379
+ This endpoint is only applicable to `custom` type sites. Managed sites have DNS verified automatically at creation.
380
+
381
+ **Request:**
382
+
383
+ No request body.
384
+
385
+ ```bash
386
+ curl -s --cert client.p12:password \
387
+ -X POST \
388
+ https://203.0.113.42:9292/api/sites/d4e5f6a7-b8c9-0123-defa-234567890123/verify-dns | jq
389
+ ```
390
+
391
+ **Response (200) — DNS correct:**
392
+
393
+ ```json
394
+ {
395
+ "ok": true,
396
+ "message": "DNS verified, certificate issued, and site is now live."
397
+ }
398
+ ```
399
+
400
+ **Response (200) — DNS not yet propagated:**
401
+
402
+ ```json
403
+ {
404
+ "ok": false,
405
+ "fqdn": "myblog.net",
406
+ "expectedIp": "203.0.113.42",
407
+ "resolvedIps": [],
408
+ "message": "Domain does not resolve yet. Please add an A record pointing myblog.net to 203.0.113.42."
409
+ }
410
+ ```
411
+
412
+ **Response (200) — DNS points to wrong IP:**
413
+
414
+ ```json
415
+ {
416
+ "ok": false,
417
+ "fqdn": "myblog.net",
418
+ "expectedIp": "203.0.113.42",
419
+ "resolvedIps": ["198.51.100.1"],
420
+ "message": "Domain resolves to 198.51.100.1 but your server IP is 203.0.113.42. Please update your A record."
421
+ }
422
+ ```
423
+
424
+ **Response (200) — an alias does not resolve yet:**
425
+
426
+ ```json
427
+ {
428
+ "ok": false,
429
+ "fqdn": "www.myblog.net",
430
+ "expectedIp": "203.0.113.42",
431
+ "resolvedIps": [],
432
+ "message": "Add A records pointing www.myblog.net to 203.0.113.42 — every alias shares the site's certificate."
433
+ }
434
+ ```
435
+
436
+ **Response (200) — already verified:**
437
+
438
+ ```json
439
+ {
440
+ "ok": true,
441
+ "message": "DNS already verified and certificate issued"
442
+ }
443
+ ```
444
+
445
+ **Errors:**
446
+
447
+ | Status | Body | When |
448
+ | ------ | ------------------------------------------------------------------------------- | ---------------------------------- |
449
+ | 400 | `{"error":"DNS verification is only needed for custom domains"}` | Site type is `managed` |
450
+ | 404 | `{"error":"Site not found"}` | No site with the given UUID |
451
+ | 500 | `{"error":"DNS verified but certificate issuance failed","details":"..."}` | DNS passed but certbot failed |
452
+ | 500 | `{"error":"Certificate issued but nginx configuration failed","details":"..."}` | Cert issued but vhost write failed |
453
+
454
+ ---
455
+
456
+ ### `GET /api/sites/:id/files`
457
+
458
+ Lists files in a site's upload directory. Supports browsing subdirectories via the `path` query parameter.
459
+
460
+ **Query parameters:**
461
+
462
+ | Parameter | Type | Default | Description |
463
+ | --------- | -------- | ------- | ----------------------------------------------- |
464
+ | `path` | `string` | `"."` | Relative path within the site directory to list |
465
+
466
+ ```bash
467
+ curl -s --cert client.p12:password \
468
+ "https://203.0.113.42:9292/api/sites/<uuid>/files" | jq
469
+
470
+ # List a subdirectory
471
+ curl -s --cert client.p12:password \
472
+ "https://203.0.113.42:9292/api/sites/<uuid>/files?path=css" | jq
473
+ ```
474
+
475
+ **Response (200):**
476
+
477
+ ```json
478
+ {
479
+ "files": [
480
+ {
481
+ "name": "index.html",
482
+ "type": "file",
483
+ "size": 2048,
484
+ "modifiedAt": "2026-03-19T10:30:00.000Z",
485
+ "relativePath": "index.html"
486
+ },
487
+ {
488
+ "name": "css",
489
+ "type": "directory",
490
+ "size": 4096,
491
+ "modifiedAt": "2026-03-19T10:30:00.000Z",
492
+ "relativePath": "css"
493
+ },
494
+ {
495
+ "name": "logo.png",
496
+ "type": "file",
497
+ "size": 15360,
498
+ "modifiedAt": "2026-03-19T10:30:00.000Z",
499
+ "relativePath": "logo.png"
500
+ }
501
+ ],
502
+ "path": "."
503
+ }
504
+ ```
505
+
506
+ **Errors:**
507
+
508
+ | Status | Body | When |
509
+ | ------ | ---------------------------- | --------------------------------------------------- |
510
+ | 400 | `{"error":"..."}` | Invalid or disallowed path (path traversal attempt) |
511
+ | 404 | `{"error":"Site not found"}` | No site with the given UUID |
512
+
513
+ ---
514
+
515
+ ### `POST /api/sites/:id/files`
516
+
517
+ Uploads one or more files to a site's directory via multipart form data. Files are saved relative to the directory specified by the `path` query parameter.
518
+
519
+ **Query parameters:**
520
+
521
+ | Parameter | Type | Default | Description |
522
+ | --------- | -------- | ------- | --------------------------------------------------- |
523
+ | `path` | `string` | `"."` | Target directory within the site for uploaded files |
524
+
525
+ **Request:**
526
+
527
+ ```bash
528
+ # Upload a single file
529
+ curl -s --cert client.p12:password \
530
+ -X POST \
531
+ -F "file=@index.html" \
532
+ "https://203.0.113.42:9292/api/sites/<uuid>/files" | jq
533
+
534
+ # Upload to a subdirectory
535
+ curl -s --cert client.p12:password \
536
+ -X POST \
537
+ -F "file=@style.css" \
538
+ "https://203.0.113.42:9292/api/sites/<uuid>/files?path=css" | jq
539
+
540
+ # Upload multiple files
541
+ curl -s --cert client.p12:password \
542
+ -X POST \
543
+ -F "file=@index.html" \
544
+ -F "file=@style.css" \
545
+ "https://203.0.113.42:9292/api/sites/<uuid>/files" | jq
546
+ ```
547
+
548
+ **Content-Type:** `multipart/form-data`
549
+
550
+ **File size limit:** 50 MB per file.
551
+
552
+ **Response (200):**
553
+
554
+ ```json
555
+ {
556
+ "ok": true,
557
+ "files": ["index.html", "style.css"]
558
+ }
559
+ ```
560
+
561
+ **Response (200) — with size warning:**
562
+
563
+ ```json
564
+ {
565
+ "ok": true,
566
+ "files": ["large-bundle.js"],
567
+ "warning": "Site size (52.3 MB) exceeds the 50.0 MB limit.",
568
+ "totalSize": 54857728
569
+ }
570
+ ```
571
+
572
+ **Errors:**
573
+
574
+ | Status | Body | When |
575
+ | ------ | -------------------------------- | -------------------------------------------------------------------------------- |
576
+ | 400 | `{"error":"Upload failed: ..."}` | Path traversal attempt, invalid path, disallowed file extension, or stream error |
577
+ | 404 | `{"error":"Site not found"}` | No site with the given UUID |
578
+
579
+ File paths are validated to prevent directory traversal. Paths containing `..` or absolute paths are rejected.
580
+
581
+ ---
582
+
583
+ ### `DELETE /api/sites/:id/files`
584
+
585
+ Deletes a single file from a site's directory.
586
+
587
+ **Request:**
588
+
589
+ ```json
590
+ {
591
+ "path": "old-page.html"
592
+ }
593
+ ```
594
+
595
+ | Field | Type | Validation | Description |
596
+ | ------ | -------- | -------------------- | --------------------------------------------------- |
597
+ | `path` | `string` | Min 1 char, required | Relative path to the file within the site directory |
598
+
599
+ ```bash
600
+ curl -s --cert client.p12:password \
601
+ -X DELETE \
602
+ -H "Content-Type: application/json" \
603
+ -d '{"path":"old-page.html"}' \
604
+ "https://203.0.113.42:9292/api/sites/<uuid>/files" | jq
605
+ ```
606
+
607
+ **Response (200):**
608
+
609
+ ```json
610
+ {
611
+ "ok": true
612
+ }
613
+ ```
614
+
615
+ **Errors:**
616
+
617
+ | Status | Body | When |
618
+ | ------ | ---------------------------------------------------------- | ---------------------------------------- |
619
+ | 400 | `{"error":"Validation failed","details":{"issues":[...]}}` | Missing or empty path |
620
+ | 400 | `{"error":"..."}` | Path traversal attempt or file not found |
621
+ | 404 | `{"error":"Site not found"}` | No site with the given UUID |
622
+
623
+ ## Quick Reference
624
+
625
+ | Method | Path | Description |
626
+ | ------ | --------------------------- | ------------------------------------------------------------------------ |
627
+ | GET | `/api/sites` | List all static sites (newest first) |
628
+ | POST | `/api/sites` | Create a static site |
629
+ | PATCH | `/api/sites/:id` | Update site settings (spaMode, autheliaProtected, allowedUsers, aliases) |
630
+ | DELETE | `/api/sites/:id` | Delete a site and all its files |
631
+ | POST | `/api/sites/:id/verify-dns` | Verify DNS for custom domain sites |
632
+ | GET | `/api/sites/:id/files` | List files in a site directory |
633
+ | POST | `/api/sites/:id/files` | Upload files (multipart) |
634
+ | DELETE | `/api/sites/:id/files` | Delete a file |
635
+
636
+ ### Site Object Shape
637
+
638
+ ```json
639
+ {
640
+ "id": "uuid-v4",
641
+ "name": "docs",
642
+ "fqdn": "docs.example.com",
643
+ "type": "managed",
644
+ "spaMode": false,
645
+ "autheliaProtected": false,
646
+ "allowedUsers": [],
647
+ "dnsVerified": true,
648
+ "certIssued": true,
649
+ "rootPath": "/var/www/lamaste/<uuid>",
650
+ "createdAt": "2026-03-13T14:30:00.000Z",
651
+ "totalSize": 524288
652
+ }
653
+ ```
654
+
655
+ ### Managed vs Custom Site Flow
656
+
657
+ ```
658
+ Managed site:
659
+ POST /sites (name, type: "managed")
660
+ → cert issued → vhost written → directory created → LIVE
661
+
662
+ Custom domain site:
663
+ POST /sites (name, type: "custom", customDomain: "myblog.net", aliases: ["www.myblog.net"])
664
+ → directory created → state saved → DNS PENDING
665
+
666
+ (user adds A records for myblog.net and www.myblog.net)
667
+
668
+ POST /sites/:id/verify-dns
669
+ → DNS checked for every name → one cert for all names
670
+ → vhost written (www.myblog.net → 301 https://myblog.net) → LIVE
671
+ ```
672
+
673
+ ### curl Cheat Sheet
674
+
675
+ ```bash
676
+ # List sites
677
+ curl -s --cert client.p12:password \
678
+ https://203.0.113.42:9292/api/sites | jq
679
+
680
+ # Create managed site
681
+ curl -s --cert client.p12:password \
682
+ -X POST -H "Content-Type: application/json" \
683
+ -d '{"name":"docs","type":"managed"}' \
684
+ https://203.0.113.42:9292/api/sites | jq
685
+
686
+ # Create custom domain site
687
+ curl -s --cert client.p12:password \
688
+ -X POST -H "Content-Type: application/json" \
689
+ -d '{"name":"blog","type":"custom","customDomain":"myblog.net","spaMode":true}' \
690
+ https://203.0.113.42:9292/api/sites | jq
691
+
692
+ # Verify DNS for custom site
693
+ curl -s --cert client.p12:password \
694
+ -X POST \
695
+ https://203.0.113.42:9292/api/sites/<uuid>/verify-dns | jq
696
+
697
+ # Upload files
698
+ curl -s --cert client.p12:password \
699
+ -X POST -F "file=@index.html" -F "file=@style.css" \
700
+ https://203.0.113.42:9292/api/sites/<uuid>/files | jq
701
+
702
+ # List files
703
+ curl -s --cert client.p12:password \
704
+ "https://203.0.113.42:9292/api/sites/<uuid>/files" | jq
705
+
706
+ # Delete a file
707
+ curl -s --cert client.p12:password \
708
+ -X DELETE -H "Content-Type: application/json" \
709
+ -d '{"path":"old-page.html"}' \
710
+ "https://203.0.113.42:9292/api/sites/<uuid>/files" | jq
711
+
712
+ # Delete site
713
+ curl -s --cert client.p12:password \
714
+ -X DELETE \
715
+ https://203.0.113.42:9292/api/sites/<uuid> | jq
716
+ ```