Repository: compiler-explorer/compiler-explorer
Pull request #7578 (2025-04-17 23:51:37)

[swift]: add 6.1 arm release compiler
adds a config entry for cross compiling to arm for the 6.1 release compiler. necessary infra changes added in https://github.com/compiler-explorer/infra/pull/1564. tested that the new config should work by manually adding the flags here: https://swift.godbolt.org/z/EPrsbKPf4.


Patch:
diff --git a/app.ts b/app.ts
index d9fa99731..e58676dd6 100755
--- a/app.ts
+++ b/app.ts
@@ -47,6 +47,7 @@ import systemdSocket from 'systemd-socket';
 import _ from 'underscore';
 import urljoin from 'url-join';
 
+import {unwrap} from './lib/assert.js';
 import * as aws from './lib/aws.js';
 import * as normalizer from './lib/clientstate-normalizer.js';
 import {GoldenLayoutRootStruct} from './lib/clientstate-normalizer.js';
@@ -635,9 +636,10 @@ async function main() {
         logger.debug('Compilers:', compilers);
         prevCompilers = compilers;
         await clientOptionsHandler.setCompilers(compilers);
-        routeApi.apiHandler.setCompilers(compilers);
-        routeApi.apiHandler.setLanguages(languages);
-        routeApi.apiHandler.setOptions(clientOptionsHandler);
+        const apiHandler = unwrap(routeApi.apiHandler);
+        apiHandler.setCompilers(compilers);
+        apiHandler.setLanguages(languages);
+        apiHandler.setOptions(clientOptionsHandler);
     }
 
     await onCompilerChange(initialCompilers);
@@ -687,7 +689,7 @@ async function main() {
             next({status: 404, message: `page "${req.path}" could not be found`});
         })
         // sentry error handler must be the first error handling middleware
