fix(web-app-stalwart): make the lockup URLs relative so one stylesheet fits both networks

sys-front-inj-design renders a role's stylesheet per host context and then copies it, with a plain copy, to a path that carries no network and no host dimension: one style.css serves the clearnet and the onion CDN vhost alike. The design URLs baked into it come from lookup('cdn') -> lookup('tls','web-svc-cdn','url.base'), an absolute scheme://host that depends on whether the rendering host sits in svc-net-tor.

In swarm that file is written four times. The manager renders the onion vhosts at 06:34 and 06:37; a worker outside svc-net-tor then renders mail.main.infinito.test at 06:59 as changed, overwriting the manager's onion version - and changed is itself the proof the bytes differed. The CDN is manager-pinned, so the file served is the one that worker clobbered. Nothing repairs it at request time either: body_filter.lua.j2 returns early for CSS before onionize is defined.

The negative control settles it. webmail-design.inc.php.j2 bakes the same absolute URL and passed, because it is written with stack_host_template, which skips non-stack hosts. The only artefact lacking that guard is the only one that failed.

Stripping scheme and authority lets the computed background-image resolve against the stylesheet's own origin, so the CSSOM reports the onion CDN under onion and the clearnet CDN under clearnet. The third call site is included on purpose: with all three origin-free the rendered file is byte-identical in every host context, so the 06:59 re-render reports ok instead of changed and the race is removed rather than survived.

Clearnet output is unchanged byte for byte; the regex keeps any CDN sub-path. 33 role stylesheets bake absolute design.urls.* the same way, and the underlying weakness is that unguarded copy into an un-dimensioned path - stalwart is simply the only role with clearnet vhosts in a swarm run today.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-10-15 15:08:52 +02:00
1 parent 781be75a00
commit cfc0c337ab
1 file changed
+4 -3
@@ -1,4 +1,5 @@
{% set design = lookup('design', application_id) %}
{% set design_origin = '^[a-z][a-z0-9+.-]*://[^/]+' %}
:root,
.dark {
--background: var(--design-surface-2);
@@ -93,7 +94,7 @@ html:has(> body > .card > #login-form) .alert.success {
}
{% if design.logo is string %}
svg[viewBox="95 84 500 90"] {
background: url("{{ design.urls.brand.svg if design.title is string else design.urls.icon.svg }}") left center / contain no-repeat;
background: url("{{ (design.urls.brand.svg if design.title is string else design.urls.icon.svg) | regex_replace(design_origin, '') }}") left center / contain no-repeat;
}
svg[viewBox="95 84 500 90"] > *,
html:has(> body > .card > #login-form) svg.default-logo > * {
@@ -106,7 +107,7 @@ html:has(> body > .card > #login-form) svg.default-logo > * {
}
}
html:has(> body > .card > #login-form) svg.default-logo {
background: url("{{ design.urls.brand.svg if design.title is string else design.urls.icon.svg }}") center / auto 33% no-repeat;
background: url("{{ (design.urls.brand.svg if design.title is string else design.urls.icon.svg) | regex_replace(design_origin, '') }}") center / auto 33% no-repeat;
}
{% endif %}
body[class*="task-"] {
@@ -2206,7 +2207,7 @@ html:has(> body[class*="task-"]) {
html:not([lang]) > body {
background-color: var(--design-surface-2);
{% if design.logo is string %}
background-image: url("{{ design.urls.icon.svg }}");
background-image: url("{{ design.urls.icon.svg | regex_replace(design_origin, '') }}");
{% endif %}
}
html:not([lang]):not(.dark-mode) > body:before {