@lamalibre/create-lamaste 4.5.0 → 4.7.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 (178) hide show
  1. package/package.json +2 -2
  2. package/scripts/bundle-vendor.js +20 -3
  3. package/scripts/lamaste-priv +56 -5
  4. package/scripts/pin-release.js +98 -0
  5. package/src/index.js +8 -1
  6. package/src/lib/env.js +19 -0
  7. package/src/lib/service-config.js +7 -0
  8. package/src/tasks/redeploy.js +8 -0
  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 +6 -2
  13. package/vendor/docs/01-concepts/authorization.md +4 -0
  14. package/vendor/docs/01-concepts/certificates.md +3 -3
  15. package/vendor/docs/01-concepts/dns-and-domains.md +9 -0
  16. package/vendor/docs/01-concepts/mtls.md +2 -2
  17. package/vendor/docs/01-concepts/security-model.md +28 -3
  18. package/vendor/docs/01-concepts/tickets.md +2 -2
  19. package/vendor/docs/01-concepts/tunneling.md +4 -4
  20. package/vendor/docs/02-guides/agent-setup.md +5 -5
  21. package/vendor/docs/02-guides/certificate-management.md +2 -2
  22. package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
  23. package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
  24. package/vendor/docs/02-guides/installation.md +3 -3
  25. package/vendor/docs/02-guides/managing-users.md +5 -11
  26. package/vendor/docs/02-guides/onboarding.md +2 -2
  27. package/vendor/docs/02-guides/second-domain.md +82 -0
  28. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
  29. package/vendor/docs/02-guides/static-sites.md +4 -0
  30. package/vendor/docs/03-architecture/installer-flow.md +2 -2
  31. package/vendor/docs/03-architecture/installer.md +4 -4
  32. package/vendor/docs/03-architecture/management-flow.md +1 -1
  33. package/vendor/docs/03-architecture/nginx-configuration.md +80 -12
  34. package/vendor/docs/03-architecture/overview.md +5 -5
  35. package/vendor/docs/03-architecture/panel-client.md +6 -6
  36. package/vendor/docs/03-architecture/panel-server.md +2 -2
  37. package/vendor/docs/03-architecture/state-management.md +3 -3
  38. package/vendor/docs/03-architecture/system-overview.md +1 -1
  39. package/vendor/docs/04-api-reference/agent-hosted-plugins.md +116 -37
  40. package/vendor/docs/04-api-reference/app-host.md +3 -3
  41. package/vendor/docs/04-api-reference/domains.md +192 -0
  42. package/vendor/docs/04-api-reference/gatekeeper.md +54 -8
  43. package/vendor/docs/04-api-reference/lifecycle-hooks.md +4 -4
  44. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  45. package/vendor/docs/04-api-reference/overview.md +9 -0
  46. package/vendor/docs/04-api-reference/registry-and-oidc.md +26 -13
  47. package/vendor/docs/04-api-reference/sites.md +9 -0
  48. package/vendor/docs/04-api-reference/tunnels.md +35 -28
  49. package/vendor/docs/04-api-reference/users.md +41 -29
  50. package/vendor/docs/05-operations/backup-and-restore.md +1 -1
  51. package/vendor/docs/05-operations/uninstalling.md +1 -1
  52. package/vendor/docs/05-operations/upgrades.md +15 -6
  53. package/vendor/docs/06-reference/config-files.md +19 -15
  54. package/vendor/docs/06-reference/glossary.md +3 -3
  55. package/vendor/docs/06-reference/installer-flags.md +13 -13
  56. package/vendor/docs/06-reference/ports-and-services.md +1 -1
  57. package/vendor/docs/06-reference/troubleshooting.md +8 -8
  58. package/vendor/docs/_index.json +105 -19
  59. package/vendor/docs/external-members-design.md +816 -0
  60. package/vendor/docs/gatekeeper-design.md +8 -8
  61. package/vendor/docs/identity-directory-design.md +3 -3
  62. package/vendor/docs/multi-domain-relay-design.md +836 -0
  63. package/vendor/docs/package.json +1 -1
  64. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  65. package/vendor/gatekeeper/dist/index.d.ts +3 -3
  66. package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
  67. package/vendor/gatekeeper/dist/index.js +2 -2
  68. package/vendor/gatekeeper/dist/index.js.map +1 -1
  69. package/vendor/gatekeeper/dist/lib/authz.d.ts +9 -0
  70. package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -1
  71. package/vendor/gatekeeper/dist/lib/authz.js +31 -0
  72. package/vendor/gatekeeper/dist/lib/authz.js.map +1 -1
  73. package/vendor/gatekeeper/dist/lib/constants.d.ts +5 -2
  74. package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -1
  75. package/vendor/gatekeeper/dist/lib/constants.js +5 -2
  76. package/vendor/gatekeeper/dist/lib/constants.js.map +1 -1
  77. package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
  78. package/vendor/gatekeeper/dist/lib/grants.js +20 -13
  79. package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
  80. package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0004_unconsume_access_grants.sql +8 -0
  81. package/vendor/gatekeeper/dist/lib/secret.d.ts +6 -0
  82. package/vendor/gatekeeper/dist/lib/secret.d.ts.map +1 -0
  83. package/vendor/gatekeeper/dist/lib/secret.js +12 -0
  84. package/vendor/gatekeeper/dist/lib/secret.js.map +1 -0
  85. package/vendor/gatekeeper/dist/lib/templates.d.ts +20 -0
  86. package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -1
  87. package/vendor/gatekeeper/dist/lib/templates.js +80 -14
  88. package/vendor/gatekeeper/dist/lib/templates.js.map +1 -1
  89. package/vendor/gatekeeper/dist/lib/tunnels.d.ts +3 -6
  90. package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -1
  91. package/vendor/gatekeeper/dist/lib/tunnels.js +4 -16
  92. package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -1
  93. package/vendor/gatekeeper/dist/lib/types.d.ts +0 -19
  94. package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
  95. package/vendor/gatekeeper/dist/server/index.d.ts +6 -1
  96. package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
  97. package/vendor/gatekeeper/dist/server/index.js +56 -11
  98. package/vendor/gatekeeper/dist/server/index.js.map +1 -1
  99. package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
  100. package/vendor/gatekeeper/dist/server/routes/authz.js +325 -55
  101. package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
  102. package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
  103. package/vendor/gatekeeper/dist/server/routes/diagnostic.js +5 -2
  104. package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
  105. package/vendor/gatekeeper/package.json +2 -2
  106. package/vendor/server/bin/lamaste-server.js +7 -0
  107. package/vendor/server/package.json +2 -2
  108. package/vendor/server/src/commands/domains.js +214 -0
  109. package/vendor/server/src/commands/plugin-identity.js +2 -16
  110. package/vendor/server/src/commands/plugin-web-surface.js +269 -0
  111. package/vendor/server/src/commands/plugins.js +175 -14
  112. package/vendor/server/src/commands/registry-credential.js +10 -3
  113. package/vendor/server/src/commands/sites.js +18 -1
  114. package/vendor/server/src/commands/status.js +19 -0
  115. package/vendor/server/src/commands/tunnels.js +4 -1
  116. package/vendor/server/src/commands/users.js +8 -19
  117. package/vendor/server/src/domains.js +92 -0
  118. package/vendor/server/src/fail.js +21 -0
  119. package/vendor/server/src/oidc-client-grants.js +63 -0
  120. package/vendor/server/src/panel-api.js +15 -0
  121. package/vendor/server/src/plugin-approval.js +80 -1
  122. package/vendor/server/src/plugin-web-surface.js +307 -0
  123. package/vendor/server-ui/dist/assets/index-B8Tv_ocF.js +84 -0
  124. package/vendor/server-ui/dist/assets/index-Cb-ADsbB.css +1 -0
  125. package/vendor/server-ui/dist/index.html +2 -2
  126. package/vendor/serverd/package.json +2 -2
  127. package/vendor/serverd/src/index.js +50 -19
  128. package/vendor/serverd/src/lib/app-error.js +24 -0
  129. package/vendor/serverd/src/lib/audience.js +12 -3
  130. package/vendor/serverd/src/lib/authelia.js +341 -35
  131. package/vendor/serverd/src/lib/chisel-runtime.js +18 -6
  132. package/vendor/serverd/src/lib/chisel.js +10 -2
  133. package/vendor/serverd/src/lib/directory.js +67 -52
  134. package/vendor/serverd/src/lib/domains.js +1792 -0
  135. package/vendor/serverd/src/lib/gatekeeper-client.js +34 -7
  136. package/vendor/serverd/src/lib/hook-pipelines.js +8 -3
  137. package/vendor/serverd/src/lib/identity-sync.js +35 -0
  138. package/vendor/serverd/src/lib/migrations/state/0015_plugin_web_surface.sql +14 -0
  139. package/vendor/serverd/src/lib/migrations/state/0016_domains.sql +53 -0
  140. package/vendor/serverd/src/lib/migrations/state/0017_membership_kind.sql +15 -0
  141. package/vendor/serverd/src/lib/migrations/state/0018_domain_group_keys.sql +23 -0
  142. package/vendor/serverd/src/lib/nginx.js +576 -131
  143. package/vendor/serverd/src/lib/owned-groups.js +326 -0
  144. package/vendor/serverd/src/lib/plugin-domain.js +86 -0
  145. package/vendor/serverd/src/lib/plugin-hooks.js +40 -11
  146. package/vendor/serverd/src/lib/plugin-lifecycle.js +129 -5
  147. package/vendor/serverd/src/lib/plugin-oidc.js +144 -42
  148. package/vendor/serverd/src/lib/plugin-packages.js +74 -2
  149. package/vendor/serverd/src/lib/plugins.js +82 -9
  150. package/vendor/serverd/src/lib/registry-credential.js +39 -6
  151. package/vendor/serverd/src/lib/services.js +9 -1
  152. package/vendor/serverd/src/lib/state.js +29 -0
  153. package/vendor/serverd/src/lib/tunnel-deps.js +2 -4
  154. package/vendor/serverd/src/lib/user-surface.js +77 -31
  155. package/vendor/serverd/src/lib/users-lock.js +23 -0
  156. package/vendor/serverd/src/lib/vhost-reconcile.js +151 -41
  157. package/vendor/serverd/src/lib/web-surface.js +783 -0
  158. package/vendor/serverd/src/middleware/user-access-session.js +11 -4
  159. package/vendor/serverd/src/routes/invite.js +15 -5
  160. package/vendor/serverd/src/routes/management/domains.js +154 -0
  161. package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +44 -8
  162. package/vendor/serverd/src/routes/management/identity.js +21 -7
  163. package/vendor/serverd/src/routes/management/invitations.js +33 -4
  164. package/vendor/serverd/src/routes/management/plugin-web-surface.js +151 -0
  165. package/vendor/serverd/src/routes/management/plugins.js +6 -2
  166. package/vendor/serverd/src/routes/management/registry-credential.js +86 -57
  167. package/vendor/serverd/src/routes/management/sites.js +188 -71
  168. package/vendor/serverd/src/routes/management/tunnels.js +37 -22
  169. package/vendor/serverd/src/routes/management/users.js +184 -64
  170. package/vendor/serverd/src/routes/management.js +4 -0
  171. package/vendor/serverd/src/routes/onboarding/dns.js +3 -17
  172. package/vendor/serverd/src/routes/onboarding/provision.js +22 -13
  173. package/vendor/serverd/src/routes/user-access.js +13 -1
  174. package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +0 -7
  175. package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +0 -14
  176. package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +0 -9
  177. package/vendor/server-ui/dist/assets/index-D-fer5ef.css +0 -1
  178. package/vendor/server-ui/dist/assets/index-DSaDPJaw.js +0 -84