-        .use(Sentry.Handlers.errorHandler)
+        .use(Sentry.Handlers.errorHandler())
         // eslint-disable-next-line no-unused-vars
         .use((err: any, req: express.Request, res: express.Response, _next: express.NextFunction) => {
             const status = err.status || err.statusCode || err.status_code || err.output?.statusCode || 500;
@@ -837,7 +839,7 @@ async function main() {
             res.set('Content-Type', 'application/javascript');
             res.end(`window.compilerExplorerOptions = ${clientOptionsHandler.getJSON()};`);
         })
-        .use('/bits/:bits(\\w+).html', cached, csp, (req, res) => {
+        .use('/bits/:bits.html', cached, csp, (req, res) => {
             res.render(
                 `bits/${sanitize(req.params.bits)}`,
                 renderConfig(
diff --git a/etc/config/c++.amazon.properties b/etc/config/c++.amazon.properties
index ee2cb21d1..aa8edbb4f 100644
--- a/etc/config/c++.amazon.properties
+++ b/etc/config/c++.amazon.properties
@@ -290,6 +290,7 @@ group.clang.unwiseOptions=-march=native
 group.clang.supportsPVS-Studio=true
 group.clang.supportsSonar=true
 group.clang.supportsLlvmCov=true
+group.clang.supportsBrontoRefactor=true
 group.clang.licenseName=LLVM Apache 2
 group.clang.licenseLink=https://github.com/llvm/llvm-project/blob/main/LICENSE.TXT
 group.clang.licensePreamble=The LLVM Project is under the Apache License v2.0 with LLVM Exceptions
@@ -477,6 +478,7 @@ group.clangx86trunk.isSemVer=true
 group.clangx86trunk.isNightly=true
 group.clangx86trunk.compilerType=clang
 group.clangx86trunk.unwiseOptions=-march=native
+group.clangx86trunk.supportsBrontoRefactor=true
 group.clangx86trunk.supportsPVS-Studio=true
 group.clangx86trunk.supportsSonar=true
 group.clangx86trunk.supportsLlvmCov=true
@@ -4114,7 +4116,7 @@ compiler.z180-clang-1507.options=-target z180 -g0 -nostdinc -fno-threadsafe-stat
 #################################
 #################################
 # Installed libs
-libs=abseil:array:async_simple:belleviews:beman_iterator_interface:beman_inplace_vector:beman_optional:benchmark:benri:blaze:boost:bmulti:brigand:catch2:cccl:cctz:cereal:cmcstl2:cnl:cppcoro:cppitertools:cpptrace:crosscables:ctbignum:cthash:ctre:date:dataframe:dawjson:dlib:doctest:eastl:eigen:enoki:entt:etl:eve:expected_lite:fastor:flux:fmt:gcem:gemmlowp:glaze:glm:gnufs:gnulibbacktrace:gnuexp:googletest:gsl:hdf5:hedley:hfsm:highfive:highway:hotels-template-library:immer:jsoncons:jsoncpp:kiwaku:kokkos:kumi:kvasir:kyosu:lager:lagom:lexy:libassert:libbpf:libguarded:libsimdpp:libuv:llvm:llvmfs:lua:magic_enum:mfem:mimicpp:mlir:mp-coro:mp-units:namedtype:nanorange:nlohmann_json:nsimd:ofw:openssl:outcome:pegtl:pipes:proxy:pugixml:pybind11:python:rangesv3:raberu:reactive_plus_plus:scnlib:seastar:seqan3:simde:simdjson:sol2:spdlog:spy:stdexec:strong_type:taojson:taskflow:tbb:thinkcell:tlexpected:toml11:tomlplusplus:trompeloeil:tts:type_safe:unifex:ureact:vcl:xercesc:xsimd:xtensor:xtl:yomm2:zug:cli11:avr-libstdcpp:curl:copperspice:sqlite:ztdcuneicode:ztdencodingtables:ztdidk:ztdstaticcontainers:ztdtext:ztdplatform:qt:quill:pcre2:widberg-defs:jwt-cpp:xieite:option:mdspan
+libs=abseil:array:async_simple:belleviews:beman_any_view:beman_iterator_interface:beman_inplace_vector:beman_optional:benchmark:benri:blaze:boost:bmulti:brigand:bronto:catch2:cccl:cctz:cereal:cmcstl2:cnl:cppcoro:cppitertools:cpptrace:crosscables:ctbignum:cthash:ctre:date:dataframe:dawjson:dlib:doctest:eastl:eigen:enoki:entt:etl:eve:expected_lite:fastor:flux:fmt:gcem:gemmlowp:glaze:glm:gnufs:gnulibbacktrace:gnuexp:googletest:gsl:hdf5:hedley:hfsm:highfive:highway:hotels-template-library:immer:jsoncons:jsoncpp:kiwaku:kokkos:kumi:kvasir:kyosu:lager:lagom:lexy:libassert:libbpf:libguarded:libsimdpp:libuv:llvm:llvmfs:lua:magic_enum:mfem:mimicpp:mlir:mp-coro:mp-units:namedtype:nanorange:nlohmann_json:nsimd:ofw:openssl:outcome:pegtl:pipes:proxy:pugixml:pybind11:python:rangesv3:raberu:reactive_plus_plus:scnlib:seastar:seqan3:simde:simdjson:sol2:spdlog:spy:stdexec:strong_type:taojson:taskflow:tbb:thinkcell:tlexpected:toml11:tomlplusplus:trompeloeil:tts:type_safe:unifex:ureact:vcl:xercesc:xsimd:xtensor:xtl:yomm2:zug:cli11:avr-libstdcpp:curl:copperspice:sqlite:ztdcuneicode:ztdencodingtables:ztdidk:ztdstaticcontainers:ztdtext:ztdplatform:qt:quill:pcre2:widberg-defs:jwt-cpp:xieite:option:mdspan
 
 
 libs.abseil.name=Abseil
@@ -4146,6 +4148,12 @@ libs.belleviews.description=A library of C++ views that just works for all basic
 libs.belleviews.versions.trunk.version=trunk
 libs.belleviews.versions.trunk.path=/opt/compiler-explorer/libs/belleviews/trunk/sources
 
+libs.beman_any_view.name=beman.any_view
+libs.beman_any_view.versions=trunk
+libs.beman_any_view.url=https://github.com/bemanproject/any_view
+libs.beman_any_view.versions.trunk.version=trunk
+libs.beman_any_view.versions.trunk.path=/opt/compiler-explorer/libs/beman_any_view/main/include
+
 libs.beman_iterator_interface.name=beman.iterator_interface
 libs.beman_iterator_interface.versions=trunk
 libs.beman_iterator_interface.url=https://github.com/bemanproject/iterator_interface
@@ -4289,6 +4297,13 @@ libs.brigand.versions.130.version=1.3.0
 libs.brigand.versions.trunk.path=/opt/compiler-explorer/libs/brigand/trunk/include
 libs.brigand.versions.130.path=/opt/compiler-explorer/libs/brigand/1.3.0
 
+libs.bronto.name=Bronto
+libs.bronto.versions=0_1_0
+libs.bronto.url=https://brontosource.dev/
+libs.bronto.versions.0_1_0.version=0.1.0
+libs.bronto.versions.0_1_0.alias=trunk
+libs.bronto.versions.0_1_0.path=/opt/compiler-explorer/libs/bronto/v0.1.0/include
+
 libs.catch2.name=Catch2
 libs.catch2.description=Catch2
 libs.catch2.url=https://github.com/catchorg/Catch2
@@ -5458,7 +5473,7 @@ libs.qt.url=https://www.qt.io
 libs.qt.packagedheaders=true
 libs.qt.liblink=Qt6Core
 libs.qt.options=-DQT_NO_VERSION_TAGGING
-libs.qt.versions=642:652:660:670:680
+libs.qt.versions=642:652:660:670:680:690
 libs.qt.versions.642.version=6.4.2
 libs.qt.versions.642.path=/app/qt/include/QtCore
 libs.qt.versions.652.version=6.5.2
@@ -5469,6 +5484,8 @@ libs.qt.versions.670.version=6.7.0
 libs.qt.versions.670.path=/app/qt/include/QtCore
 libs.qt.versions.680.version=6.8.0
 libs.qt.versions.680.path=/app/qt/include/QtCore
+libs.qt.versions.690.version=6.9.0
+libs.qt.versions.690.path=/app/qt/include/QtCore
 
 libs.quill.name=Quill
 libs.quill.description=C++ Low Latency Logging Library
@@ -6038,7 +6055,7 @@ libs.option.versions.131.path=/opt/compiler-explorer/libs/option/v1.3.1/include
 #################################
 #################################
 # Installed tools
-tools=PVS-Studio:clangformattrunk:clangquerytrunk:clangtidytrunk:iwyu:iwyu022:ldd:llvm-covtrunk:llvm-mcatrunk:osacatrunk:pahole:readelf:nm:Sonar:strings:x86to6502:llvmdwarfdumptrunk:bloaty11
+tools=PVS-Studio:bronto-refactor:clangformattrunk:clangquerytrunk:clangtidytrunk:iwyu:iwyu022:ldd:llvm-covtrunk:llvm-mcatrunk:osacatrunk:pahole:readelf:nm:Sonar:strings:x86to6502:llvmdwarfdumptrunk:bloaty11
 
 tools.PVS-Studio.name=PVS-Studio
 tools.PVS-Studio.exe=/opt/compiler-explorer/pvs-studio-latest/bin/pvs-studio-analyzer
@@ -6048,6 +6065,13 @@ tools.PVS-Studio.class=pvs-studio-tool
 tools.PVS-Studio.stdinHint=disabled
 tools.PVS-Studio.includeKey=supportsPVS-Studio
 
+tools.bronto-refactor.exe=/opt/compiler-explorer/brontosource/bronto-refactor-ubuntu-22.04-x86-64/bronto-refactor
+tools.bronto-refactor.name=bronto-refactor
+tools.bronto-refactor.type=independent
+tools.bronto-refactor.class=bronto-refactor-tool
+tools.bronto-refactor.stdinHint=disabled
+tools.bronto-refactor.includeKey=supportsBrontoRefactor
+
 tools.clangformattrunk.exe=/opt/compiler-explorer/clang-trunk/bin/clang-format
 tools.clangformattrunk.name=clang-format
 tools.clangformattrunk.type=independent
diff --git a/etc/config/c.amazon.properties b/etc/config/c.amazon.properties
index 2efc3dea6..366999925 100644
--- a/etc/config/c.amazon.properties
+++ b/etc/config/c.amazon.properties
@@ -3646,6 +3646,8 @@ compiler.c2rust-master.name=C2Rust (master)
 libs=cs50:hedley:libbpf:libuv:liblzma:lua:nsimd:openssl:python:simde:curl:sqlite:widberg-defs
 
 libs.cs50.name=cs50
+libs.cs50.description=This is CS50's Library for C.
+libs.cs50.url=https://github.com/cs50/libcs50
 libs.cs50.versions=910
 libs.cs50.versions.910.version=9.1.0
 libs.cs50.versions.910.path=/opt/compiler-explorer/libs/cs50/9.1.0/include
diff --git a/etc/config/d.amazon.properties b/etc/config/d.amazon.properties
index 03be5e453..b23e79c9d 100644
--- a/etc/config/d.amazon.properties
+++ b/etc/config/d.amazon.properties
@@ -1,5 +1,5 @@
 compilers=&gdc:&ldc:&dmd:&gdccross
-defaultCompiler=ldc1_39
+defaultCompiler=ldc1_40
 
 group.gdc.compilers=&gdcassert:gdc48:gdc49:gdc52:gdc92:gdc93:gdc95:gdc101:gdc102:gdc105:gdc111:gdc113:gdc114:gdc121:gdc122:gdc123:gdc124:gdc131:gdc132:gdc133:gdc141:gdc142:gdctrunk
 group.gdc.groupName=GDC x86-64
@@ -695,7 +695,7 @@ compiler.gdchppa1420.semver=14.2.0
 compiler.gdchppa1420.objdumper=/opt/compiler-explorer/hppa/gcc-14.2.0/hppa-unknown-linux-gnu/bin/hppa-unknown-linux-gnu-objdump
 compiler.gdchppa1420.demangler=/opt/compiler-explorer/hppa/gcc-14.2.0/hppa-unknown-linux-gnu/bin/hppa-unknown-linux-gnu-c++filt
 
-group.ldc.compilers=ldc017:ldc100:ldc110:ldc120:ldc130:ldc140:ldc150:ldc160:ldc170:ldc180:ldc190:ldc1_10:ldc1_11:ldc1_12:ldc1_13:ldc1_14:ldc1_15:ldc1_16:ldc1_17:ldc1_18:ldc1_19:ldc1_20:ldc1_21:ldc1_22:ldc1_23:ldc1_24:ldc1_25:ldc1_26:ldc1_27:ldc1_28:ldc1_29:ldc1_30:ldc1_31:ldc1_32:ldc1_33:ldc1_34:ldc1_35:ldc1_36:ldc1_37:ldc1_38:ldc1_39:ldcbeta:ldclatestci
+group.ldc.compilers=ldc017:ldc100:ldc110:ldc120:ldc130:ldc140:ldc150:ldc160:ldc170:ldc180:ldc190:ldc1_10:ldc1_11:ldc1_12:ldc1_13:ldc1_14:ldc1_15:ldc1_16:ldc1_17:ldc1_18:ldc1_19:ldc1_20:ldc1_21:ldc1_22:ldc1_23:ldc1_24:ldc1_25:ldc1_26:ldc1_27:ldc1_28:ldc1_29:ldc1_30:ldc1_31:ldc1_32:ldc1_33:ldc1_34:ldc1_35:ldc1_36:ldc1_37:ldc1_38:ldc1_39:ldc1_40:ldcbeta:ldclatestci
 group.ldc.compilerType=ldc
 group.ldc.isSemVer=true
 group.ldc.baseName=ldc
@@ -806,6 +806,9 @@ compiler.ldc1_38.options=-gcc=gcc
 compiler.ldc1_39.exe=/opt/compiler-explorer/ldc1.39.0/ldc2-1.39.0-linux-x86_64/bin/ldc2
 compiler.ldc1_39.semver=1.39.0
 compiler.ldc1_39.options=-gcc=gcc
+compiler.ldc1_40.exe=/opt/compiler-explorer/ldc1.40.1/ldc2-1.40.1-linux-x86_64/bin/ldc2
+compiler.ldc1_40.semver=1.40.1
+compiler.ldc1_40.options=-gcc=gcc
 compiler.ldcbeta.exe=/opt/compiler-explorer/ldcbeta/bin/ldc2
 compiler.ldcbeta.semver=beta
 compiler.ldcbeta.options=-gcc=gcc
diff --git a/etc/config/swift.amazon.properties b/etc/config/swift.amazon.properties
index d8b63dac7..97164ae19 100644
--- a/etc/config/swift.amazon.properties
+++ b/etc/config/swift.amazon.properties
@@ -14,7 +14,7 @@ group.swift.groupName=swift (x86-64)
 group.swift.baseName=x86-64 swiftc
 
 # arm-swift (aarch64)
-group.arm-swift.compilers=arm-swift603
+group.arm-swift.compilers=arm-swift603:arm-swift61
 group.arm-swift.isSemVer=true
 group.arm-swift.groupName=swift (aarch64)
 group.arm-swift.baseName=aarch64 swiftc
@@ -46,6 +46,10 @@ compiler.swift603.semver=6.0.3
 
 # 6.x (arm)
 
+compiler.arm-swift61.exe=/opt/compiler-explorer/swift-6.1/usr/bin/swiftc
+compiler.arm-swift61.semver=6.1
+compiler.arm-swift61.options=-target aarch64-swift-linux-musl -sdk /opt/compiler-explorer/swift-6.1-static-sdk/swift-linux-musl/musl-1.2.5.sdk/aarch64 -resource-dir /opt/compiler-explorer/swift-6.1-static-sdk/swift-linux-musl/musl-1.2.5.sdk/aarch64/usr/lib/swift_static
+
 compiler.arm-swift603.exe=/opt/compiler-explorer/swift-6.0.3/usr/bin/swiftc
 compiler.arm-swift603.semver=6.0.3
 compiler.arm-swift603.options=-target aarch64-swift-linux-musl -sdk /opt/compiler-explorer/swift-6.0.3-static-sdk/swift-linux-musl/musl-1.2.5.sdk/aarch64 -resource-dir /opt/compiler-explorer/swift-6.0.3-static-sdk/swift-linux-musl/musl-1.2.5.sdk/aarch64/usr/lib/swift_static
diff --git a/examples/rust/default.rs b/examples/rust/default.rs
index 65233495f..acf928648 100644
--- a/examples/rust/default.rs
+++ b/examples/rust/default.rs
@@ -3,10 +3,10 @@
 // As of Rust 1.75, small functions are automatically
 // marked as `#[inline]` so they will not show up in
 // the output when compiling with optimisations. Use
-// `#[no_mangle]` or `#[inline(never)]` to work around
-// this issue.
+// `#[unsafe(no_mangle)]` or `#[inline(never)]` to
+// work around this issue.
 // See https://github.com/compiler-explorer/compiler-explorer/issues/5939
-#[no_mangle]
+#[unsafe(no_mangle)]
 pub fn square(num: i32) -> i32 {
     num * num
 }
diff --git a/lib/binaries/binary-utils.ts b/lib/binaries/binary-utils.ts
index a386c1232..eaa65848f 100644
--- a/lib/binaries/binary-utils.ts
+++ b/lib/binaries/binary-utils.ts
@@ -123,11 +123,11 @@ export class BinaryInfoLinux {
         return undefined;
     }
 
-    static async readFile(filepath: string): Promise<BinaryInfo | undefined> {
+    static async readFile(filepath: string, instructionSetHint?: InstructionSet): Promise<BinaryInfo | undefined> {
         if (os.platform() === 'win32') {
             return {
                 os: OSType.windows,
-                instructionSet: 'amd64',
+                instructionSet: instructionSetHint || 'amd64',
             };
         }
         const info = await executeDirect('/usr/bin/file', ['-b', filepath], {});
diff --git a/lib/cfg/cfg-parsers/llvm-ir.ts b/lib/cfg/cfg-parsers/llvm-ir.ts
index 792ca7c9a..470ae2e23 100644
--- a/lib/cfg/cfg-parsers/llvm-ir.ts
+++ b/lib/cfg/cfg-parsers/llvm-ir.ts
@@ -198,6 +198,8 @@ export class LlvmIrCfgParser extends BaseCFGParser {
                 terminator = 'invoke';
             } else if (terminatingInstruction.includes('callbr')) {
                 terminator = 'callbr';
+            } else if (terminatingInstruction.includes('catchswitch')) {
+                terminator = 'catchswitch';
             } else {
                 terminator = terminatingInstruction.trim().split(' ')[0].replaceAll(',', '');
             }
@@ -327,17 +329,53 @@ export class LlvmIrCfgParser extends BaseCFGParser {
                 }
                 case 'catchswitch': {
                     // %cs2 = catchswitch within %parenthandler [label %handler0] unwind label %cleanup
-                    // TODO
+                    const handlersOrUnwindPart = terminatingInstruction.slice(
+                        terminatingInstruction.lastIndexOf('[label '),
+                    );
+                    const catchswitchLabels = [...handlersOrUnwindPart.matchAll(this.labelReference)].map(m => m[1]);
+
+                    for (const label of catchswitchLabels) {
+                        edges.push({
+                            from: bb.nameId,
+                            to: label,
+                            arrows: 'to',
+                            color: 'blue',
+                        });
+                    }
                     break;
                 }
                 case 'catchret': {
                     // catchret from %catch to label %continue
-                    // TODO
+                    const catchretLabelsPart = terminatingInstruction.slice(
+                        terminatingInstruction.lastIndexOf('to label'),
+                    );
+                    const catchretLabels = [...catchretLabelsPart.matchAll(this.labelReference)].map(m => m[1]);
+                    // Should be a single one, but sticking to the regular handling
+                    for (const label of catchretLabels) {
+                        edges.push({
+                            from: bb.nameId,
+                            to: label,
+                            arrows: 'to',
+                            color: 'blue',
+                        });
+                    }
                     break;
                 }
                 case 'cleanupret': {
                     // cleanupret from %cleanup unwind label %continue
-                    // TODO
+                    const continueLabelsPart = terminatingInstruction.slice(
+                        terminatingInstruction.lastIndexOf('unwind label'),
+                    );
+                    const continueLabels = [...continueLabelsPart.matchAll(this.labelReference)].map(m => m[1]);
+                    // Should be a single one, but sticking to the regular handling
+                    for (const label of continueLabels) {
+                        edges.push({
+                            from: bb.nameId,
+                            to: label,
+                            arrows: 'to',
+                            color: 'blue',
+                        });
+                    }
                     break;
                 }
                 default: {
diff --git a/lib/compilers/sail.ts b/lib/compilers/sail.ts
index b1f746008..dd1e8f8cc 100644
--- a/lib/compilers/sail.ts
+++ b/lib/compilers/sail.ts
@@ -22,8 +22,8 @@
 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 // POSSIBILITY OF SUCH DAMAGE.
 
+import fs from 'node:fs/promises';
 import path from 'node:path';
-import fs from 'fs-extra';
 
 import {CompilationResult, ExecutionOptionsWithEnv} from '../../types/compilation/compilation.interfaces.js';
 import type {PreliminaryCompilerInfo} from '../../types/compiler.interfaces.js';
@@ -31,6 +31,7 @@ import type {ParseFiltersAndOutputOptions} from '../../types/features/filters.in
 import {SelectedLibraryVersion} from '../../types/libraries/libraries.interfaces.js';
 import {BaseCompiler} from '../base-compiler.js';
 import {CompilationEnvironment} from '../compilation-env.js';
+import * as utils from '../utils.js';
 
 export class SailCompiler extends BaseCompiler {
     // Path to C compiler to use to compile generated C code to binary.
@@ -152,7 +153,7 @@ export class SailCompiler extends BaseCompiler {
         // and when compiling to a binary. If you don't do this it
         // tries to do nonsensical things like objdumping the C, so we
         // copy the binary back over the C file.
-        if (compileResult.code === 0 && (await fs.pathExists(outputFilenameExe))) {
+        if (compileResult.code === 0 && (await utils.fileExists(outputFilenameExe))) {
             console.log(`Copying ${outputFilenameExe} to ${outputFilenameC}`);
             await fs.copyFile(outputFilenameExe, outputFilenameC);
         }
diff --git a/lib/execution/execution-query.ts b/lib/execution/execution-query.ts
index 63038f516..0b7667608 100644
--- a/lib/execution/execution-query.ts
+++ b/lib/execution/execution-query.ts
@@ -76,7 +76,7 @@ export class RemoteExecutionQuery {
         const triple = new BaseExecutionTriple();
 
         if (result.executableFilename) {
-            const info = await BinaryInfoLinux.readFile(result.executableFilename);
+            const info = await BinaryInfoLinux.readFile(result.executableFilename, result.instructionSet);
             if (info) {
                 triple.instructionSet = info.instructionSet;
                 triple.os = info.os;
diff --git a/lib/handlers/api.ts b/lib/handlers/api.ts
index 3c98bf12c..6dfbaee8b 100644
--- a/lib/handlers/api.ts
+++ b/lib/handlers/api.ts
@@ -43,8 +43,7 @@ import {StorageBase} from '../storage/index.js';
 import {CompileHandler} from './compile.js';
 
 function methodNotAllowed(req: express.Request, res: express.Response) {
-    res.send('Method Not Allowed');
-    return res.status(405).end();
+    res.status(405).send('Method Not Allowed').end();
 }
 
 export class ApiHandler {
diff --git a/lib/handlers/api/assembly-documentation-controller.ts b/lib/handlers/api/assembly-documentation-controller.ts
index 85317b6a6..41b23a4a2 100644
--- a/lib/handlers/api/assembly-documentation-controller.ts
+++ b/lib/handlers/api/assembly-documentation-controller.ts
@@ -48,7 +48,8 @@ export class AssemblyDocumentationController implements HttpController {
             const instruction = (req.params.opcode || '__UNKNOWN_OPCODE').toUpperCase();
             const information = provider.getInstructionInformation(instruction);
             if (information === null) {
-                return res.status(404).send({error: `Unknown opcode '${instruction}'`});
+                res.status(404).send({error: `Unknown opcode '${instruction}'`});
+                return;
             }
 
             const contentType = req.accepts(['text', 'json']);
@@ -67,7 +68,7 @@ export class AssemblyDocumentationController implements HttpController {
                 }
             }
         } catch {
-            return res.status(404).send({error: `No documentation for '${req.params.arch}'`});
+            res.status(404).send({error: `No documentation for '${req.params.arch}'`});
         }
     }
 }
diff --git a/lib/handlers/api/formatting-controller.ts b/lib/handlers/api/formatting-controller.ts
index c293f8861..b343d8c05 100644
--- a/lib/handlers/api/formatting-controller.ts
+++ b/lib/handlers/api/formatting-controller.ts
@@ -44,7 +44,8 @@ export class FormattingController implements HttpController {
         const formatter = this.formattingService.getFormatterById(req.params.tool);
         // Ensure the target formatter exists
         if (formatter === null) {
-            return res.status(422).json({exit: 2, answer: `Unknown format tool '${req.params.tool}'`});
+            res.status(422).json({exit: 2, answer: `Unknown format tool '${req.params.tool}'`});
+            return;
         }
         // Ensure there is source code to format
         if (!req.body || !req.body.source) {
@@ -53,7 +54,8 @@ export class FormattingController implements HttpController {
         }
         // Ensure that the requested style is supported by the formatter
         if (!formatter.isValidStyle(req.body.base)) {
-            return res.status(422).json({exit: 3, answer: `Style '${req.body.base}' is not supported`});
+            res.status(422).json({exit: 3, answer: `Style '${req.body.base}' is not supported`});
+            return;
         }
         // Do the formatting
         try {
diff --git a/lib/handlers/api/healthcheck-controller.ts b/lib/handlers/api/healthcheck-controller.ts
index 4a086e1f4..a3e21bac2 100644
--- a/lib/handlers/api/healthcheck-controller.ts
+++ b/lib/handlers/api/healthcheck-controller.ts
@@ -62,7 +62,8 @@ export class HealthcheckController implements HttpController {
         // If this is a worker, we don't require that the server has languages configured.
         if (!this.isExecutionWorker && !this.compileHandler.hasLanguages()) {
             logger.error('*** HEALTH CHECK FAILURE: no languages/compilers detected');
-            return res.status(500).send();
+            res.status(500).send();
+            return;
         }
 
         // If we have a healthcheck file, we require that it exists and it is non-empty. The /efs/.health file contents
@@ -74,14 +75,16 @@ export class HealthcheckController implements HttpController {
                     throw new Error('File is empty');
                 }
                 res.set('Content-Type', 'text/html');
-                return res.send(content);
+                res.send(content);
+                return;
             } catch (e) {
                 logger.error(`*** HEALTH CHECK FAILURE: while reading file '${this.healthCheckFilePath}' got ${e}`);
                 SentryCapture(e, 'Health check');
-                return res.status(500).send();
+                res.status(500).send();
+                return;
             }
         }
 
-        return res.send('Everything is awesome');
+        res.send('Everything is awesome');
     }
 }
diff --git a/lib/handlers/compile.ts b/lib/handlers/compile.ts
index a67866db3..f8f0826a2 100644
--- a/lib/handlers/compile.ts
+++ b/lib/handlers/compile.ts
@@ -486,7 +486,8 @@ export class CompileHandler implements ICompileHandler {
     handlePopularArguments(req: express.Request, res: express.Response) {
         const compiler = this.compilerFor(req);
         if (!compiler) {
-            return res.sendStatus(404);
+            res.sendStatus(404);
+            return;
         }
         res.send(compiler.possibleArguments.getPopularArguments(this.getUsedOptions(req)));
     }
@@ -494,7 +495,8 @@ export class CompileHandler implements ICompileHandler {
     handleOptimizationArguments(req: express.Request, res: express.Response) {
         const compiler = this.compilerFor(req);
         if (!compiler) {
-            return res.sendStatus(404);
+            res.sendStatus(404);
+            return;
         }
         res.send(compiler.possibleArguments.getOptimizationArguments(this.getUsedOptions(req)));
     }
@@ -513,18 +515,20 @@ export class CompileHandler implements ICompileHandler {
 
     handleApiError(error: any, res: express.Response, next: express.NextFunction) {
         if (error.message) {
-            return res.status(400).send({
+            res.status(400).send({
                 error: true,
                 message: error.message,
             });
+            return;
         }
-        return next(error);
+        next(error);
     }
 
     handleCmake(req: express.Request, res: express.Response, next: express.NextFunction) {
         const compiler = this.compilerFor(req);
         if (!compiler) {
-            return res.sendStatus(404);
+            res.sendStatus(404);
+            return;
         }
 
         const remote = compiler.getRemote();
@@ -561,14 +565,16 @@ export class CompileHandler implements ICompileHandler {
                     return this.handleApiError(e, res, next);
                 });
         } catch (e) {
-            return this.handleApiError(e, res, next);
+            this.handleApiError(e, res, next);
+            return;
         }
     }
 
     handle(req: express.Request, res: express.Response, next: express.NextFunction) {
         const compiler = this.compilerFor(req);
         if (!compiler) {
-            return res.sendStatus(404);
+            res.sendStatus(404);
+            return;
         }
 
         const remote = compiler.getRemote();
@@ -585,7 +591,8 @@ export class CompileHandler implements ICompileHandler {
         try {
             parsedRequest = this.parseRequest(req, compiler);
         } catch (error) {
-            return this.handleApiError(error, res, next);
+            this.handleApiError(error, res, next);
+            return;
         }
 
         const {source, options, backendOptions, filters, bypassCache, tools, executeParameters, libraries} =
diff --git a/lib/handlers/middleware.ts b/lib/handlers/middleware.ts
index 3e940c983..f02be6fd0 100644
--- a/lib/handlers/middleware.ts
+++ b/lib/handlers/middleware.ts
@@ -35,9 +35,10 @@ const ceProps = props.propsFor('compiler-explorer');
  */
 export const jsonOnly: express.Handler = (req, res, next) => {
     if (req.headers['content-type'] !== 'application/json') {
-        return res.status(400).json({message: 'bad request, expected json content'});
+        res.status(400).json({message: 'bad request, expected json content'});
+        return;
     }
-    return next();
+    next();
 };
 
 /** Add static headers to the response */
diff --git a/lib/handlers/route-api.ts b/lib/handlers/route-api.ts
index a1ddb6b7e..5c90773ff 100644
--- a/lib/handlers/route-api.ts
+++ b/lib/handlers/route-api.ts
@@ -29,7 +29,7 @@ import express from 'express';
 import {AppDefaultArguments, CompilerExplorerOptions} from '../../app.js';
 import {isString} from '../../shared/common-utils.js';
 import {Language} from '../../types/languages.interfaces.js';
-import {assert} from '../assert.js';
+import {assert, unwrap} from '../assert.js';
 import {ClientStateGoldenifier, ClientStateNormalizer} from '../clientstate-normalizer.js';
 import {ClientState} from '../clientstate.js';
 import {CompilationEnvironment} from '../compilation-env.js';
@@ -67,7 +67,7 @@ export type ShortLinkMetaData = {
 export class RouteAPI {
     renderGoldenLayout: any;
     storageHandler: StorageBase;
-    apiHandler: ApiHandler;
+    apiHandler: ApiHandler | undefined;
 
     constructor(
         private readonly router: express.Router,
@@ -89,17 +89,19 @@ export class RouteAPI {
 
             this.apiHandler.setReleaseInfo(config.defArgs.gitReleaseName, config.defArgs.releaseBuildNumber);
         } else {
-            this.apiHandler = undefined as any;
+            this.apiHandler = undefined;
         }
     }
 
     InitializeRoutes() {
+        if (this.apiHandler) {
+            this.router.use('/api', this.apiHandler.handle);
+        }
         this.router
-            .use('/api', this.apiHandler.handle)
             .get('/z/:id', cached, csp, this.storedStateHandler.bind(this))
             .get('/z/:id/code/:session', cached, csp, this.storedCodeHandler.bind(this))
             .get('/resetlayout/:id', cached, csp, this.storedStateHandlerResetLayout.bind(this))
-            .get('/clientstate/:clientstatebase64([^]*)', cached, csp, this.unstoredStateHandler.bind(this))
+            .get(/^\/clientstate\/(?<clientstatebase64>.*)/, cached, csp, this.unstoredStateHandler.bind(this))
             .get('/fromsimplelayout', cached, csp, this.simpleLayoutHandler.bind(this));
     }
 
@@ -111,7 +113,7 @@ export class RouteAPI {
             .then(result => {
                 const config = JSON.parse(result.config);
 
-                let clientstate: ClientState | null = null;
+                let clientstate: ClientState | null;
                 if (config.content) {
                     const normalizer = new ClientStateNormalizer();
                     normalizer.fromGoldenLayout(config);
@@ -268,7 +270,7 @@ export class RouteAPI {
             const sources = utils.glGetMainContents(config.content);
             if (sources.editors.length === 1) {
                 const editor = sources.editors[0];
-                lang = this.apiHandler.languages[editor.language];
+                lang = unwrap(this.apiHandler).languages[editor.language];
                 source = editor.source;
             } else {
                 const normalizer = new ClientStateNormalizer();
@@ -277,7 +279,7 @@ export class RouteAPI {
 
                 if (clientstate.trees && clientstate.trees.length === 1) {
                     const tree = clientstate.trees[0];
-                    lang = this.apiHandler.languages[tree.compilerLanguageId];
+                    lang = unwrap(this.apiHandler).languages[tree.compilerLanguageId];
 
                     if (tree.isCMakeProject) {
                         const firstSource = tree.files.find(file => {
@@ -304,7 +306,7 @@ export class RouteAPI {
                 metadata.ogTitle += ` - ${lang.name}`;
                 if (sources && sources.compilers.length === 1) {
                     const compilerId = sources.compilers[0].compiler;
-                    const compiler = this.apiHandler.compilers.find(c => c.id === compilerId);
+                    const compiler = unwrap(this.apiHandler).compilers.find(c => c.id === compilerId);
                     if (compiler) {
                         metadata.ogTitle += ` (${compiler.name})`;
                     }
diff --git a/lib/tooling/_all.ts b/lib/tooling/_all.ts
index dcd4f63c5..408974a01 100644
--- a/lib/tooling/_all.ts
+++ b/lib/tooling/_all.ts
@@ -42,3 +42,4 @@ export {x86to6502Tool} from './x86to6502-tool.js';
 export {TestingTool} from './testing-tool.js';
 export {BloatyTool} from './bloaty-tool.js';
 export {ClippyTool} from './clippy-tool.js';
+export {BrontoRefactorTool} from './bronto-refactor-tool.js';
diff --git a/lib/tooling/bronto-refactor-tool.ts b/lib/tooling/bronto-refactor-tool.ts
new file mode 100644
index 000000000..c2740ee72
--- /dev/null
+++ b/lib/tooling/bronto-refactor-tool.ts
@@ -0,0 +1,66 @@
+// Copyright (c) 2025, Compiler Explorer Authors
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//     * Redistributions of source code must retain the above copyright notice,
+//       this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above copyright
+//       notice, this list of conditions and the following disclaimer in the
+//       documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+import {splitArguments} from '../../shared/common-utils.js';
+import {CompilationInfo} from '../../types/compilation/compilation.interfaces.js';
+import {ToolInfo} from '../../types/tool.interfaces.js';
+import {OptionsHandlerLibrary} from '../options-handler.js';
+
+import {ToolEnv} from './base-tool.interface.js';
+import {BaseTool} from './base-tool.js';
+
+export class BrontoRefactorTool extends BaseTool {
+    static get key() {
+        return 'bronto-refactor-tool';
+    }
+
+    constructor(toolInfo: ToolInfo, env: ToolEnv) {
+        super(toolInfo, env);
+
+        this.addOptionsToToolArgs = false;
+    }
+
+    override async runTool(
+        compilationInfo: CompilationInfo,
+        inputFilepath: string,
+        _args?: string[],
+        _stdin?: string,
+        supportedLibraries?: Record<string, OptionsHandlerLibrary>,
+    ) {
+        const sourcefile = inputFilepath;
+        const options = compilationInfo.options;
+        const includeflags = super.getIncludeArguments(compilationInfo.libraries, supportedLibraries || {});
+        const libOptions = super.getLibraryOptions(compilationInfo.libraries, supportedLibraries || {});
+
+        let compileFlags = ['compiler-explorer', sourcefile, '--clang-path', compilationInfo.compiler.exe, '--'].concat(
+            splitArguments(compilationInfo.compiler.options),
+        );
+        compileFlags = compileFlags.concat(includeflags);
+        compileFlags = compileFlags.concat(libOptions);
+
+        const manualCompileFlags = options.filter(option => option !== sourcefile);
+        compileFlags = compileFlags.concat(manualCompileFlags);
+        return super.runTool(compilationInfo, sourcefile, compileFlags);
+    }
+}
diff --git a/package-lock.json b/package-lock.json
index e1978aec1..d424c4838 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,7 +31,7 @@
         "cross-env": "^7.0.3",
         "enhanced-ms": "^4.1.0",
         "events": "^3.3.0",
-        "express": "^4.21.2",
+        "express": "^5.1.0",
         "file-saver": "^2.0.5",
         "golden-layout": "^1.5.9",
         "http-proxy": "^1.18.1",
@@ -82,7 +82,7 @@
         "@types/bootstrap": "^5.2.10",
         "@types/chai": "^5.2.1",
         "@types/compression": "^1.7.5",
-        "@types/express": "^4.17.21",
+        "@types/express": "^5.0.1",
         "@types/file-saver": "^2.0.7",
         "@types/http-proxy": "^1.17.16",
         "@types/jquery": "^3.5.32",
@@ -4593,22 +4593,21 @@
       "license": "MIT"
     },
     "node_modules/@types/express": {
-      "version": "4.17.21",
-      "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
-      "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz",
+      "integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@types/body-parser": "*",
-        "@types/express-serve-static-core": "^4.17.33",
-        "@types/qs": "*",
+        "@types/express-serve-static-core": "^5.0.0",
         "@types/serve-static": "*"
       }
     },
     "node_modules/@types/express-serve-static-core": {
-      "version": "4.19.6",
-      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
-      "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
+      "version": "5.0.6",
+      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz",
+      "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -5303,22 +5302,34 @@
       }
     },
     "node_modules/accepts": {
-      "version": "1.3.8",
-      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
-      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+      "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
       "license": "MIT",
       "dependencies": {
-        "mime-types": "~2.1.34",
-        "negotiator": "0.6.3"
+        "mime-types": "^3.0.0",
+        "negotiator": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/accepts/node_modules/mime-types": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+      "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
       },
       "engines": {
         "node": ">= 0.6"
       }
     },
     "node_modules/accepts/node_modules/negotiator": {
-      "version": "0.6.3",
-      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
-      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+      "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
       "license": "MIT",
       "engines": {
         "node": ">= 0.6"
@@ -5498,12 +5509,6 @@
       ],
       "license": "MIT"
     },
-    "node_modules/array-flatten": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
-      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
-      "license": "MIT"
-    },
     "node_modules/asap": {
       "version": "2.0.6",
       "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
@@ -5741,69 +5746,23 @@
       "license": "MIT"
     },
     "node_modules/body-parser": {
-      "version": "1.20.3",
-      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
-      "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
-      "license": "MIT",
-      "dependencies": {
-        "bytes": "3.1.2",
-        "content-type": "~1.0.5",
-        "debug": "2.6.9",
-        "depd": "2.0.0",
-        "destroy": "1.2.0",
-        "http-errors": "2.0.0",
-        "iconv-lite": "0.4.24",
-        "on-finished": "2.4.1",
-        "qs": "6.13.0",
-        "raw-body": "2.5.2",
-        "type-is": "~1.6.18",
-        "unpipe": "1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.8",
-        "npm": "1.2.8000 || >= 1.4.16"
-      }
-    },
-    "node_modules/body-parser/node_modules/debug": {
-      "version": "2.6.9",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
-      "license": "MIT",
-      "dependencies": {
-        "ms": "2.0.0"
-      }
-    },
-    "node_modules/body-parser/node_modules/iconv-lite": {
-      "version": "0.4.24",
-      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
-      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
+      "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
       "license": "MIT",
       "dependencies": {
-        "safer-buffer": ">= 2.1.2 < 3"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/body-parser/node_modules/ms": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
-      "license": "MIT"
-    },
-    "node_modules/body-parser/node_modules/qs": {
-      "version": "6.13.0",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
-      "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
-      "license": "BSD-3-Clause",
-      "dependencies": {
-        "side-channel": "^1.0.6"
+        "bytes": "^3.1.2",
+        "content-type": "^1.0.5",
+        "debug": "^4.4.0",
+        "http-errors": "^2.0.0",
+        "iconv-lite": "^0.6.3",
+        "on-finished": "^2.4.1",
+        "qs": "^6.14.0",
+        "raw-body": "^3.0.0",
+        "type-is": "^2.0.0"
       },
       "engines": {
-        "node": ">=0.6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">=18"
       }
     },
     "node_modules/boolbase": {
@@ -6508,9 +6467,9 @@
       }
     },
     "node_modules/content-disposition": {
-      "version": "0.5.4",
-      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
-      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
+      "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
       "license": "MIT",
       "dependencies": {
         "safe-buffer": "5.2.1"
@@ -6538,10 +6497,13 @@
       }
     },
     "node_modules/cookie-signature": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
-      "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
-      "license": "MIT"
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+      "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.6.0"
+      }
     },
     "node_modules/cookiejar": {
       "version": "2.1.4",
@@ -7040,7 +7002,6 @@
       "version": "4.4.0",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
       "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "ms": "^2.1.3"
@@ -7095,16 +7056,6 @@
         "node": ">= 0.8"
       }
     },
-    "node_modules/destroy": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
-      "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.8",
-        "npm": "1.2.8000 || >= 1.4.16"
-      }
-    },
     "node_modules/detect-libc": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
@@ -7624,79 +7575,66 @@
       }
     },
     "node_modules/express": {
-      "version": "4.21.2",
-      "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
-      "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
-      "license": "MIT",
-      "dependencies": {
-        "accepts": "~1.3.8",
-        "array-flatten": "1.1.1",
-        "body-parser": "1.20.3",
-        "content-disposition": "0.5.4",
-        "content-type": "~1.0.4",
-        "cookie": "0.7.1",
-        "cookie-signature": "1.0.6",
-        "debug": "2.6.9",
-        "depd": "2.0.0",
-        "encodeurl": "~2.0.0",
-        "escape-html": "~1.0.3",
-        "etag": "~1.8.1",
-        "finalhandler": "1.3.1",
-        "fresh": "0.5.2",
-        "http-errors": "2.0.0",
-        "merge-descriptors": "1.0.3",
-        "methods": "~1.1.2",
-        "on-finished": "2.4.1",
-        "parseurl": "~1.3.3",
-        "path-to-regexp": "0.1.12",
-        "proxy-addr": "~2.0.7",
-        "qs": "6.13.0",
-        "range-parser": "~1.2.1",
-        "safe-buffer": "5.2.1",
-        "send": "0.19.0",
-        "serve-static": "1.16.2",
-        "setprototypeof": "1.2.0",
-        "statuses": "2.0.1",
-        "type-is": "~1.6.18",
-        "utils-merge": "1.0.1",
-        "vary": "~1.1.2"
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
+      "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "^2.0.0",
+        "body-parser": "^2.2.0",
+        "content-disposition": "^1.0.0",
+        "content-type": "^1.0.5",
+        "cookie": "^0.7.1",
+        "cookie-signature": "^1.2.1",
+        "debug": "^4.4.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "finalhandler": "^2.1.0",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.0",
+        "merge-descriptors": "^2.0.0",
+        "mime-types": "^3.0.0",
+        "on-finished": "^2.4.1",
+        "once": "^1.4.0",
+        "parseurl": "^1.3.3",
+        "proxy-addr": "^2.0.7",
+        "qs": "^6.14.0",
+        "range-parser": "^1.2.1",
+        "router": "^2.2.0",
+        "send": "^1.1.0",
+        "serve-static": "^2.2.0",
+        "statuses": "^2.0.1",
+        "type-is": "^2.0.1",
+        "vary": "^1.1.2"
       },
       "engines": {
-        "node": ">= 0.10.0"
+        "node": ">= 18"
       },
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/express"
       }
     },
-    "node_modules/express/node_modules/debug": {
-      "version": "2.6.9",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+    "node_modules/express/node_modules/fresh": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+      "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
       "license": "MIT",
-      "dependencies": {
-        "ms": "2.0.0"
+      "engines": {
+        "node": ">= 0.8"
       }
     },
-    "node_modules/express/node_modules/ms": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
-      "license": "MIT"
-    },
-    "node_modules/express/node_modules/qs": {
-      "version": "6.13.0",
-      "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
-      "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
-      "license": "BSD-3-Clause",
+    "node_modules/express/node_modules/mime-types": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+      "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+      "license": "MIT",
       "dependencies": {
-        "side-channel": "^1.0.6"
+        "mime-db": "^1.54.0"
       },
       "engines": {
-        "node": ">=0.6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/ljharb"
+        "node": ">= 0.6"
       }
     },
     "node_modules/extend": {
@@ -7900,38 +7838,22 @@
       }
     },
     "node_modules/finalhandler": {
-      "version": "1.3.1",
-      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
-      "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
+      "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
       "license": "MIT",
       "dependencies": {
-        "debug": "2.6.9",
-        "encodeurl": "~2.0.0",
-        "escape-html": "~1.0.3",
-        "on-finished": "2.4.1",
-        "parseurl": "~1.3.3",
-        "statuses": "2.0.1",
-        "unpipe": "~1.0.0"
+        "debug": "^4.4.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "on-finished": "^2.4.1",
+        "parseurl": "^1.3.3",
+        "statuses": "^2.0.1"
       },
       "engines": {
         "node": ">= 0.8"
       }
     },
-    "node_modules/finalhandler/node_modules/debug": {
-      "version": "2.6.9",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
-      "license": "MIT",
-      "dependencies": {
-        "ms": "2.0.0"
-      }
-    },
-    "node_modules/finalhandler/node_modules/ms": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
-      "license": "MIT"
-    },
     "node_modules/find-up": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
@@ -8546,7 +8468,6 @@
       "version": "0.6.3",
       "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
       "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -9997,12 +9918,12 @@
       "license": "CC0-1.0"
     },
     "node_modules/media-typer": {
-      "version": "0.3.0",
-      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
-      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+      "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
       "license": "MIT",
       "engines": {
-        "node": ">= 0.6"
+        "node": ">= 0.8"
       }
     },
     "node_modules/memfs": {
@@ -10026,10 +9947,13 @@
       }
     },
     "node_modules/merge-descriptors": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
-      "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+      "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
       "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
@@ -10044,6 +9968,7 @@
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
       "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 0.6"
@@ -10063,18 +9988,6 @@
         "node": ">=8.6"
       }
     },
-    "node_modules/mime": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
-      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
-      "license": "MIT",
-      "bin": {
-        "mime": "cli.js"
-      },
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/mime-db": {
       "version": "1.54.0",
       "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
@@ -10351,16 +10264,6 @@
         "@sinonjs/commons": "^3.0.1"
       }
     },
-    "node_modules/nise/node_modules/path-to-regexp": {
-      "version": "8.2.0",
-      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
-      "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=16"
-      }
-    },
     "node_modules/nock": {
       "version": "14.0.3",
       "resolved": "https://registry.npmjs.org/nock/-/nock-14.0.3.tgz",
@@ -10771,10 +10674,13 @@
       "license": "ISC"
     },
     "node_modules/path-to-regexp": {
-      "version": "0.1.12",
-      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
-      "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
-      "license": "MIT"
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
+      "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=16"
+      }
     },
     "node_modules/pathe": {
       "version": "2.0.3",
@@ -11754,7 +11660,6 @@
       "version": "6.14.0",
       "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
       "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
-      "dev": true,
       "license": "BSD-3-Clause",
       "dependencies": {
         "side-channel": "^1.1.0"
@@ -11785,32 +11690,20 @@
       }
     },
     "node_modules/raw-body": {
-      "version": "2.5.2",
-      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
-      "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
+      "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
       "license": "MIT",
       "dependencies": {
         "bytes": "3.1.2",
         "http-errors": "2.0.0",
-        "iconv-lite": "0.4.24",
+        "iconv-lite": "0.6.3",
         "unpipe": "1.0.0"
       },
       "engines": {
         "node": ">= 0.8"
       }
     },
-    "node_modules/raw-body/node_modules/iconv-lite": {
-      "version": "0.4.24",
-      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
-      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
-      "license": "MIT",
-      "dependencies": {
-        "safer-buffer": ">= 2.1.2 < 3"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/readable-stream": {
       "version": "2.3.8",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
@@ -12118,6 +12011,28 @@
         "fsevents": "~2.3.2"
       }
     },
+    "node_modules/router": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+      "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "depd": "^2.0.0",
+        "is-promise": "^4.0.0",
+        "parseurl": "^1.3.3",
+        "path-to-regexp": "^8.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/router/node_modules/is-promise": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+      "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+      "license": "MIT"
+    },
     "node_modules/rrweb-cssom": {
       "version": "0.8.0",
       "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
@@ -12335,51 +12250,46 @@
       }
     },
     "node_modules/send": {
-      "version": "0.19.0",
-      "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
-      "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
-      "license": "MIT",
-      "dependencies": {
-        "debug": "2.6.9",
-        "depd": "2.0.0",
-        "destroy": "1.2.0",
-        "encodeurl": "~1.0.2",
-        "escape-html": "~1.0.3",
-        "etag": "~1.8.1",
-        "fresh": "0.5.2",
-        "http-errors": "2.0.0",
-        "mime": "1.6.0",
-        "ms": "2.1.3",
-        "on-finished": "2.4.1",
-        "range-parser": "~1.2.1",
-        "statuses": "2.0.1"
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
+      "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.3.5",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.0",
+        "mime-types": "^3.0.1",
+        "ms": "^2.1.3",
+        "on-finished": "^2.4.1",
+        "range-parser": "^1.2.1",
+        "statuses": "^2.0.1"
       },
       "engines": {
-        "node": ">= 0.8.0"
+        "node": ">= 18"
       }
     },
-    "node_modules/send/node_modules/debug": {
-      "version": "2.6.9",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
-      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+    "node_modules/send/node_modules/fresh": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+      "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
       "license": "MIT",
-      "dependencies": {
-        "ms": "2.0.0"
+      "engines": {
+        "node": ">= 0.8"
       }
     },
-    "node_modules/send/node_modules/debug/node_modules/ms": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
-      "license": "MIT"
-    },
-    "node_modules/send/node_modules/encodeurl": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
-      "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+    "node_modules/send/node_modules/mime-types": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+      "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
       "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
+      },
       "engines": {
-        "node": ">= 0.8"
+        "node": ">= 0.6"
       }
     },
     "node_modules/serialize-javascript": {
@@ -12420,18 +12330,18 @@
       "license": "MIT"
     },
     "node_modules/serve-static": {
-      "version": "1.16.2",
-      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
-      "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
+      "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
       "license": "MIT",
       "dependencies": {
-        "encodeurl": "~2.0.0",
-        "escape-html": "~1.0.3",
-        "parseurl": "~1.3.3",
-        "send": "0.19.0"
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "parseurl": "^1.3.3",
+        "send": "^1.2.0"
       },
       "engines": {
-        "node": ">= 0.8.0"
+        "node": ">= 18"
       }
     },
     "node_modules/setimmediate": {
@@ -13615,13 +13525,26 @@
       }
     },
     "node_modules/type-is": {
-      "version": "1.6.18",
-      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
-      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+      "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
       "license": "MIT",
       "dependencies": {
-        "media-typer": "0.3.0",
-        "mime-types": "~2.1.24"
+        "content-type": "^1.0.5",
+        "media-typer": "^1.1.0",
+        "mime-types": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/type-is/node_modules/mime-types": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+      "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
       },
       "engines": {
         "node": ">= 0.6"
@@ -13758,15 +13681,6 @@
       "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
       "license": "MIT"
     },
-    "node_modules/utils-merge": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
-      "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.4.0"
-      }
-    },
     "node_modules/uuid": {
       "version": "9.0.1",
       "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
diff --git a/package.json b/package.json
index 30aa9d078..30e187dba 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,7 @@
     "cross-env": "^7.0.3",
     "enhanced-ms": "^4.1.0",
     "events": "^3.3.0",
-    "express": "^4.21.2",
+    "express": "^5.1.0",
     "file-saver": "^2.0.5",
     "golden-layout": "^1.5.9",
     "http-proxy": "^1.18.1",
@@ -91,7 +91,7 @@
     "@types/bootstrap": "^5.2.10",
     "@types/chai": "^5.2.1",
     "@types/compression": "^1.7.5",
-    "@types/express": "^4.17.21",
+    "@types/express": "^5.0.1",
     "@types/file-saver": "^2.0.7",
     "@types/http-proxy": "^1.17.16",
     "@types/jquery": "^3.5.32",
diff --git a/static/panes/output.ts b/static/panes/output.ts
index 552513caa..3ef0ec1a8 100644
--- a/static/panes/output.ts
+++ b/static/panes/output.ts
@@ -211,8 +211,11 @@ export class Output extends Pane<OutputState> {
             }
         }
 
-        if (result.execResult && (result.execResult.didExecute || result.didExecute)) {
-            this.add('Program returned: ' + result.execResult.code);
+        if (result.execResult) {
+            if (result.execResult.didExecute || result.didExecute) {
+                this.add('Program returned: ' + result.execResult.code);
+            }
+
             if (result.execResult.stderr.length || result.execResult.stdout.length) {
                 for (const obj of result.execResult.stderr) {
                     // Conserve empty lines as they are discarded by ansiToHtml


Test patch:
diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml
index 68cca8af7..9dae21819 100644
--- a/.github/workflows/test-and-deploy.yml
+++ b/.github/workflows/test-and-deploy.yml
@@ -24,12 +24,12 @@ jobs:
       - name: Run checks
         run: |
           npm run lint-check
-          npm run test
+          npm run test -- --coverage
           npm run ts-check
           python3 ./etc/scripts/util/propscheck.py
           python3 ./etc/scripts/util/propschecktest.py
           python3 ./etc/scripts/test_numba_wrapper.py
-      - uses: codecov/codecov-action@v4
+      - uses: codecov/codecov-action@v5
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
           verbose: true
diff --git a/test/handlers/middleware-tests.ts b/test/handlers/middleware-tests.ts
index 65b1bc9ab..a0bf6dc4b 100644
--- a/test/handlers/middleware-tests.ts
+++ b/test/handlers/middleware-tests.ts
@@ -31,7 +31,9 @@ import {cached, cors} from '../../lib/handlers/middleware.js';
 describe('middleware functions', () => {
     it('adds cache controls headers with cached()', async () => {
         const app = express();
-        app.use('/', cached, (_, res) => res.send('Hello World!'));
+        app.use('/', cached, (_, res) => {
+            res.send('Hello World!');
+        });
         await request(app)
             .get('/')
             .expect(200, 'Hello World!')
@@ -40,7 +42,9 @@ describe('middleware functions', () => {
 
     it('adds cors headers with cors()', async () => {
         const app = express();
-        app.use('/', cors, (_, res) => res.send('Hello World!'));
+        app.use('/', cors, (_, res) => {
+            res.send('Hello World!');
+        });
         await request(app)
             .get('/')
             .expect(200, 'Hello World!')
diff --git a/test/handlers/route-api-test.ts b/test/handlers/route-api-test.ts
index b258c4aec..7cd7b0ccb 100644
--- a/test/handlers/route-api-test.ts
+++ b/test/handlers/route-api-test.ts
@@ -24,9 +24,17 @@
 
 import zlib from 'node:zlib';
 
-import {describe, expect, it} from 'vitest';
+import {beforeAll, describe, expect, it} from 'vitest';
 
-import {extractJsonFromBufferAndInflateIfRequired} from '../../lib/handlers/route-api.js';
+import express from 'express';
+import request from 'supertest';
+import {GoldenLayoutRootStruct} from '../../lib/clientstate-normalizer.js';
+import {
+    HandlerConfig,
+    RouteAPI,
+    ShortLinkMetaData,
+    extractJsonFromBufferAndInflateIfRequired,
+} from '../../lib/handlers/route-api.js';
 
 function possibleCompression(buffer: Buffer): boolean {
     // code used in extractJsonFromBufferAndInflateIfRequired
@@ -63,3 +71,47 @@ describe('extractJsonFromBufferAndInflateIfRequired test cases', () => {
         expect(() => extractJsonFromBufferAndInflateIfRequired(buffer)).toThrow();
     });
 });
+
+describe('clientStateHandler', () => {
+    let app: express.Express;
+
+    beforeAll(() => {
+        app = express();
+        const apiHandler = new RouteAPI(app, {
+            renderGoldenLayout: (
+                config: GoldenLayoutRootStruct,
+                metadata: ShortLinkMetaData,
+                req: express.Request,
+                res: express.Response,
+            ) => {
+                res.send('This is ok');
+            },
+        } as unknown as HandlerConfig);
+        apiHandler.InitializeRoutes();
+    });
+
+    it('should return 200 for /clientstate', async () => {
+        // A valid document is a base64 encoded JSON object with a sessions array.
+        const document = Buffer.from(JSON.stringify({sessions: []}), 'utf-8').toString('base64');
+        const response = await request(app).get(`/clientstate/${document}`);
+        expect(response.status).toBe(200);
+    });
+    it('should return 200 for /clientstate even if the base64 contains `/`', async () => {
+        let document = '';
+        for (let i = 0; i < 1000; i++) {
+            document = Buffer.from(
+                JSON.stringify({
+                    sessions: [],
+                    magic: String.fromCharCode(i, i + 1234),
+                }),
+                'utf-8',
+            ).toString('base64');
+            if (document.includes('/')) {
+                break;
+            }
+        }
+        expect(document).toContain('/');
+        const response = await request(app).get(`/clientstate/${document}`);
+        expect(response.status).toBe(200);
+    });
+});