@lamalibre/create-lamaste 4.4.1 → 4.6.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 (170) hide show
  1. package/package.json +2 -2
  2. package/scripts/lamaste-priv +234 -18
  3. package/src/index.js +1 -0
  4. package/src/lib/backend-cookie.js +65 -0
  5. package/src/lib/ip-panel-vhost.js +208 -0
  6. package/src/lib/service-config.js +5 -3
  7. package/src/tasks/nginx.js +18 -135
  8. package/src/tasks/redeploy.js +39 -1
  9. package/vendor/docs/00-introduction/how-it-works.md +2 -2
  10. package/vendor/docs/00-introduction/quickstart.md +5 -5
  11. package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
  12. package/vendor/docs/01-concepts/authentication.md +1 -1
  13. package/vendor/docs/01-concepts/authorization.md +66 -48
  14. package/vendor/docs/01-concepts/certificates.md +3 -3
  15. package/vendor/docs/01-concepts/mtls.md +2 -2
  16. package/vendor/docs/01-concepts/security-model.md +70 -33
  17. package/vendor/docs/01-concepts/tickets.md +2 -2
  18. package/vendor/docs/01-concepts/tunneling.md +4 -4
  19. package/vendor/docs/02-guides/agent-setup.md +5 -5
  20. package/vendor/docs/02-guides/certificate-management.md +2 -2
  21. package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
  22. package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
  23. package/vendor/docs/02-guides/installation.md +3 -3
  24. package/vendor/docs/02-guides/managing-users.md +113 -25
  25. package/vendor/docs/02-guides/onboarding.md +2 -2
  26. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
  27. package/vendor/docs/02-guides/static-sites.md +4 -0
  28. package/vendor/docs/03-architecture/installer-flow.md +2 -2
  29. package/vendor/docs/03-architecture/installer.md +4 -4
  30. package/vendor/docs/03-architecture/management-flow.md +3 -1
  31. package/vendor/docs/03-architecture/nginx-configuration.md +229 -24
  32. package/vendor/docs/03-architecture/overview.md +13 -17
  33. package/vendor/docs/03-architecture/panel-client.md +6 -6
  34. package/vendor/docs/03-architecture/panel-server.md +3 -2
  35. package/vendor/docs/03-architecture/state-management.md +4 -8
  36. package/vendor/docs/03-architecture/system-overview.md +1 -1
  37. package/vendor/docs/04-api-reference/agent-hosted-plugins.md +378 -34
  38. package/vendor/docs/04-api-reference/app-host.md +3 -3
  39. package/vendor/docs/04-api-reference/gatekeeper.md +78 -5
  40. package/vendor/docs/04-api-reference/lifecycle-hooks.md +14 -12
  41. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  42. package/vendor/docs/04-api-reference/registry-and-oidc.md +3 -3
  43. package/vendor/docs/04-api-reference/sites.md +4 -0
  44. package/vendor/docs/04-api-reference/tunnels.md +87 -47
  45. package/vendor/docs/04-api-reference/users.md +241 -45
  46. package/vendor/docs/05-operations/backup-and-restore.md +35 -26
  47. package/vendor/docs/05-operations/monitoring.md +39 -0
  48. package/vendor/docs/05-operations/uninstalling.md +2 -1
  49. package/vendor/docs/05-operations/upgrades.md +24 -6
  50. package/vendor/docs/06-reference/config-files.md +100 -202
  51. package/vendor/docs/06-reference/glossary.md +3 -3
  52. package/vendor/docs/06-reference/installer-flags.md +13 -13
  53. package/vendor/docs/06-reference/ports-and-services.md +25 -26
  54. package/vendor/docs/06-reference/troubleshooting.md +8 -8
  55. package/vendor/docs/gatekeeper-design.md +10 -8
  56. package/vendor/docs/identity-directory-design.md +48 -14
  57. package/vendor/docs/package.json +1 -1
  58. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  59. package/vendor/gatekeeper/dist/cli/index.js +0 -0
  60. package/vendor/gatekeeper/dist/index.d.ts +4 -3
  61. package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
  62. package/vendor/gatekeeper/dist/index.js +4 -2
  63. package/vendor/gatekeeper/dist/index.js.map +1 -1
  64. package/vendor/gatekeeper/dist/lib/authz.d.ts +9 -0
  65. package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -1
  66. package/vendor/gatekeeper/dist/lib/authz.js +31 -0
  67. package/vendor/gatekeeper/dist/lib/authz.js.map +1 -1
  68. package/vendor/gatekeeper/dist/lib/constants.d.ts +5 -2
  69. package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -1
  70. package/vendor/gatekeeper/dist/lib/constants.js +5 -2
  71. package/vendor/gatekeeper/dist/lib/constants.js.map +1 -1
  72. package/vendor/gatekeeper/dist/lib/grants.d.ts +6 -0
  73. package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
  74. package/vendor/gatekeeper/dist/lib/grants.js +6 -1
  75. package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
  76. package/vendor/gatekeeper/dist/lib/groups.d.ts +5 -0
  77. package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -1
  78. package/vendor/gatekeeper/dist/lib/groups.js +5 -1
  79. package/vendor/gatekeeper/dist/lib/groups.js.map +1 -1
  80. package/vendor/gatekeeper/dist/lib/principals.d.ts +29 -0
  81. package/vendor/gatekeeper/dist/lib/principals.d.ts.map +1 -0
  82. package/vendor/gatekeeper/dist/lib/principals.js +67 -0
  83. package/vendor/gatekeeper/dist/lib/principals.js.map +1 -0
  84. package/vendor/gatekeeper/dist/lib/tunnels.d.ts +10 -0
  85. package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -0
  86. package/vendor/gatekeeper/dist/lib/tunnels.js +56 -0
  87. package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -0
  88. package/vendor/gatekeeper/dist/server/index.d.ts +2 -1
  89. package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
  90. package/vendor/gatekeeper/dist/server/index.js +49 -56
  91. package/vendor/gatekeeper/dist/server/index.js.map +1 -1
  92. package/vendor/gatekeeper/dist/server/routes/authz.d.ts +15 -1
  93. package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
  94. package/vendor/gatekeeper/dist/server/routes/authz.js +332 -104
  95. package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
  96. package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
  97. package/vendor/gatekeeper/dist/server/routes/diagnostic.js +61 -4
  98. package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
  99. package/vendor/gatekeeper/dist/server/routes/principals.d.ts +3 -0
  100. package/vendor/gatekeeper/dist/server/routes/principals.d.ts.map +1 -0
  101. package/vendor/gatekeeper/dist/server/routes/principals.js +27 -0
  102. package/vendor/gatekeeper/dist/server/routes/principals.js.map +1 -0
  103. package/vendor/gatekeeper/package.json +2 -2
  104. package/vendor/server/bin/lamaste-server.js +15 -1
  105. package/vendor/server/package.json +2 -2
  106. package/vendor/server/src/commands/plugin-identity.js +335 -0
  107. package/vendor/server/src/commands/plugin-web-surface.js +269 -0
  108. package/vendor/server/src/commands/plugins.js +156 -11
  109. package/vendor/server/src/commands/registry-credential.js +2 -1
  110. package/vendor/server/src/commands/sites.js +15 -1
  111. package/vendor/server/src/commands/tunnels.js +102 -2
  112. package/vendor/server/src/commands/uninstall.js +3 -1
  113. package/vendor/server/src/commands/users.js +358 -0
  114. package/vendor/server/src/plugin-approval.js +186 -5
  115. package/vendor/server/src/plugin-identity.js +382 -0
  116. package/vendor/server/src/plugin-web-surface.js +307 -0
  117. package/vendor/server-ui/dist/assets/index-BZwQ_aQc.css +1 -0
  118. package/vendor/server-ui/dist/assets/index-BvEfCZYS.js +84 -0
  119. package/vendor/server-ui/dist/index.html +2 -2
  120. package/vendor/serverd/package.json +2 -2
  121. package/vendor/serverd/src/index.js +34 -5
  122. package/vendor/serverd/src/lib/audience.js +576 -0
  123. package/vendor/serverd/src/lib/authelia.js +78 -7
  124. package/vendor/serverd/src/lib/chisel-runtime.js +18 -6
  125. package/vendor/serverd/src/lib/chisel.js +10 -2
  126. package/vendor/serverd/src/lib/config.js +68 -43
  127. package/vendor/serverd/src/lib/directory.js +1113 -0
  128. package/vendor/serverd/src/lib/entitlements.js +517 -0
  129. package/vendor/serverd/src/lib/gatekeeper-client.js +226 -10
  130. package/vendor/serverd/src/lib/hook-pipelines.js +21 -3
  131. package/vendor/serverd/src/lib/identity-sync.js +905 -0
  132. package/vendor/serverd/src/lib/migrations/state/0013_identity_directory.sql +158 -0
  133. package/vendor/serverd/src/lib/migrations/state/0014_identity_ledger.sql +20 -0
  134. package/vendor/serverd/src/lib/migrations/state/0015_plugin_web_surface.sql +14 -0
  135. package/vendor/serverd/src/lib/mtls.js +6 -2
  136. package/vendor/serverd/src/lib/nginx.js +546 -48
  137. package/vendor/serverd/src/lib/plugin-hooks.js +127 -45
  138. package/vendor/serverd/src/lib/plugin-hosting.js +33 -5
  139. package/vendor/serverd/src/lib/plugin-lifecycle.js +227 -13
  140. package/vendor/serverd/src/lib/plugin-packages.js +74 -2
  141. package/vendor/serverd/src/lib/plugins.js +95 -7
  142. package/vendor/serverd/src/lib/registry-credential.js +9 -6
  143. package/vendor/serverd/src/lib/request-logging.js +19 -0
  144. package/vendor/serverd/src/lib/retained-data.js +50 -4
  145. package/vendor/serverd/src/lib/state.js +36 -0
  146. package/vendor/serverd/src/lib/tunnel-hooks.js +4 -1
  147. package/vendor/serverd/src/lib/user-access-session.js +21 -11
  148. package/vendor/serverd/src/lib/user-caller.js +67 -0
  149. package/vendor/serverd/src/lib/user-surface.js +754 -0
  150. package/vendor/serverd/src/lib/vhost-reconcile.js +137 -33
  151. package/vendor/serverd/src/lib/web-surface.js +766 -0
  152. package/vendor/serverd/src/middleware/user-access-session.js +9 -0
  153. package/vendor/serverd/src/routes/agent-forwarder.js +4 -2
  154. package/vendor/serverd/src/routes/invite.js +53 -12
  155. package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +107 -16
  156. package/vendor/serverd/src/routes/management/identity-directory.js +79 -0
  157. package/vendor/serverd/src/routes/management/invitations.js +78 -68
  158. package/vendor/serverd/src/routes/management/plugin-identity.js +221 -0
  159. package/vendor/serverd/src/routes/management/plugin-web-surface.js +151 -0
  160. package/vendor/serverd/src/routes/management/plugins.js +2 -0
  161. package/vendor/serverd/src/routes/management/registry-credential.js +1 -1
  162. package/vendor/serverd/src/routes/management/sites.js +143 -65
  163. package/vendor/serverd/src/routes/management/tunnels.js +109 -22
  164. package/vendor/serverd/src/routes/management/users.js +290 -208
  165. package/vendor/serverd/src/routes/management.js +6 -0
  166. package/vendor/serverd/src/routes/onboarding/provision.js +14 -1
  167. package/vendor/serverd/src/routes/user-access.js +17 -4
  168. package/vendor/serverd/src/routes/user-surface.js +69 -0
  169. package/vendor/server-ui/dist/assets/index-25wo_8av.css +0 -1
  170. package/vendor/server-ui/dist/assets/index-roDHz3dM.js +0 -84