@@ -37,15 +37,27 @@
37
37
  * tunnel or agent backend keeps Authelia's session cookie from it
38
38
  * (`$lamaste_backend_cookie`), so an installed vhost from before that is
39
39
  * rewritten too.
40
+ *
41
+ * From the multi-domain relay, tunnel vhosts are named by their full
42
+ * hostname and every domain has its own auth portal
43
+ * (`lamalibre-lamaste-auth-<domain>`): {@link renameLegacyVhosts} renames
44
+ * the ones an older version named by subdomain, in one reload, before the
45
+ * reconcile renders them.
40
46
  */
41
47
 
42
- import { tunnelUserSurfaceRoutes } from '@lamalibre/lamaste';
43
- import { withTunnelLock } from '@lamalibre/lamaste/server';
48
+ import { domainOfHost, tunnelUserSurfaceRoutes } from '@lamalibre/lamaste';
49
+ import { tunnelHost, vhostTunnelHost, withTunnelLock } from '@lamalibre/lamaste/server';
44
50
  import {
51
+ LEGACY_AUTH_VHOST,
45
52
  PANEL_DOMAIN_VHOST,
53
+ agentPanelVhostName,
54
+ appVhostName,
55
+ authVhostName,
46
56
  installVhost,
47
57
  isSiteEnabled,
58
+ legacyTunnelVhostName,
48
59
  readInstalledVhost,
60
+ renameVhosts,
49
61
  renderAgentPanelVhost,
50
62
  renderAuthVhost,
51
63
  renderPanelVhost,
@@ -53,13 +65,12 @@ import {
53
65
  renderTunnelVhost,
54
66
  vhostCertDir,
55
67
  } from './nginx.js';
68
+ import { notifyAdminsBestEffort } from './notifications.js';
56
69
  import { readSites, readTunnels } from './state.js';
57
70
 
58
71
  /** A subdomain or site id as it may appear in a site name. */
59
72
  const SITE_NAME_PART_RE = /^[A-Za-z0-9][A-Za-z0-9-]{0,62}$/;
60
73
 
61
- const AUTH_VHOST = 'lamalibre-lamaste-auth';
62
-
63
74
  /**
64
75
  * Which of a tunnel's bound routes its vhost serves now — the panel's
65
76
  * plugins and their hostings decide (`lib/user-surface.js`), loaded when
@@ -84,11 +95,10 @@ async function servedUserSurface(routes, tunnel) {
84
95
  * @returns {{ name: string, fqdn: string, config: string } | null}
85
96
  */
86
97
  export function expectedTunnelVhost(tunnel, certDir, userSurface = []) {
87
- if (!managedTunnelVhostName(tunnel) || typeof tunnel.fqdn !== 'string') return null;
88
- if (!tunnel.fqdn.startsWith(`${tunnel.subdomain}.`)) return null;
89
- const domain = tunnel.fqdn.slice(tunnel.subdomain.length + 1);
98
+ if (!managedTunnelVhostName(tunnel)) return null;
99
+ const { fqdn, domain } = tunnelHost(tunnel);
90
100
  if (tunnel.type === 'panel') {
91
- return renderAgentPanelVhost(tunnel.subdomain, domain, tunnel.port, certDir);
101
+ return renderAgentPanelVhost(fqdn, domain, tunnel.port, certDir);
92
102
  }
93
103
  // As createTunnel and updateTunnel write it: the stored access mode
94
104
  // (restricted when absent, as Gatekeeper reads it), the plugin route of a
@@ -97,21 +107,27 @@ export function expectedTunnelVhost(tunnel, certDir, userSurface = []) {
97
107
  // rendered, and a bound tunnel cannot be made public).
98
108
  const accessMode = tunnel.accessMode ?? 'restricted';
99
109
  if (accessMode === 'public' && userSurface.length > 0) return null;
100
- return renderTunnelVhost(accessMode, tunnel.subdomain, domain, tunnel.port, certDir, {
110
+ return renderTunnelVhost(accessMode, fqdn, domain, tunnel.port, certDir, {
101
111
  ...(tunnel.type === 'plugin' && tunnel.pluginRoute ? { pathPrefix: tunnel.pluginRoute } : {}),
102
112
  ...(tunnel.maxBodySizeMb !== undefined ? { maxBodySizeMb: tunnel.maxBodySizeMb } : {}),
103
113
  ...(userSurface.length > 0 ? { userSurface } : {}),
104
114
  });
105
115
  }
106
116
 
107
- /** Site name of a tunnel's vhost, or null for a tunnel without one. */
117
+ /**
118
+ * Site name of a tunnel's vhost — by its hostname — or null for a tunnel
119
+ * without one (internal, or whose state names no `<subdomain>.<domain>`).
120
+ */
108
121
  function managedTunnelVhostName(tunnel) {
109
- if (tunnel.exposure === 'internal' || typeof tunnel.subdomain !== 'string') return null;
110
- if (!SITE_NAME_PART_RE.test(tunnel.subdomain)) return null;
111
- if (tunnel.type === 'panel') return `lamalibre-lamaste-agent-panel-${tunnel.subdomain}`;
122
+ if (typeof tunnel.subdomain !== 'string' || !SITE_NAME_PART_RE.test(tunnel.subdomain)) {
123
+ return null;
124
+ }
125
+ const host = vhostTunnelHost(tunnel);
126
+ if (host === null) return null;
127
+ if (tunnel.type === 'panel') return agentPanelVhostName(host.fqdn);
112
128
  // Tunnels persisted before `type` existed are app tunnels.
113
129
  if (tunnel.type === 'app' || tunnel.type === 'plugin' || tunnel.type === undefined) {
114
- return `lamalibre-lamaste-app-${tunnel.subdomain}`;
130
+ return appVhostName(host.fqdn);
115
131
  }
116
132
  return null;
117
133
  }
@@ -295,44 +311,58 @@ export async function reconcileUserSurfaceVhosts(
295
311
  });
296
312
  }
297
313
 
298
- const defaultSiteDeps = { readSites, readInstalledVhost, installVhost, isSiteEnabled };
314
+ const defaultSiteDeps = {
315
+ readSites,
316
+ readInstalledVhost,
317
+ installVhost,
318
+ isSiteEnabled,
319
+ notify: notifyAdminsBestEffort,
320
+ };
299
321
 
300
322
  /**
301
323
  * 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. Site changes take no lock, so
304
- * call it before the panel listens — then nothing can change a site
324
+ * directly: the panel's own (`panel.<primary>`), every active domain's auth
325
+ * portal (`auth.<domain>`) and every static site but a plugin's web surface.
326
+ * Call it before the panel listens — then nothing can change a site
305
327
  * meanwhile.
306
328
  *
307
329
  * @param {Parameters<typeof logResult>[0]} log
308
- * @param {string} domain
330
+ * @param {ReadonlyArray<{ name: string, primary: boolean }>} domains the active domains
309
331
  * @param {typeof defaultSiteDeps} [deps]
310
332
  * @returns {Promise<ReturnType<typeof newResult>>}
311
333
  */
312
- export async function reconcileSiteVhosts(log, domain, deps = defaultSiteDeps) {
334
+ export async function reconcileSiteVhosts(log, domains, deps = defaultSiteDeps) {
313
335
  const result = newResult();
336
+ const primary = domains.find((d) => d.primary)?.name;
314
337
 
315
- await reconcileOne(
316
- {
317
- name: PANEL_DOMAIN_VHOST,
318
- label: `panel.${domain}`,
319
- render: (certDir) => renderPanelVhost(domain, certDir),
320
- enabled: () => deps.isSiteEnabled(PANEL_DOMAIN_VHOST),
321
- },
322
- result,
323
- deps,
324
- );
338
+ if (primary) {
339
+ await reconcileOne(
340
+ {
341
+ name: PANEL_DOMAIN_VHOST,
342
+ label: `panel.${primary}`,
343
+ render: (certDir) => renderPanelVhost(primary, certDir),
344
+ enabled: () => deps.isSiteEnabled(PANEL_DOMAIN_VHOST),
345
+ },
346
+ result,
347
+ deps,
348
+ );
349
+ }
325
350
 
326
- await reconcileOne(
327
- {
328
- name: AUTH_VHOST,
329
- label: `auth.${domain}`,
330
- render: (certDir) => renderAuthVhost(domain, certDir),
331
- enabled: () => deps.isSiteEnabled(AUTH_VHOST),
332
- },
333
- result,
334
- deps,
335
- );
351
+ for (const domain of domains) {
352
+ const name = authVhostName(domain.name);
353
+ await reconcileOne(
354
+ {
355
+ name,
356
+ label: `auth.${domain.name}`,
357
+ render: (certDir) => renderAuthVhost(domain.name, certDir, { primary: domain.primary }),
358
+ enabled: () => deps.isSiteEnabled(name),
359
+ },
360
+ result,
361
+ deps,
362
+ );
363
+ }
364
+
365
+ const names = domains.map((d) => d.name);
336
366
 
337
367
  let sites = [];
338
368
  try {
@@ -340,14 +370,29 @@ export async function reconcileSiteVhosts(log, domain, deps = defaultSiteDeps) {
340
370
  } catch (err) {
341
371
  result.failed.push({ vhost: 'sites', error: err instanceof Error ? err.message : String(err) });
342
372
  }
373
+ const closed = [];
343
374
  for (const site of sites) {
344
375
  if (typeof site.id !== 'string' || !SITE_NAME_PART_RE.test(site.id)) continue;
376
+ // A plugin's web surface is its plugin's to render (lib/web-surface.js
377
+ // `reconcileWebSurfaces`, once plugins and hostings can be read).
378
+ if (site.owner) continue;
379
+
345
380
  const name = `lamalibre-lamaste-site-${site.id}`;
346
381
  await reconcileOne(
347
382
  {
348
383
  name,
349
384
  label: site.fqdn ?? site.id,
350
- render: (certDir) => renderStaticSiteVhost(site, certDir, domain),
385
+ render: (certDir) => {
386
+ const rendered = renderStaticSiteVhost(
387
+ site,
388
+ certDir,
389
+ typeof site.fqdn === 'string' ? domainOfHost(site.fqdn, names) : null,
390
+ );
391
+ // Protected, but under none of the relay's domains (possible since
392
+ // 4.6): it renders closed — said once, at the end.
393
+ if (rendered.closed) closed.push(site.fqdn);
394
+ return rendered;
395
+ },
351
396
  enabled: () => deps.isSiteEnabled(name),
352
397
  // A site still waiting for its DNS or certificate has no vhost yet.
353
398
  reportMissing: site.certIssued === true,
@@ -357,6 +402,71 @@ export async function reconcileSiteVhosts(log, domain, deps = defaultSiteDeps) {
357
402
  );
358
403
  }
359
404
 
405
+ if (closed.length > 0) {
406
+ log.warn(
407
+ { sites: closed },
408
+ 'protected static sites under none of the relay domains are closed (403)',
409
+ );
410
+ await deps.notify(
411
+ {
412
+ source: 'lamaste',
413
+ level: 'warning',
414
+ message: `Protected static sites under none of this relay's domains answer 403 to everyone: ${closed.join(', ')}. Add their domain, or turn protection off.`,
415
+ },
416
+ log,
417
+ );
418
+ }
419
+ result.closed = closed;
420
+
360
421
  logResult(log, 'site', result);
361
422
  return result;
362
423
  }
424
+
425
+ const defaultRenameDeps = { readTunnels, renameVhosts };
426
+
427
+ /**
428
+ * Rename the vhosts an older version named by subdomain — every tunnel's
429
+ * (`lamalibre-lamaste-app-<subdomain>`, `-agent-panel-<subdomain>`) and the
430
+ * auth portal (`lamalibre-lamaste-auth`, the primary's) — to their names by
431
+ * hostname, in one reload ({@link renameVhosts} in nginx.js: same text, same
432
+ * link state; all of it undone when nginx refuses). Idempotent: a vhost
433
+ * already renamed, or never installed, is left alone. Before the panel
434
+ * listens, under the tunnel lock; a failure is logged and the old names keep
435
+ * serving (the next start tries again).
436
+ *
437
+ * @param {Parameters<typeof logResult>[0]} log
438
+ * @param {string} primary the primary domain
439
+ * @param {typeof defaultRenameDeps} [deps]
440
+ * @returns {Promise<string[]>} the names written
441
+ */
442
+ export async function renameLegacyVhosts(log, primary, deps = defaultRenameDeps) {
443
+ return withTunnelLock(async () => {
444
+ const renames = [
445
+ { from: LEGACY_AUTH_VHOST, to: authVhostName(primary), serverName: `auth.${primary}` },
446
+ ];
447
+ for (const tunnel of await deps.readTunnels()) {
448
+ const to = managedTunnelVhostName(tunnel);
449
+ if (to === null) continue;
450
+ // Names by subdomain existed only on the primary domain, and an old
451
+ // file is the tunnel's only when it serves the tunnel's hostname — a
452
+ // same-subdomain tunnel of another domain never takes it.
453
+ if (tunnel.fqdn !== `${tunnel.subdomain}.${primary}`) continue;
454
+ renames.push({
455
+ from: legacyTunnelVhostName(tunnel.type, tunnel.subdomain),
456
+ to,
457
+ serverName: tunnel.fqdn,
458
+ });
459
+ }
460
+ try {
461
+ const renamed = await deps.renameVhosts(renames);
462
+ if (renamed.length > 0) log.info({ renamed }, 'Renamed vhosts to their hostnames');
463
+ return renamed;
464
+ } catch (err) {
465
+ log.error(
466
+ { err: err instanceof Error ? err.message : String(err) },
467
+ 'Renaming vhosts to their hostnames failed; the old names keep serving',
468
+ );
469
+ return [];
470
+ }
471
+ });
472
+ }