@@ -13,7 +13,11 @@
13
13
  * Authelia-protected static site and of the auth portal named the client's
14
14
  * `Host` header rather than the vhost's own hostname, so a request line for
15
15
  * one hostname sent with the `Host` of another was authorized for the other
16
- * (see `originalUrl` in nginx.js).
16
+ * (see `originalUrl` in nginx.js);
17
+ * - from 4.5 the panel serves plugins' user surfaces to a relay assertion
18
+ * Gatekeeper signs, so every vhost that proxies a browser to the panel —
19
+ * the panel's own and the auth portal — clears one a client sent, and so
20
+ * does every tunnel vhost that does not set Gatekeeper's.
17
21
  *
18
22
  * Each installed vhost is rendered again from state, on the certificate
19
23
  * directory the installed file names (a wildcard certificate stays a wildcard
@@ -23,15 +27,28 @@
23
27
  * A rewrite never adds or removes authentication: a vhost that disagrees with
24
28
  * its state on that is reported and left alone. The one exception is the
25
29
  * agent-panel vhost, whose mTLS check gains the panel's `auth_request`.
30
+ *
31
+ * A tunnel bound as plugins' user surface is rendered with the locations of
32
+ * the routes it serves now (`servedUserSurfaceRoutes`: identity declared and
33
+ * approved, and the tunnel's agent hosting the plugin); the same reconcile,
34
+ * limited to the tunnels a route is bound to, follows the plugin's identity
35
+ * through approval, update and uninstall, its hosting, and the tunnels'
36
+ * agents ({@link reconcileUserSurfaceVhosts}). Every location proxying to a
37
+ * tunnel or agent backend keeps Authelia's session cookie from it
38
+ * (`$lamaste_backend_cookie`), so an installed vhost from before that is
39
+ * rewritten too.
26
40
  */
27
41
 
42
+ import { tunnelUserSurfaceRoutes } from '@lamalibre/lamaste';
28
43
  import { withTunnelLock } from '@lamalibre/lamaste/server';
29
44
  import {
45
+ PANEL_DOMAIN_VHOST,
30
46
  installVhost,
31
47
  isSiteEnabled,
32
48
  readInstalledVhost,
33
49
  renderAgentPanelVhost,
34
50
  renderAuthVhost,
51
+ renderPanelVhost,
35
52
  renderStaticSiteVhost,
36
53
  renderTunnelVhost,
37
54
  vhostCertDir,
@@ -43,15 +60,30 @@ const SITE_NAME_PART_RE = /^[A-Za-z0-9][A-Za-z0-9-]{0,62}$/;
43
60
 
44
61
  const AUTH_VHOST = 'lamalibre-lamaste-auth';
45
62
 
63
+ /**
64
+ * Which of a tunnel's bound routes its vhost serves now — the panel's
65
+ * plugins and their hostings decide (`lib/user-surface.js`), loaded when
66
+ * first needed so this module stays free of the plugin registry.
67
+ *
68
+ * @param {readonly string[]} routes
69
+ * @param {{ agentLabel?: string }} tunnel
70
+ */
71
+ async function servedUserSurface(routes, tunnel) {
72
+ const { servedUserSurfaceRoutes } = await import('./user-surface.js');
73
+ return servedUserSurfaceRoutes(routes, tunnel);
74
+ }
75
+
46
76
  /**
47
77
  * The vhost a tunnel should have, rendered on `certDir`; null for tunnels
48
78
  * without one (internal plugin tunnels) or whose state cannot be rendered.
49
79
  *
50
80
  * @param {object} tunnel
51
81
  * @param {string} certDir
82
+ * @param {ReadonlyArray<{ route: string, maxBodySizeMb: number }>} [userSurface] - the user
83
+ * surfaces its vhost serves (`servedUserSurfaceRoutes` of its bound routes)
52
84
  * @returns {{ name: string, fqdn: string, config: string } | null}
53
85
  */
54
- export function expectedTunnelVhost(tunnel, certDir) {
86
+ export function expectedTunnelVhost(tunnel, certDir, userSurface = []) {
55
87
  if (!managedTunnelVhostName(tunnel) || typeof tunnel.fqdn !== 'string') return null;
56
88
  if (!tunnel.fqdn.startsWith(`${tunnel.subdomain}.`)) return null;
57
89
  const domain = tunnel.fqdn.slice(tunnel.subdomain.length + 1);
@@ -60,18 +92,16 @@ export function expectedTunnelVhost(tunnel, certDir) {
60
92
  }
61
93
  // As createTunnel and updateTunnel write it: the stored access mode
62
94
  // (restricted when absent, as Gatekeeper reads it), the plugin route of a
63
- // plugin tunnel, and a body limit only when the tunnel has one.
64
- return renderTunnelVhost(
65
- tunnel.accessMode ?? 'restricted',
66
- tunnel.subdomain,
67
- domain,
68
- tunnel.port,
69
- certDir,
70
- {
71
- ...(tunnel.type === 'plugin' && tunnel.pluginRoute ? { pathPrefix: tunnel.pluginRoute } : {}),
72
- ...(tunnel.maxBodySizeMb !== undefined ? { maxBodySizeMb: tunnel.maxBodySizeMb } : {}),
73
- },
74
- );
95
+ // plugin tunnel, a body limit only when the tunnel has one, and the user
96
+ // surfaces it serves (never on a public vhost: such state cannot be
97
+ // rendered, and a bound tunnel cannot be made public).
98
+ const accessMode = tunnel.accessMode ?? 'restricted';
99
+ if (accessMode === 'public' && userSurface.length > 0) return null;
100
+ return renderTunnelVhost(accessMode, tunnel.subdomain, domain, tunnel.port, certDir, {
101
+ ...(tunnel.type === 'plugin' && tunnel.pluginRoute ? { pathPrefix: tunnel.pluginRoute } : {}),
102
+ ...(tunnel.maxBodySizeMb !== undefined ? { maxBodySizeMb: tunnel.maxBodySizeMb } : {}),
103
+ ...(userSurface.length > 0 ? { userSurface } : {}),
104
+ });
75
105
  }
76
106
 
77
107
  /** Site name of a tunnel's vhost, or null for a tunnel without one. */
@@ -168,7 +198,45 @@ function logResult(log, what, result) {
168
198
  }
169
199
  }
170
200
 
171
- const defaultTunnelDeps = { readTunnels, readInstalledVhost, installVhost, isSiteEnabled };
201
+ const defaultTunnelDeps = {
202
+ readTunnels,
203
+ readInstalledVhost,
204
+ installVhost,
205
+ isSiteEnabled,
206
+ servedUserSurface,
207
+ };
208
+
209
+ /**
210
+ * Reconcile one tunnel's vhost (see {@link reconcileOne}), with the user
211
+ * surfaces it serves now. The caller holds the tunnel lock.
212
+ */
213
+ async function reconcileTunnel(tunnel, result, deps) {
214
+ const name = managedTunnelVhostName(tunnel);
215
+ if (!name) return;
216
+ const label = tunnel.fqdn ?? tunnel.subdomain;
217
+ const bound = tunnelUserSurfaceRoutes(tunnel);
218
+ let served;
219
+ try {
220
+ served = bound.length > 0 ? await deps.servedUserSurface(bound, tunnel) : [];
221
+ } catch (err) {
222
+ result.failed.push({ vhost: label, error: err instanceof Error ? err.message : String(err) });
223
+ return;
224
+ }
225
+ await reconcileOne(
226
+ {
227
+ name,
228
+ label,
229
+ render: (certDir) => expectedTunnelVhost(tunnel, certDir, served),
230
+ // Linked only when it is now and the tunnel is enabled: a rewrite of
231
+ // the text never publishes a vhost someone unlinked, and unlinks a
232
+ // disabled tunnel's.
233
+ enabled: async () => tunnel.enabled !== false && (await deps.isSiteEnabled(name)),
234
+ keepAuthentication: tunnel.type !== 'panel',
235
+ },
236
+ result,
237
+ deps,
238
+ );
239
+ }
172
240
 
173
241
  /**
174
242
  * Reconcile the vhost of every tunnel that has one: agent-panel, app and
@@ -184,33 +252,55 @@ export async function reconcileTunnelVhosts(log, deps = defaultTunnelDeps) {
184
252
  return withTunnelLock(async () => {
185
253
  const result = newResult();
186
254
  for (const tunnel of await deps.readTunnels()) {
187
- const name = managedTunnelVhostName(tunnel);
188
- if (!name) continue;
189
- await reconcileOne(
190
- {
191
- name,
192
- label: tunnel.fqdn ?? tunnel.subdomain,
193
- render: (certDir) => expectedTunnelVhost(tunnel, certDir),
194
- // Linked only when it is now and the tunnel is enabled: a rewrite of
195
- // the text never publishes a vhost someone unlinked, and unlinks a
196
- // disabled tunnel's.
197
- enabled: async () => tunnel.enabled !== false && (await deps.isSiteEnabled(name)),
198
- keepAuthentication: tunnel.type !== 'panel',
199
- },
200
- result,
201
- deps,
202
- );
255
+ await reconcileTunnel(tunnel, result, deps);
203
256
  }
204
257
  logResult(log, 'tunnel', result);
205
258
  return result;
206
259
  });
207
260
  }
208
261
 
262
+ /**
263
+ * Reconcile the vhosts of the tunnels `route` is bound to as a user
264
+ * surface — after a change that decides whether they serve it: the
265
+ * plugin's installation, the approval of its version, an update or its
266
+ * reversal, its hosting. `ids` names tunnels to reconcile besides (the
267
+ * route's binding was just released from them, or their agent was
268
+ * revoked — `route` null then). Under the tunnel lock.
269
+ *
270
+ * @param {Parameters<typeof logResult>[0]} log
271
+ * @param {string | null} route
272
+ * @param {{ ids?: readonly string[] }} [opts]
273
+ * @param {typeof defaultTunnelDeps} [deps]
274
+ * @returns {Promise<ReturnType<typeof newResult>>}
275
+ */
276
+ export async function reconcileUserSurfaceVhosts(
277
+ log,
278
+ route,
279
+ { ids = [] } = {},
280
+ deps = defaultTunnelDeps,
281
+ ) {
282
+ return withTunnelLock(async () => {
283
+ const result = newResult();
284
+ for (const tunnel of await deps.readTunnels()) {
285
+ if (
286
+ !ids.includes(tunnel.id) &&
287
+ (route === null || !tunnelUserSurfaceRoutes(tunnel).includes(route))
288
+ ) {
289
+ continue;
290
+ }
291
+ await reconcileTunnel(tunnel, result, deps);
292
+ }
293
+ logResult(log, 'user-surface tunnel', result);
294
+ return result;
295
+ });
296
+ }
297
+
209
298
  const defaultSiteDeps = { readSites, readInstalledVhost, installVhost, isSiteEnabled };
210
299
 
211
300
  /**
212
- * Reconcile the vhosts that ask Authelia directly: the auth portal
213
- * (`auth.<domain>`) and every static site. Site changes take no lock, so call
301
+ * Reconcile the vhosts that proxy a browser to the panel or ask Authelia
302
+ * directly: the panel's own (`panel.<domain>`), the auth portal
303
+ * (`auth.<domain>`) and every static site but a plugin's web surface. Call
214
304
  * it before the panel listens — then nothing can change a site meanwhile.
215
305
  *
216
306
  * @param {Parameters<typeof logResult>[0]} log
@@ -221,6 +311,17 @@ const defaultSiteDeps = { readSites, readInstalledVhost, installVhost, isSiteEna
221
311
  export async function reconcileSiteVhosts(log, domain, deps = defaultSiteDeps) {
222
312
  const result = newResult();
223
313
 
314
+ await reconcileOne(
315
+ {
316
+ name: PANEL_DOMAIN_VHOST,
317
+ label: `panel.${domain}`,
318
+ render: (certDir) => renderPanelVhost(domain, certDir),
319
+ enabled: () => deps.isSiteEnabled(PANEL_DOMAIN_VHOST),
320
+ },
321
+ result,
322
+ deps,
323
+ );
324
+
224
325
  await reconcileOne(
225
326
  {
226
327
  name: AUTH_VHOST,
@@ -240,6 +341,9 @@ export async function reconcileSiteVhosts(log, domain, deps = defaultSiteDeps) {
240
341
  }
241
342
  for (const site of sites) {
242
343
  if (typeof site.id !== 'string' || !SITE_NAME_PART_RE.test(site.id)) continue;
344
+ // A plugin's web surface is its plugin's to render (lib/web-surface.js
345
+ // `reconcileWebSurfaces`, once plugins and hostings can be read).
346
+ if (site.owner) continue;
243
347
  const name = `lamalibre-lamaste-site-${site.id}`;
244
348
  await reconcileOne(
245
349
  {