diff --git a/CHANGELOG.md b/CHANGELOG.md index 71eb5de722cd..f2d75bb4a0eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,138 @@ + + +# 20.3.25 (2026-04-29) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------- | +| [5e01ef40e](https://github.com/angular/angular-cli/commit/5e01ef40eb87deda79d18654fc696b64d18bf889) | fix | upgrade postcss to 8.5.12 | + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------------------------------------------------------------- | +| [6686848d9](https://github.com/angular/angular-cli/commit/6686848d946ca157f9b92b84db377e912266395e) | fix | introduce trustProxyHeaders option to safely validate and sanitize proxy headers | + + + + + +# 20.3.24 (2026-04-15) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | -------------------------- | +| [10c09c77b](https://github.com/angular/angular-cli/commit/10c09c77b75602293377b962b2a8397a2819036c) | fix | update esbuild to `0.28.0` | + + + + + +# 20.3.23 (2026-04-08) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ---------------------- | +| [ccab02ba0](https://github.com/angular/angular-cli/commit/ccab02ba0413f25464a6e4cb5871716b221013b7) | fix | update vite to `7.3.2` | + + + + + +# 20.3.22 (2026-03-27) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------- | +| [5978eeeff](https://github.com/angular/angular-cli/commit/5978eeeff63cd62f1515d949eaad0b5e6f7c44cd) | fix | update picomatch to 4.0.4 | + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------- | +| [6e9b92612](https://github.com/angular/angular-cli/commit/6e9b926129a9dd79f01d47b7446411b8963ffb62) | fix | update picomatch to 4.0.4 | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------- | +| [6f209c26d](https://github.com/angular/angular-cli/commit/6f209c26dc5a454acd1cd76f25240c26978fa827) | fix | update picomatch to 4.0.4 | + + + + + +# 20.3.21 (2026-03-19) + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------------------------------------- | +| [1dc6992a5](https://github.com/angular/angular-cli/commit/1dc6992a5ae6c5a1f16f22f6c94690d5cf218c38) | fix | disallow x-forwarded-prefix starting with a backslash | +| [0a2ff0b2b](https://github.com/angular/angular-cli/commit/0a2ff0b2b3aceb228c9447c19fb762df742d7265) | fix | ensure unique values in redirect response Vary header | +| [cdbac82a8](https://github.com/angular/angular-cli/commit/cdbac82a85b35f24c70a062eeb8a13b521831019) | fix | support custom headers in redirect responses | + + + + + +# 20.3.20 (2026-03-11) + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------------------------- | +| [0fd6823af](https://github.com/angular/angular-cli/commit/0fd6823af0adec23f7c3f1d531f45f6432afe555) | fix | pass process environment variables to prerender workers | + + + + + +# 20.3.19 (2026-03-04) + +### @angular-devkit/build-angular + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------------------- | +| [0299b4d1a](https://github.com/angular/angular-cli/commit/0299b4d1aca13f11a06e2e92c593fe3e20906d23) | fix | update copy-webpack-plugin to v14.0.0 | + + + + + +# 20.3.18 (2026-02-26) + +### @angular-devkit/core + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ------------------------ | +| [39596d529](https://github.com/angular/angular-cli/commit/39596d529f831f72a2134bc3c9ac163867ff5702) | fix | update `ajv` to `8.18.0` | + +### @angular/build + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | ----------------------- | +| [f668e2778](https://github.com/angular/angular-cli/commit/f668e2778c4c4dbecc8a1c6831c092f5512d1ec1) | fix | update rollup to 4.59.0 | + + + + + +# 20.3.17 (2026-02-23) + +### @angular/ssr + +| Commit | Type | Description | +| --------------------------------------------------------------------------------------------------- | ---- | --------------------------------------------------- | +| [8700e18d7](https://github.com/angular/angular-cli/commit/8700e18d7cf175d80fe6ce6205589767b7870c1c) | fix | prevent open redirect via X-Forwarded-Prefix header | +| [67582a946](https://github.com/angular/angular-cli/commit/67582a946808d2c021cbcfacbf203ef58a6fbded) | fix | validate host headers to prevent header-based SSRF | + + + # 20.3.16 (2026-02-09) diff --git a/goldens/public-api/angular/ssr/index.api.md b/goldens/public-api/angular/ssr/index.api.md index 81764fcc1f62..1c2f8f2c625f 100644 --- a/goldens/public-api/angular/ssr/index.api.md +++ b/goldens/public-api/angular/ssr/index.api.md @@ -11,11 +11,19 @@ import { Type } from '@angular/core'; // @public export class AngularAppEngine { + constructor(options?: AngularAppEngineOptions); handle(request: Request, requestContext?: unknown): Promise; static ɵallowStaticRouteRender: boolean; + static ɵdisableAllowedHostsCheck: boolean; static ɵhooks: Hooks; } +// @public +export interface AngularAppEngineOptions { + allowedHosts?: readonly string[]; + trustProxyHeaders?: boolean | readonly string[]; +} + // @public export function createRequestHandler(handler: RequestHandlerFunction): RequestHandlerFunction; diff --git a/goldens/public-api/angular/ssr/node/index.api.md b/goldens/public-api/angular/ssr/node/index.api.md index eccb6396938e..2c1c5f4bf86a 100644 --- a/goldens/public-api/angular/ssr/node/index.api.md +++ b/goldens/public-api/angular/ssr/node/index.api.md @@ -15,8 +15,12 @@ import { Type } from '@angular/core'; // @public export class AngularNodeAppEngine { - constructor(); - handle(request: IncomingMessage | Http2ServerRequest, requestContext?: unknown): Promise; + constructor(options?: AngularNodeAppEngineOptions); + handle(request: IncomingMessage | Http2ServerRequest | Request, requestContext?: unknown): Promise; +} + +// @public +export interface AngularNodeAppEngineOptions extends AngularAppEngineOptions { } // @public @@ -27,6 +31,7 @@ export class CommonEngine { // @public (undocumented) export interface CommonEngineOptions { + allowedHosts?: readonly string[]; bootstrap?: Type<{}> | ((context: BootstrapContext) => Promise); enablePerformanceProfiler?: boolean; providers?: StaticProvider[]; @@ -50,7 +55,7 @@ export interface CommonEngineRenderOptions { export function createNodeRequestHandler(handler: T): T; // @public -export function createWebRequestFromNodeRequest(nodeRequest: IncomingMessage | Http2ServerRequest): Request; +export function createWebRequestFromNodeRequest(nodeRequest: IncomingMessage | Http2ServerRequest, trustProxyHeaders?: boolean | readonly string[]): Request; // @public export function isMainModule(url: string): boolean; diff --git a/package.json b/package.json index b76fb7e75829..105e77fb4d40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/devkit-repo", - "version": "20.3.16", + "version": "20.3.25", "private": true, "description": "Software Development Kit for Angular", "keywords": [ @@ -96,12 +96,12 @@ "@types/yarnpkg__lockfile": "^1.1.5", "@typescript-eslint/eslint-plugin": "8.39.1", "@typescript-eslint/parser": "8.39.1", - "ajv": "8.17.1", + "ajv": "8.18.0", "ansi-colors": "4.1.3", "beasties": "0.3.5", "buffer": "6.0.3", - "esbuild": "0.25.9", - "esbuild-wasm": "0.25.9", + "esbuild": "0.28.0", + "esbuild-wasm": "0.28.0", "eslint": "9.33.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-header": "3.1.1", @@ -132,7 +132,7 @@ "protractor": "~7.0.0", "puppeteer": "18.2.1", "quicktype-core": "23.2.6", - "rollup": "4.46.2", + "rollup": "4.59.0", "rollup-license-plugin": "~3.0.1", "semver": "7.7.2", "shelljs": "^0.10.0", diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json index ea57319b5721..cc7abb24e8bb 100644 --- a/packages/angular/build/package.json +++ b/packages/angular/build/package.json @@ -27,7 +27,7 @@ "@vitejs/plugin-basic-ssl": "2.1.0", "beasties": "0.3.5", "browserslist": "^4.23.0", - "esbuild": "0.25.9", + "esbuild": "0.28.0", "https-proxy-agent": "7.0.6", "istanbul-lib-instrument": "6.0.3", "jsonc-parser": "3.3.1", @@ -35,14 +35,14 @@ "magic-string": "0.30.17", "mrmime": "2.0.1", "parse5-html-rewriting-stream": "8.0.0", - "picomatch": "4.0.3", + "picomatch": "4.0.4", "piscina": "5.1.3", - "rollup": "4.52.3", + "rollup": "4.59.0", "sass": "1.90.0", "semver": "7.7.2", "source-map-support": "0.5.21", "tinyglobby": "0.2.14", - "vite": "7.1.11", + "vite": "7.3.2", "watchpack": "2.4.4" }, "optionalDependencies": { @@ -54,7 +54,7 @@ "jsdom": "26.1.0", "less": "4.4.0", "ng-packagr": "20.3.0", - "postcss": "8.5.6", + "postcss": "8.5.12", "rxjs": "7.8.2", "vitest": "3.2.4" }, diff --git a/packages/angular/build/src/builders/application/execute-build.ts b/packages/angular/build/src/builders/application/execute-build.ts index 0654cd965558..aaddc5b6ef7e 100644 --- a/packages/angular/build/src/builders/application/execute-build.ts +++ b/packages/angular/build/src/builders/application/execute-build.ts @@ -56,6 +56,7 @@ export async function executeBuild( verbose, colors, jsonLogs, + security, } = options; // TODO: Consider integrating into watch mode. Would require full rebuild on target changes. @@ -263,7 +264,7 @@ export async function executeBuild( if (serverEntryPoint) { executionResult.addOutputFile( SERVER_APP_ENGINE_MANIFEST_FILENAME, - generateAngularServerAppEngineManifest(i18nOptions, baseHref), + generateAngularServerAppEngineManifest(i18nOptions, security.allowedHosts, baseHref), BuildOutputFileType.ServerRoot, ); } diff --git a/packages/angular/build/src/builders/application/options.ts b/packages/angular/build/src/builders/application/options.ts index 25bd87253357..87bec31ed381 100644 --- a/packages/angular/build/src/builders/application/options.ts +++ b/packages/angular/build/src/builders/application/options.ts @@ -400,8 +400,9 @@ export async function normalizeOptions( } } - const autoCsp = options.security?.autoCsp; + const { autoCsp, allowedHosts = [] } = options.security ?? {}; const security = { + allowedHosts, autoCsp: autoCsp ? { unsafeEval: autoCsp === true ? false : !!autoCsp.unsafeEval, diff --git a/packages/angular/build/src/builders/application/schema.json b/packages/angular/build/src/builders/application/schema.json index 8db4e6145b3f..5498a21fe004 100644 --- a/packages/angular/build/src/builders/application/schema.json +++ b/packages/angular/build/src/builders/application/schema.json @@ -52,6 +52,14 @@ "type": "object", "additionalProperties": false, "properties": { + "allowedHosts": { + "description": "A list of hostnames that are allowed to access the server-side application. For more information, see https://angular.dev/best-practices/security#preventing-server-side-request-forgery-ssrf.", + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, "autoCsp": { "description": "Enables automatic generation of a hash-based Strict Content Security Policy (https://web.dev/articles/strict-csp#choose-hash) based on scripts in index.html. Will default to true once we are out of experimental/preview phases.", "default": false, diff --git a/packages/angular/build/src/builders/dev-server/vite-server.ts b/packages/angular/build/src/builders/dev-server/vite-server.ts index 7f8665dd8061..4df3cc3d4023 100644 --- a/packages/angular/build/src/builders/dev-server/vite-server.ts +++ b/packages/angular/build/src/builders/dev-server/vite-server.ts @@ -113,7 +113,16 @@ export async function* serveWithVite( } // Disable auto CSP. + const allowedHosts = Array.isArray(serverOptions.allowedHosts) + ? [...serverOptions.allowedHosts] + : []; + + // Always allow the dev server host + allowedHosts.push(serverOptions.host); + browserOptions.security = { + allowedHosts, + // Disable auto CSP. autoCsp: false, }; diff --git a/packages/angular/build/src/utils/server-rendering/manifest.ts b/packages/angular/build/src/utils/server-rendering/manifest.ts index 2dfad0ff2dfb..975f7718715d 100644 --- a/packages/angular/build/src/utils/server-rendering/manifest.ts +++ b/packages/angular/build/src/utils/server-rendering/manifest.ts @@ -53,11 +53,13 @@ function escapeUnsafeChars(str: string): string { * * @param i18nOptions - The internationalization options for the application build. This * includes settings for inlining locales and determining the output structure. + * @param allowedHosts - A list of hosts that are allowed to access the server-side application. * @param baseHref - The base HREF for the application. This is used to set the base URL * for all relative URLs in the application. */ export function generateAngularServerAppEngineManifest( i18nOptions: NormalizedApplicationBuildOptions['i18nOptions'], + allowedHosts: string[], baseHref: string | undefined, ): string { const entryPoints: Record = {}; @@ -84,6 +86,7 @@ export function generateAngularServerAppEngineManifest( const manifestContent = ` export default { basePath: '${basePath}', + allowedHosts: ${JSON.stringify(allowedHosts, undefined, 2)}, supportedLocales: ${JSON.stringify(supportedLocales, undefined, 2)}, entryPoints: { ${Object.entries(entryPoints) diff --git a/packages/angular/build/src/utils/server-rendering/prerender.ts b/packages/angular/build/src/utils/server-rendering/prerender.ts index f33f851f10c4..52890cac22ac 100644 --- a/packages/angular/build/src/utils/server-rendering/prerender.ts +++ b/packages/angular/build/src/utils/server-rendering/prerender.ts @@ -225,6 +225,10 @@ async function renderPages( hasSsrEntry: !!outputFilesForWorker['server.mjs'], } as RenderWorkerData, execArgv: workerExecArgv, + env: { + ...process.env, + 'NG_ALLOWED_HOSTS': 'localhost', + }, }); try { @@ -337,6 +341,10 @@ async function getAllRoutes( hasSsrEntry: !!outputFilesForWorker['server.mjs'], } as RoutesExtractorWorkerData, execArgv: workerExecArgv, + env: { + ...process.env, + 'NG_ALLOWED_HOSTS': 'localhost', + }, }); try { diff --git a/packages/angular/ssr/BUILD.bazel b/packages/angular/ssr/BUILD.bazel index 2218488daeb6..982bdfbb577b 100644 --- a/packages/angular/ssr/BUILD.bazel +++ b/packages/angular/ssr/BUILD.bazel @@ -20,7 +20,7 @@ ts_project( ), args = [ "--lib", - "dom,es2020", + "dom.iterable,dom,es2022", ], data = [ "//packages/angular/ssr/third_party/beasties:beasties_bundled", diff --git a/packages/angular/ssr/node/public_api.ts b/packages/angular/ssr/node/public_api.ts index 81932873a440..89900f95cc5c 100644 --- a/packages/angular/ssr/node/public_api.ts +++ b/packages/angular/ssr/node/public_api.ts @@ -12,7 +12,7 @@ export { type CommonEngineOptions, } from './src/common-engine/common-engine'; -export { AngularNodeAppEngine } from './src/app-engine'; +export { AngularNodeAppEngine, type AngularNodeAppEngineOptions } from './src/app-engine'; export { createNodeRequestHandler, type NodeRequestHandlerFunction } from './src/handler'; export { writeResponseToNodeResponse } from './src/response'; diff --git a/packages/angular/ssr/node/src/app-engine.ts b/packages/angular/ssr/node/src/app-engine.ts index 8edac0ef69c6..910a6d884e98 100644 --- a/packages/angular/ssr/node/src/app-engine.ts +++ b/packages/angular/ssr/node/src/app-engine.ts @@ -9,9 +9,16 @@ import { AngularAppEngine } from '@angular/ssr'; import type { IncomingMessage } from 'node:http'; import type { Http2ServerRequest } from 'node:http2'; +import { AngularAppEngineOptions } from '../../src/app-engine'; +import { getAllowedHostsFromEnv } from './environment-options'; import { attachNodeGlobalErrorHandlers } from './errors'; import { createWebRequestFromNodeRequest } from './request'; +/** + * Options for the Angular Node.js server application engine. + */ +export interface AngularNodeAppEngineOptions extends AngularAppEngineOptions {} + /** * Angular server application engine. * Manages Angular server applications (including localized ones), handles rendering requests, @@ -21,9 +28,20 @@ import { createWebRequestFromNodeRequest } from './request'; * application to ensure consistent handling of rendering requests and resource management. */ export class AngularNodeAppEngine { - private readonly angularAppEngine = new AngularAppEngine(); + private readonly angularAppEngine: AngularAppEngine; + private readonly trustProxyHeaders?: boolean | readonly string[]; + + /** + * Creates a new instance of the Angular Node.js server application engine. + * @param options Options for the Angular Node.js server application engine. + */ + constructor(options?: AngularNodeAppEngineOptions) { + this.angularAppEngine = new AngularAppEngine({ + ...options, + allowedHosts: [...getAllowedHostsFromEnv(), ...(options?.allowedHosts ?? [])], + }); + this.trustProxyHeaders = options?.trustProxyHeaders; - constructor() { attachNodeGlobalErrorHandlers(); } @@ -31,21 +49,38 @@ export class AngularNodeAppEngine { * Handles an incoming HTTP request by serving prerendered content, performing server-side rendering, * or delivering a static file for client-side rendered routes based on the `RenderMode` setting. * - * This method adapts Node.js's `IncomingMessage` or `Http2ServerRequest` + * This method adapts Node.js's `IncomingMessage`, `Http2ServerRequest` or `Request` * to a format compatible with the `AngularAppEngine` and delegates the handling logic to it. * - * @param request - The incoming HTTP request (`IncomingMessage` or `Http2ServerRequest`). + * @param request - The incoming HTTP request (`IncomingMessage`, `Http2ServerRequest` or `Request`). * @param requestContext - Optional context for rendering, such as metadata associated with the request. * @returns A promise that resolves to the resulting HTTP response object, or `null` if no matching Angular route is found. * * @remarks A request to `https://www.example.com/page/index.html` will serve or render the Angular route * corresponding to `https://www.example.com/page`. + * + * @remarks + * To prevent potential Server-Side Request Forgery (SSRF), this function verifies the hostname + * of the `request.url` against a list of authorized hosts. + * If the hostname is not recognized and `allowedHosts` is not empty, a Client-Side Rendered (CSR) version of the + * page is returned otherwise a 400 Bad Request is returned. + * + * Resolution: + * Authorize your hostname by configuring `allowedHosts` in `angular.json` in: + * `projects.[project-name].architect.build.options.security.allowedHosts`. + * Alternatively, you can define the allowed hostname via the environment variable `process.env['NG_ALLOWED_HOSTS']` + * or pass it directly through the configuration options of `AngularNodeAppEngine`. + * + * For more information see: https://angular.dev/best-practices/security#preventing-server-side-request-forgery-ssrf */ async handle( - request: IncomingMessage | Http2ServerRequest, + request: IncomingMessage | Http2ServerRequest | Request, requestContext?: unknown, ): Promise { - const webRequest = createWebRequestFromNodeRequest(request); + const webRequest = + request instanceof Request + ? request + : createWebRequestFromNodeRequest(request, this.trustProxyHeaders); return this.angularAppEngine.handle(webRequest, requestContext); } diff --git a/packages/angular/ssr/node/src/common-engine/common-engine.ts b/packages/angular/ssr/node/src/common-engine/common-engine.ts index 079c1187696b..1c130d9abe86 100644 --- a/packages/angular/ssr/node/src/common-engine/common-engine.ts +++ b/packages/angular/ssr/node/src/common-engine/common-engine.ts @@ -12,6 +12,8 @@ import { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/plat import * as fs from 'node:fs'; import { dirname, join, normalize, resolve } from 'node:path'; import { URL } from 'node:url'; +import { validateUrl } from '../../../src/utils/validation'; +import { getAllowedHostsFromEnv } from '../environment-options'; import { attachNodeGlobalErrorHandlers } from '../errors'; import { CommonEngineInlineCriticalCssProcessor } from './inline-css-processor'; import { @@ -31,6 +33,9 @@ export interface CommonEngineOptions { /** Enable request performance profiling data collection and printing the results in the server console. */ enablePerformanceProfiler?: boolean; + + /** A set of hostnames that are allowed to access the server. */ + allowedHosts?: readonly string[]; } export interface CommonEngineRenderOptions { @@ -64,8 +69,14 @@ export class CommonEngine { private readonly templateCache = new Map(); private readonly inlineCriticalCssProcessor = new CommonEngineInlineCriticalCssProcessor(); private readonly pageIsSSG = new Map(); + private readonly allowedHosts: ReadonlySet; constructor(private options?: CommonEngineOptions) { + this.allowedHosts = new Set([ + ...getAllowedHostsFromEnv(), + ...(this.options?.allowedHosts ?? []), + ]); + attachNodeGlobalErrorHandlers(); } @@ -74,6 +85,40 @@ export class CommonEngine { * render options */ async render(opts: CommonEngineRenderOptions): Promise { + const { url } = opts; + + if (url && URL.canParse(url)) { + const urlObj = new URL(url); + try { + validateUrl(urlObj, this.allowedHosts); + } catch (error) { + const isAllowedHostConfigured = this.allowedHosts.size > 0; + // eslint-disable-next-line no-console + console.error( + `ERROR: ${(error as Error).message}` + + 'Please provide a list of allowed hosts in the "allowedHosts" option in the "CommonEngine" constructor.', + isAllowedHostConfigured + ? '' + : '\nFalling back to client side rendering. This will become a 400 Bad Request in a future major version.', + ); + + if (!isAllowedHostConfigured) { + // Fallback to CSR to avoid a breaking change. + // TODO(alanagius): Return a 400 and remove this fallback in the next major version (v22). + let document = opts.document; + if (!document && opts.documentFilePath) { + document = opts.document ?? (await this.getDocument(opts.documentFilePath)); + } + + if (document) { + return document; + } + } + + throw error; + } + } + const enablePerformanceProfiler = this.options?.enablePerformanceProfiler; const runMethod = enablePerformanceProfiler diff --git a/packages/angular/ssr/node/src/environment-options.ts b/packages/angular/ssr/node/src/environment-options.ts new file mode 100644 index 000000000000..b18dcd08ebe6 --- /dev/null +++ b/packages/angular/ssr/node/src/environment-options.ts @@ -0,0 +1,29 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +/** + * Retrieves the list of allowed hosts from the environment variable `NG_ALLOWED_HOSTS`. + * @returns An array of allowed hosts. + */ +export function getAllowedHostsFromEnv(): ReadonlyArray { + const allowedHosts: string[] = []; + const envNgAllowedHosts = process.env['NG_ALLOWED_HOSTS']; + if (!envNgAllowedHosts) { + return allowedHosts; + } + + const hosts = envNgAllowedHosts.split(','); + for (const host of hosts) { + const trimmed = host.trim(); + if (trimmed.length > 0) { + allowedHosts.push(trimmed); + } + } + + return allowedHosts; +} diff --git a/packages/angular/ssr/node/src/request.ts b/packages/angular/ssr/node/src/request.ts index 32d90d0029fc..dd49a3d8b24a 100644 --- a/packages/angular/ssr/node/src/request.ts +++ b/packages/angular/ssr/node/src/request.ts @@ -8,6 +8,11 @@ import type { IncomingHttpHeaders, IncomingMessage } from 'node:http'; import type { Http2ServerRequest } from 'node:http2'; +import { + getFirstHeaderValue, + isProxyHeaderAllowed, + normalizeTrustProxyHeaders, +} from '../../src/utils/validation'; /** * A set containing all the pseudo-headers defined in the HTTP/2 specification. @@ -16,7 +21,13 @@ import type { Http2ServerRequest } from 'node:http2'; * as they are not allowed to be set directly using the `Node.js` Undici API or * the web `Headers` API. */ -const HTTP2_PSEUDO_HEADERS = new Set([':method', ':scheme', ':authority', ':path', ':status']); +const HTTP2_PSEUDO_HEADERS: ReadonlySet = new Set([ + ':method', + ':scheme', + ':authority', + ':path', + ':status', +]); /** * Converts a Node.js `IncomingMessage` or `Http2ServerRequest` into a @@ -26,16 +37,25 @@ const HTTP2_PSEUDO_HEADERS = new Set([':method', ':scheme', ':authority', ':path * be used by web platform APIs. * * @param nodeRequest - The Node.js request object (`IncomingMessage` or `Http2ServerRequest`) to convert. + * @param trustProxyHeaders - A boolean or an array of proxy headers to trust when constructing the request URL. + * + * @remarks + * When `trustProxyHeaders` is enabled, headers such as `X-Forwarded-Host` and + * `X-Forwarded-Prefix` should ideally be strictly validated at a higher infrastructure + * level (e.g., at the reverse proxy or API gateway) before reaching the application. + * * @returns A Web Standard `Request` object. */ export function createWebRequestFromNodeRequest( nodeRequest: IncomingMessage | Http2ServerRequest, + trustProxyHeaders?: boolean | readonly string[], ): Request { + const trustProxyHeadersNormalized = normalizeTrustProxyHeaders(trustProxyHeaders); const { headers, method = 'GET' } = nodeRequest; const withBody = method !== 'GET' && method !== 'HEAD'; const referrer = headers.referer && URL.canParse(headers.referer) ? headers.referer : undefined; - return new Request(createRequestUrl(nodeRequest), { + return new Request(createRequestUrl(nodeRequest, trustProxyHeadersNormalized), { method, headers: createRequestHeaders(headers), body: withBody ? nodeRequest : undefined, @@ -74,20 +94,34 @@ function createRequestHeaders(nodeHeaders: IncomingHttpHeaders): Headers { * Creates a `URL` object from a Node.js `IncomingMessage`, taking into account the protocol, host, and port. * * @param nodeRequest - The Node.js `IncomingMessage` or `Http2ServerRequest` object to extract URL information from. + * @param trustProxyHeaders - A set of allowed proxy headers. + * + * @remarks + * When `trustProxyHeaders` is enabled, headers such as `X-Forwarded-Host` and + * `X-Forwarded-Prefix` should ideally be strictly validated at a higher infrastructure + * level (e.g., at the reverse proxy or API gateway) before reaching the application. + * * @returns A `URL` object representing the request URL. */ -export function createRequestUrl(nodeRequest: IncomingMessage | Http2ServerRequest): URL { +export function createRequestUrl( + nodeRequest: IncomingMessage | Http2ServerRequest, + trustProxyHeaders: ReadonlySet, +): URL { const { headers, socket, url = '', originalUrl, } = nodeRequest as IncomingMessage & { originalUrl?: string }; + const protocol = - getFirstHeaderValue(headers['x-forwarded-proto']) ?? + getAllowedProxyHeaderValue(headers, 'x-forwarded-proto', trustProxyHeaders) ?? ('encrypted' in socket && socket.encrypted ? 'https' : 'http'); + const hostname = - getFirstHeaderValue(headers['x-forwarded-host']) ?? headers.host ?? headers[':authority']; + getAllowedProxyHeaderValue(headers, 'x-forwarded-host', trustProxyHeaders) ?? + headers.host ?? + headers[':authority']; if (Array.isArray(hostname)) { throw new Error('host value cannot be an array.'); @@ -95,7 +129,7 @@ export function createRequestUrl(nodeRequest: IncomingMessage | Http2ServerReque let hostnameWithPort = hostname; if (!hostname?.includes(':')) { - const port = getFirstHeaderValue(headers['x-forwarded-port']); + const port = getAllowedProxyHeaderValue(headers, 'x-forwarded-port', trustProxyHeaders); if (port) { hostnameWithPort += `:${port}`; } @@ -105,19 +139,19 @@ export function createRequestUrl(nodeRequest: IncomingMessage | Http2ServerReque } /** - * Extracts the first value from a multi-value header string. - * - * @param value - A string or an array of strings representing the header values. - * If it's a string, values are expected to be comma-separated. - * @returns The first trimmed value from the multi-value header, or `undefined` if the input is invalid or empty. + * Gets the first value of an allowed proxy header. * - * @example - * ```typescript - * getFirstHeaderValue("value1, value2, value3"); // "value1" - * getFirstHeaderValue(["value1", "value2"]); // "value1" - * getFirstHeaderValue(undefined); // undefined - * ``` + * @param headers - The Node.js incoming HTTP headers. + * @param headerName - The name of the proxy header to retrieve. + * @param trustProxyHeaders - A set of allowed proxy headers. + * @returns The value of the allowed proxy header, or `undefined` if not allowed or not present. */ -function getFirstHeaderValue(value: string | string[] | undefined): string | undefined { - return value?.toString().split(',', 1)[0]?.trim(); +function getAllowedProxyHeaderValue( + headers: IncomingHttpHeaders, + headerName: string, + trustProxyHeaders: ReadonlySet, +): string | undefined { + return isProxyHeaderAllowed(headerName, trustProxyHeaders) + ? getFirstHeaderValue(headers[headerName]) + : undefined; } diff --git a/packages/angular/ssr/node/test/BUILD.bazel b/packages/angular/ssr/node/test/BUILD.bazel index 5a7c45126966..9610a17557b4 100644 --- a/packages/angular/ssr/node/test/BUILD.bazel +++ b/packages/angular/ssr/node/test/BUILD.bazel @@ -7,6 +7,7 @@ ts_project( srcs = glob(["**/*_spec.ts"]), deps = [ "//:node_modules/@types/node", + "//packages/angular/ssr", "//packages/angular/ssr/node", ], ) diff --git a/packages/angular/ssr/node/test/request_spec.ts b/packages/angular/ssr/node/test/request_spec.ts index 952faefd9f28..982ae898624b 100644 --- a/packages/angular/ssr/node/test/request_spec.ts +++ b/packages/angular/ssr/node/test/request_spec.ts @@ -7,8 +7,8 @@ */ import { IncomingMessage } from 'node:http'; -import { Http2ServerRequest } from 'node:http2'; import { Socket } from 'node:net'; +import { normalizeTrustProxyHeaders } from '../../src/utils/validation'; import { createRequestUrl } from '../src/request'; // Helper to create a mock request object for testing. @@ -26,18 +26,6 @@ function createRequest(details: { } as unknown as IncomingMessage; } -// Helper to create a mock Http2ServerRequest object for testing. -function createHttp2Request(details: { - headers: Record; - url?: string; -}): Http2ServerRequest { - return { - headers: details.headers, - socket: new Socket(), - url: details.url, - } as Http2ServerRequest; -} - describe('createRequestUrl', () => { it('should create a http URL with hostname and port from the host header', () => { const url = createRequestUrl( @@ -45,6 +33,7 @@ describe('createRequestUrl', () => { headers: { host: 'localhost:8080' }, url: '/test', }), + new Set(), ); expect(url.href).toBe('http://localhost:8080/test'); }); @@ -56,6 +45,7 @@ describe('createRequestUrl', () => { encryptedSocket: true, url: '/test', }), + new Set(), ); expect(url.href).toBe('https://example.com/test'); }); @@ -67,6 +57,7 @@ describe('createRequestUrl', () => { encryptedSocket: true, url: '', }), + new Set(), ); expect(url.href).toBe('https://example.com/'); }); @@ -78,6 +69,7 @@ describe('createRequestUrl', () => { encryptedSocket: true, url: '/test?a=1', }), + new Set(), ); expect(url.href).toBe('https://example.com/test?a=1'); }); @@ -90,6 +82,7 @@ describe('createRequestUrl', () => { url: '/test', originalUrl: '/original', }), + new Set(), ); expect(url.href).toBe('https://example.com/original'); }); @@ -102,6 +95,7 @@ describe('createRequestUrl', () => { url: undefined, originalUrl: undefined, }), + new Set(), ); expect(url.href).toBe('https://example.com/'); }); @@ -112,6 +106,7 @@ describe('createRequestUrl', () => { headers: { host: 'localhost:8080' }, url: '//example.com/test', }), + new Set(), ); expect(url.href).toBe('http://localhost:8080//example.com/test'); }); @@ -123,6 +118,7 @@ describe('createRequestUrl', () => { url: '/test', originalUrl: '//example.com/original', }), + new Set(), ); expect(url.href).toBe('http://localhost:8080//example.com/original'); }); @@ -137,6 +133,7 @@ describe('createRequestUrl', () => { }, url: '/test', }), + normalizeTrustProxyHeaders(true), ); expect(url.href).toBe('https://example.com/test'); }); @@ -152,6 +149,7 @@ describe('createRequestUrl', () => { }, url: '/test', }), + normalizeTrustProxyHeaders(true), ); expect(url.href).toBe('https://example.com:8443/test'); }); diff --git a/packages/angular/ssr/public_api.ts b/packages/angular/ssr/public_api.ts index e685f4ceabe3..e566d8414f2f 100644 --- a/packages/angular/ssr/public_api.ts +++ b/packages/angular/ssr/public_api.ts @@ -8,7 +8,7 @@ export * from './private_export'; -export { AngularAppEngine } from './src/app-engine'; +export { AngularAppEngine, type AngularAppEngineOptions } from './src/app-engine'; export { createRequestHandler, type RequestHandlerFunction } from './src/handler'; export { diff --git a/packages/angular/ssr/src/app-engine.ts b/packages/angular/ssr/src/app-engine.ts index dd204a4b595f..d65977a1a927 100644 --- a/packages/angular/ssr/src/app-engine.ts +++ b/packages/angular/ssr/src/app-engine.ts @@ -11,6 +11,37 @@ import { Hooks } from './hooks'; import { getPotentialLocaleIdFromUrl, getPreferredLocale } from './i18n'; import { EntryPointExports, getAngularAppEngineManifest } from './manifest'; import { joinUrlParts } from './utils/url'; +import { + normalizeTrustProxyHeaders, + sanitizeRequestHeaders, + validateRequest, +} from './utils/validation'; + +/** + * Options for the Angular server application engine. + */ +export interface AngularAppEngineOptions { + /** + * A set of allowed hostnames for the server application. + */ + allowedHosts?: readonly string[]; + + /** + * Extends the scope of trusted proxy headers (`X-Forwarded-*`). + * + * @remarks + * When `trustProxyHeaders` is enabled, headers such as `X-Forwarded-Host` and + * `X-Forwarded-Prefix` should ideally be strictly validated at a higher infrastructure + * level (e.g., at the reverse proxy or API gateway) before reaching the application. + * + * If a `string[]` is provided, only those proxy headers are allowed. + * If `true`, all proxy headers are allowed. + * If `false`, proxy headers are ignored. + * + * @default undefined + */ + trustProxyHeaders?: boolean | readonly string[]; +} /** * Angular server application engine. @@ -40,11 +71,23 @@ export class AngularAppEngine { */ static ɵhooks = /* #__PURE__*/ new Hooks(); + /** + * A flag to disable the allowed hosts check. + * + * @private + */ + static ɵdisableAllowedHostsCheck = false; + /** * The manifest for the server application. */ private readonly manifest = getAngularAppEngineManifest(); + /** + * A set of allowed hostnames for the server application. + */ + private readonly allowedHosts: ReadonlySet; + /** * A map of supported locales from the server application's manifest. */ @@ -52,11 +95,40 @@ export class AngularAppEngine { this.manifest.supportedLocales, ); + /** + * The normalized allowed proxy headers. + */ + private readonly trustProxyHeaders: ReadonlySet; + /** * A cache that holds entry points, keyed by their potential locale string. */ private readonly entryPointsCache = new Map>(); + /** + * Creates a new instance of the Angular server application engine. + * @param options Options for the Angular server application engine. + */ + constructor(options?: AngularAppEngineOptions) { + this.allowedHosts = this.getAllowedHosts(options); + this.trustProxyHeaders = normalizeTrustProxyHeaders(options?.trustProxyHeaders); + } + + private getAllowedHosts(options: AngularAppEngineOptions | undefined): ReadonlySet { + const allowedHosts = new Set([...(options?.allowedHosts ?? []), ...this.manifest.allowedHosts]); + + if (allowedHosts.has('*')) { + // eslint-disable-next-line no-console + console.warn( + 'Allowing all hosts via "*" is a security risk. This configuration should only be used when ' + + 'validation for "Host" and "X-Forwarded-Host" headers is performed in another layer, such as a load balancer or reverse proxy. ' + + 'For more information see: https://angular.dev/best-practices/security#preventing-server-side-request-forgery-ssrf', + ); + } + + return allowedHosts; + } + /** * Handles an incoming HTTP request by serving prerendered content, performing server-side rendering, * or delivering a static file for client-side rendered routes based on the `RenderMode` setting. @@ -67,12 +139,39 @@ export class AngularAppEngine { * * @remarks A request to `https://www.example.com/page/index.html` will serve or render the Angular route * corresponding to `https://www.example.com/page`. + * + * @remarks + * To prevent potential Server-Side Request Forgery (SSRF), this function verifies the hostname + * of the `request.url` against a list of authorized hosts. + * If the hostname is not recognized and `allowedHosts` is not empty, a Client-Side Rendered (CSR) version of the + * page is returned otherwise a 400 Bad Request is returned. + * Resolution: + * Authorize your hostname by configuring `allowedHosts` in `angular.json` in: + * `projects.[project-name].architect.build.options.security.allowedHosts`. + * Alternatively, you pass it directly through the configuration options of `AngularAppEngine`. + * + * For more information see: https://angular.dev/best-practices/security#preventing-server-side-request-forgery-ssrf */ async handle(request: Request, requestContext?: unknown): Promise { - const serverApp = await this.getAngularServerAppForRequest(request); + const allowedHost = this.allowedHosts; + const { request: securedRequest, deoptToCSR } = sanitizeRequestHeaders( + request, + this.trustProxyHeaders, + ); + try { + validateRequest(securedRequest, allowedHost, AngularAppEngine.ɵdisableAllowedHostsCheck); + } catch (error) { + return this.handleValidationError(error as Error, securedRequest); + } + + const serverApp = await this.getAngularServerAppForRequest(securedRequest); if (serverApp) { - return serverApp.handle(request, requestContext); + if (deoptToCSR) { + return serverApp.serveClientSidePage(); + } + + return serverApp.handle(securedRequest, requestContext); } if (this.supportedLocales.length > 1) { @@ -201,4 +300,42 @@ export class AngularAppEngine { return this.getEntryPointExports(potentialLocale) ?? this.getEntryPointExports(''); } + + /** + * Handles validation errors by logging the error and returning an appropriate response. + * + * @param error - The validation error to handle. + * @param request - The HTTP request that caused the validation error. + * @returns A promise that resolves to a `Response` object with a 400 status code if allowed hosts are configured, + * or `null` if allowed hosts are not configured (in which case the request is served client-side). + */ + private async handleValidationError(error: Error, request: Request): Promise { + const isAllowedHostConfigured = this.allowedHosts.size > 0; + const errorMessage = error.message; + + // eslint-disable-next-line no-console + console.error( + `ERROR: Bad Request ("${request.url}").\n` + + errorMessage + + (isAllowedHostConfigured + ? '' + : '\nFalling back to client side rendering. This will become a 400 Bad Request in a future major version.') + + '\n\nFor more information, see https://angular.dev/best-practices/security#preventing-server-side-request-forgery-ssrf', + ); + + if (isAllowedHostConfigured) { + // Allowed hosts has been configured incorrectly, thus we can return a 400 bad request. + return new Response(errorMessage, { + status: 400, + statusText: 'Bad Request', + headers: { 'Content-Type': 'text/plain' }, + }); + } + + // Fallback to CSR to avoid a breaking change. + // TODO(alanagius): Return a 400 and remove this fallback in the next major version (v22). + const serverApp = await this.getAngularServerAppForRequest(request); + + return serverApp?.serveClientSidePage() ?? null; + } } diff --git a/packages/angular/ssr/src/app.ts b/packages/angular/ssr/src/app.ts index ee14f8a26105..c2aea694bb71 100644 --- a/packages/angular/ssr/src/app.ts +++ b/packages/angular/ssr/src/app.ts @@ -25,6 +25,7 @@ import { InlineCriticalCssProcessor } from './utils/inline-critical-css'; import { LRUCache } from './utils/lru-cache'; import { AngularBootstrap, renderAngular } from './utils/ng'; import { promiseWithAbort } from './utils/promise'; +import { createRedirectResponse } from './utils/redirect'; import { buildPathWithParams, joinUrlParts, stripLeadingSlash } from './utils/url'; /** @@ -174,8 +175,7 @@ export class AngularServerApp { return null; } - const { redirectTo, status, renderMode } = matchedRoute; - + const { redirectTo, status, renderMode, headers, preload } = matchedRoute; if (redirectTo !== undefined) { return createRedirectResponse( joinUrlParts( @@ -183,6 +183,7 @@ export class AngularServerApp { buildPathWithParams(redirectTo, url.pathname), ), status, + headers, ); } @@ -336,7 +337,7 @@ export class AngularServerApp { } if (result.redirectTo) { - return createRedirectResponse(result.redirectTo, status); + return createRedirectResponse(result.redirectTo, responseInit.status, headers); } if (renderMode === RenderMode.Prerender) { @@ -468,6 +469,27 @@ export class AngularServerApp { return html; } + + /** + * Serves the client-side version of the application. + * TODO(alanagius): Remove this method in version 22. + * @internal + */ + async serveClientSidePage(): Promise { + const { + manifest: { locale }, + assets, + } = this; + + const html = await assets.getServerAsset('index.csr.html').text(); + + return new Response(html, { + headers: new Headers({ + 'Content-Type': 'text/html;charset=UTF-8', + ...(locale !== undefined ? { 'Content-Language': locale } : {}), + }), + }); + } } let angularServerApp: AngularServerApp | undefined; @@ -531,20 +553,3 @@ function appendPreloadHintsToHtml(html: string, preload: readonly string[]): str html.slice(bodyCloseIdx), ].join('\n'); } - -/** - * Creates an HTTP redirect response with a specified location and status code. - * - * @param location - The URL to which the response should redirect. - * @param status - The HTTP status code for the redirection. Defaults to 302 (Found). - * See: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static#status - * @returns A `Response` object representing the HTTP redirect. - */ -function createRedirectResponse(location: string, status = 302): Response { - return new Response(null, { - status, - headers: { - 'Location': location, - }, - }); -} diff --git a/packages/angular/ssr/src/manifest.ts b/packages/angular/ssr/src/manifest.ts index 0de603bba104..21ded49b3e10 100644 --- a/packages/angular/ssr/src/manifest.ts +++ b/packages/angular/ssr/src/manifest.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.dev/license */ -import type { BootstrapContext } from '@angular/platform-browser'; import type { SerializableRouteTreeNode } from './routes/route-tree'; import { AngularBootstrap } from './utils/ng'; @@ -74,6 +73,11 @@ export interface AngularAppEngineManifest { * - `value`: The url segment associated with that locale. */ readonly supportedLocales: Readonly>; + + /** + * A readonly array of allowed hostnames. + */ + readonly allowedHosts: Readonly; } /** diff --git a/packages/angular/ssr/src/utils/redirect.ts b/packages/angular/ssr/src/utils/redirect.ts new file mode 100644 index 000000000000..79fb10f424dc --- /dev/null +++ b/packages/angular/ssr/src/utils/redirect.ts @@ -0,0 +1,71 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +/** + * An set of HTTP status codes that are considered valid for redirect responses. + */ +export const VALID_REDIRECT_RESPONSE_CODES: Set = new Set([301, 302, 303, 307, 308]); + +/** + * Checks if the given HTTP status code is a valid redirect response code. + * + * @param code The HTTP status code to check. + * @returns `true` if the code is a valid redirect response code, `false` otherwise. + */ +export function isValidRedirectResponseCode(code: number): boolean { + return VALID_REDIRECT_RESPONSE_CODES.has(code); +} + +/** + * Creates an HTTP redirect response with a specified location and status code. + * + * @param location - The URL to which the response should redirect. + * @param status - The HTTP status code for the redirection. Defaults to 302 (Found). + * See: https://developer.mozilla.org/en-US/docs/Web/API/Response/redirect_static#status + * @param headers - Additional headers to include in the response. + * @returns A `Response` object representing the HTTP redirect. + */ +export function createRedirectResponse( + location: string, + status = 302, + headers?: Record, +): Response { + if (ngDevMode && !isValidRedirectResponseCode(status)) { + throw new Error( + `Invalid redirect status code: ${status}. ` + + `Please use one of the following redirect response codes: ${[...VALID_REDIRECT_RESPONSE_CODES.values()].join(', ')}.`, + ); + } + + const resHeaders = new Headers(headers); + if (ngDevMode && resHeaders.has('location')) { + // eslint-disable-next-line no-console + console.warn( + `Location header "${resHeaders.get('location')}" will ignored and set to "${location}".`, + ); + } + + // Ensure unique values for Vary header + const varyArray = resHeaders.get('Vary')?.split(',') ?? []; + const varySet = new Set(['X-Forwarded-Prefix']); + for (const vary of varyArray) { + const value = vary.trim(); + + if (value) { + varySet.add(value); + } + } + + resHeaders.set('Vary', [...varySet].join(', ')); + resHeaders.set('Location', location); + + return new Response(null, { + status, + headers: resHeaders, + }); +} diff --git a/packages/angular/ssr/src/utils/url.ts b/packages/angular/ssr/src/utils/url.ts index faabf15b1bd4..caf7f3aad310 100644 --- a/packages/angular/ssr/src/utils/url.ts +++ b/packages/angular/ssr/src/utils/url.ts @@ -95,26 +95,32 @@ export function addTrailingSlash(url: string): string { * ``` */ export function joinUrlParts(...parts: string[]): string { - const normalizeParts: string[] = []; + const normalizedParts: string[] = []; + for (const part of parts) { if (part === '') { // Skip any empty parts continue; } - let normalizedPart = part; - if (part[0] === '/') { - normalizedPart = normalizedPart.slice(1); + let start = 0; + let end = part.length; + + // Use "Pointers" to avoid intermediate slices + while (start < end && part[start] === '/') { + start++; } - if (part[part.length - 1] === '/') { - normalizedPart = normalizedPart.slice(0, -1); + + while (end > start && part[end - 1] === '/') { + end--; } - if (normalizedPart !== '') { - normalizeParts.push(normalizedPart); + + if (start < end) { + normalizedParts.push(part.slice(start, end)); } } - return addLeadingSlash(normalizeParts.join('/')); + return addLeadingSlash(normalizedParts.join('/')); } /** diff --git a/packages/angular/ssr/src/utils/validation.ts b/packages/angular/ssr/src/utils/validation.ts new file mode 100644 index 000000000000..d27a52f51b9f --- /dev/null +++ b/packages/angular/ssr/src/utils/validation.ts @@ -0,0 +1,267 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +/** + * Common X-Forwarded-* headers. + */ +const X_FORWARDED_HEADERS = new Set([ + 'x-forwarded-for', + 'x-forwarded-host', + 'x-forwarded-port', + 'x-forwarded-proto', + 'x-forwarded-prefix', +]); + +/** + * The set of headers that should be validated for host header injection attacks. + */ +const HOST_HEADERS_TO_VALIDATE: ReadonlyArray = ['host', 'x-forwarded-host']; + +/** + * Regular expression to validate that the port is a numeric value. + */ +const VALID_PORT_REGEX = /^\d+$/; + +/** + * Regular expression to validate that the protocol is either http or https (case-insensitive). + */ +const VALID_PROTO_REGEX = /^https?$/i; + +/** + * Regular expression to validate that the prefix is valid. + */ +const VALID_PREFIX_REGEX = /^\/([a-z0-9_-]+\/)*[a-z0-9_-]*$/i; + +/** + * Extracts the first value from a multi-value header string. + * + * @param value - A string or an array of strings representing the header values. + * If it's a string, values are expected to be comma-separated. + * @returns The first trimmed value from the multi-value header, or `undefined` if the input is invalid or empty. + * + * @example + * ```typescript + * getFirstHeaderValue("value1, value2, value3"); // "value1" + * getFirstHeaderValue(["value1", "value2"]); // "value1" + * getFirstHeaderValue(undefined); // undefined + * ``` + */ +export function getFirstHeaderValue( + value: string | string[] | undefined | null, +): string | undefined { + return value?.toString().split(',', 1)[0]?.trim(); +} + +/** + * Validates a request. + * + * @param request - The incoming `Request` object to validate. + * @param allowedHosts - A set of allowed hostnames. + * @throws Error if any of the validated headers contain invalid values. + */ +export function validateRequest( + request: Request, + allowedHosts: ReadonlySet, + disableHostCheck: boolean, +): void { + validateHeaders(request, allowedHosts, disableHostCheck); + + if (!disableHostCheck) { + validateUrl(new URL(request.url), allowedHosts); + } +} + +/** + * Validates that the hostname of a given URL is allowed. + * + * @param url - The URL object to validate. + * @param allowedHosts - A set of allowed hostnames. + * @throws Error if the hostname is not in the allowlist. + */ +export function validateUrl(url: URL, allowedHosts: ReadonlySet): void { + const { hostname } = url; + if (!isHostAllowed(hostname, allowedHosts)) { + throw new Error(`URL with hostname "${hostname}" is not allowed.`); + } +} + +/** + * Sanitizes the proxy headers of a request by removing unallowed `X-Forwarded-*` headers. + * If no headers need to be removed, the original request is returned without cloning. + * + * @param request - The incoming `Request` object to sanitize. + * @param trustProxyHeaders - A set of allowed proxy headers. + * @returns An object containing the sanitized request, or the original request if no changes were needed. + */ +export function sanitizeRequestHeaders( + request: Request, + trustProxyHeaders: ReadonlySet, +): { request: Request; deoptToCSR: boolean } { + const keysToDelete: string[] = []; + let deoptToCSR = false; + + for (const [key] of request.headers) { + const lowerKey = key.toLowerCase(); + if (lowerKey.startsWith('x-forwarded-') && !isProxyHeaderAllowed(lowerKey, trustProxyHeaders)) { + // eslint-disable-next-line no-console + console.warn( + `Received "${key}" header but "trustProxyHeaders" was not set up to allow it.\n` + + `For more information, see https://angular.dev/best-practices/security#configuring-trusted-proxy-headers`, + ); + deoptToCSR = true; + keysToDelete.push(key); + } + } + + if (keysToDelete.length === 0) { + return { request, deoptToCSR }; + } + + const clonedReq = new Request(request.clone(), { + signal: request.signal, + }); + + const headers = clonedReq.headers; + for (const key of keysToDelete) { + headers.delete(key); + } + + return { request: clonedReq, deoptToCSR }; +} + +/** + * Validates a specific host header value against the allowed hosts. + * + * @param headerName - The name of the header to validate (e.g., 'host', 'x-forwarded-host'). + * @param headerValue - The value of the header to validate. + * @param allowedHosts - A set of allowed hostnames. + * @throws Error if the header value is invalid or the hostname is not in the allowlist. + */ +function verifyHostAllowed( + headerName: string, + headerValue: string, + allowedHosts: ReadonlySet, +): void { + const url = `http://${headerValue}`; + if (!URL.canParse(url)) { + throw new Error(`Header "${headerName}" contains an invalid value and cannot be parsed.`); + } + + const { hostname, pathname, search, hash, username, password } = new URL(url); + if (pathname !== '/' || search || hash || username || password) { + throw new Error( + `Header "${headerName}" with value "${headerValue}" contains characters that are not allowed.`, + ); + } + + if (!isHostAllowed(hostname, allowedHosts)) { + throw new Error(`Header "${headerName}" with value "${headerValue}" is not allowed.`); + } +} + +/** + * Checks if the hostname is allowed. + * @param hostname - The hostname to check. + * @param allowedHosts - A set of allowed hostnames. + * @returns `true` if the hostname is allowed, `false` otherwise. + */ +function isHostAllowed(hostname: string, allowedHosts: ReadonlySet): boolean { + if (allowedHosts.has(hostname)) { + return true; + } + + for (const allowedHost of allowedHosts) { + if (!allowedHost.startsWith('*.')) { + continue; + } + + const domain = allowedHost.slice(1); + if (hostname.endsWith(domain)) { + return true; + } + } + + return false; +} + +/** + * Validates the headers of an incoming request. + * + * @param request - The incoming `Request` object containing the headers to validate. + * @param allowedHosts - A set of allowed hostnames. + * @param disableHostCheck - Whether to disable the host check. + * @throws Error if any of the validated headers contain invalid values. + */ +function validateHeaders( + request: Request, + allowedHosts: ReadonlySet, + disableHostCheck: boolean, +): void { + const headers = request.headers; + for (const headerName of HOST_HEADERS_TO_VALIDATE) { + const headerValue = getFirstHeaderValue(headers.get(headerName)); + if (headerValue && !disableHostCheck) { + verifyHostAllowed(headerName, headerValue, allowedHosts); + } + } + + const xForwardedPort = getFirstHeaderValue(headers.get('x-forwarded-port')); + if (xForwardedPort && !VALID_PORT_REGEX.test(xForwardedPort)) { + throw new Error('Header "x-forwarded-port" must be a numeric value.'); + } + + const xForwardedProto = getFirstHeaderValue(headers.get('x-forwarded-proto')); + if (xForwardedProto && !VALID_PROTO_REGEX.test(xForwardedProto)) { + throw new Error('Header "x-forwarded-proto" must be either "http" or "https".'); + } + + const xForwardedPrefix = getFirstHeaderValue(headers.get('x-forwarded-prefix')); + if (xForwardedPrefix && !VALID_PREFIX_REGEX.test(xForwardedPrefix)) { + throw new Error( + 'Header "x-forwarded-prefix" is invalid. It must start with a "/" and contain ' + + 'only alphanumeric characters, hyphens, and underscores, separated by single slashes.', + ); + } +} + +/** + * Checks if a specific proxy header is allowed. + * + * @param headerName - The name of the proxy header to check. + * @param trustProxyHeaders - A set of allowed proxy headers. + * @returns `true` if the header is allowed, `false` otherwise. + */ +export function isProxyHeaderAllowed( + headerName: string, + trustProxyHeaders: ReadonlySet, +): boolean { + return trustProxyHeaders.has(headerName.toLowerCase()); +} + +/** + * Normalizes the `trustProxyHeaders` option to a consistent representation. + * @param trustProxyHeaders The input `trustProxyHeaders` value. + * @returns A `Set` of normalized header names. + */ +export function normalizeTrustProxyHeaders( + trustProxyHeaders: boolean | readonly string[] | undefined, +): ReadonlySet { + if (trustProxyHeaders === undefined) { + return new Set(['x-forwarded-host', 'x-forwarded-proto']); + } + + if (trustProxyHeaders === false) { + return new Set(); + } + + if (trustProxyHeaders === true) { + return X_FORWARDED_HEADERS; + } + + return new Set(trustProxyHeaders.map((h) => h.toLowerCase())); +} diff --git a/packages/angular/ssr/test/app-engine_spec.ts b/packages/angular/ssr/test/app-engine_spec.ts index 76b68f6d4a82..2f8359a8b0d8 100644 --- a/packages/angular/ssr/test/app-engine_spec.ts +++ b/packages/angular/ssr/test/app-engine_spec.ts @@ -11,13 +11,26 @@ import '@angular/compiler'; /* eslint-enable import/no-unassigned-import */ -import { Component } from '@angular/core'; +import { Component, REQUEST, inject } from '@angular/core'; import { destroyAngularServerApp, getOrCreateAngularServerApp } from '../src/app'; import { AngularAppEngine } from '../src/app-engine'; import { setAngularAppEngineManifest } from '../src/manifest'; import { RenderMode } from '../src/routes/route-config'; import { setAngularAppTestingManifest } from './testing-utils'; +@Component({ + selector: 'app-home', + template: 'Home works', +}) +class TestHomeComponent { + private request = inject(REQUEST); + constructor() { + // Force header access to trigger validation + this.request?.headers.get('host'); + this.request?.headers.get('x-forwarded-host'); + } +} + function createEntryPoint(locale: string) { return async () => { @Component({ @@ -84,6 +97,7 @@ describe('AngularAppEngine', () => { describe('Localized app', () => { beforeAll(() => { setAngularAppEngineManifest({ + allowedHosts: ['example.com'], // Note: Although we are testing only one locale, we need to configure two or more // to ensure that we test a different code path. entryPoints: { @@ -94,7 +108,7 @@ describe('AngularAppEngine', () => { basePath: '/', }); - appEngine = new AngularAppEngine(); + appEngine = new AngularAppEngine({ trustProxyHeaders: true }); }); describe('handle', () => { @@ -152,6 +166,21 @@ describe('AngularAppEngine', () => { expect(response?.headers.get('Vary')).toBe('Accept-Language'); }); + it('should completely ignore proxy headers if not allowed', async () => { + const strictEngine = new AngularAppEngine({ trustProxyHeaders: false }); + const request = new Request('https://example.com', { + headers: { + 'Accept-Language': 'it', + 'X-Forwarded-Prefix': '/app', + }, + }); + + // The strictEngine will ignore the prefix + const response = await strictEngine.handle(request); + expect(response?.status).toBe(302); + expect(response?.headers.get('Location')).toBe('/it'); + }); + it('should return null for requests to file-like resources in a locale', async () => { const request = new Request('https://example.com/it/logo.png'); const response = await appEngine.handle(request); @@ -163,6 +192,7 @@ describe('AngularAppEngine', () => { describe('Localized app with single locale', () => { beforeAll(() => { setAngularAppEngineManifest({ + allowedHosts: ['example.com'], entryPoints: { it: createEntryPoint('it'), }, @@ -230,6 +260,7 @@ describe('AngularAppEngine', () => { class HomeComponent {} setAngularAppEngineManifest({ + allowedHosts: ['example.com'], entryPoints: { '': async () => { setAngularAppTestingManifest( @@ -274,4 +305,148 @@ describe('AngularAppEngine', () => { expect(await response?.text()).toContain('Home works'); }); }); + + describe('Invalid host headers', () => { + let consoleErrorSpy: jasmine.Spy; + + describe('with allowed hosts configured', () => { + beforeAll(() => { + setAngularAppEngineManifest({ + allowedHosts: ['example.com'], + entryPoints: { + '': async () => { + setAngularAppTestingManifest( + [{ path: 'home', component: TestHomeComponent }], + [{ path: '**', renderMode: RenderMode.Server }], + ); + + return { + ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp, + ɵdestroyAngularServerApp: destroyAngularServerApp, + }; + }, + }, + basePath: '/', + supportedLocales: { 'en-US': '' }, + }); + + appEngine = new AngularAppEngine({ trustProxyHeaders: true }); + }); + + beforeEach(() => { + consoleErrorSpy = spyOn(console, 'error'); + }); + + it('should return 400 when disallowed host', async () => { + const request = new Request('https://evil.com'); + const response = await appEngine.handle(request); + expect(response).not.toBeNull(); + expect(response?.status).toBe(400); + expect(await response?.text()).toContain('URL with hostname "evil.com" is not allowed.'); + expect(consoleErrorSpy).toHaveBeenCalledWith( + jasmine.stringMatching('URL with hostname "evil.com" is not allowed.'), + ); + }); + + it('should return 400 when disallowed host header', async () => { + const request = new Request('https://example.com/home', { + headers: { 'host': 'evil.com' }, + }); + const response = await appEngine.handle(request); + expect(response).not.toBeNull(); + expect(response?.status).toBe(400); + expect(await response?.text()).toContain( + 'Header "host" with value "evil.com" is not allowed.', + ); + expect(consoleErrorSpy).toHaveBeenCalledWith( + jasmine.stringMatching('Header "host" with value "evil.com" is not allowed.'), + ); + }); + + it('should return 400 when disallowed x-forwarded-host header', async () => { + const request = new Request('https://example.com/home', { + headers: { 'x-forwarded-host': 'evil.com' }, + }); + const response = await appEngine.handle(request); + expect(response).not.toBeNull(); + expect(response?.status).toBe(400); + expect(await response?.text()).toContain( + 'Header "x-forwarded-host" with value "evil.com" is not allowed.', + ); + expect(consoleErrorSpy).toHaveBeenCalledWith( + jasmine.stringMatching('Header "x-forwarded-host" with value "evil.com" is not allowed.'), + ); + }); + + it('should return 400 when host with path separator', async () => { + const request = new Request('https://example.com/home', { + headers: { 'host': 'example.com/evil' }, + }); + const response = await appEngine.handle(request); + expect(response).not.toBeNull(); + expect(response?.status).toBe(400); + expect(await response?.text()).toContain( + 'Header "host" with value "example.com/evil" contains characters that are not allowed.', + ); + expect(consoleErrorSpy).toHaveBeenCalledWith( + jasmine.stringMatching( + 'Header "host" with value "example.com/evil" contains characters that are not allowed.', + ), + ); + }); + }); + + describe('without allowed hosts configured', () => { + beforeAll(() => { + setAngularAppEngineManifest({ + allowedHosts: [], + entryPoints: { + '': async () => { + setAngularAppTestingManifest( + [{ path: 'home', component: TestHomeComponent }], + [{ path: '**', renderMode: RenderMode.Server }], + ); + + return { + ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp, + ɵdestroyAngularServerApp: destroyAngularServerApp, + }; + }, + }, + basePath: '/', + supportedLocales: { 'en-US': '' }, + }); + + appEngine = new AngularAppEngine(); + }); + + beforeEach(() => { + consoleErrorSpy = spyOn(console, 'error'); + }); + + it('should log error and fallback to CSR when disallowed host', async () => { + const request = new Request('https://example.com'); + const response = await appEngine.handle(request); + expect(response).not.toBeNull(); + expect(await response?.text()).toContain('CSR page'); + expect(consoleErrorSpy).toHaveBeenCalledWith( + jasmine.stringMatching('URL with hostname "example.com" is not allowed.'), + ); + }); + + it('should log error and fallback to CSR when host with path separator', async () => { + const request = new Request('https://example.com/home', { + headers: { 'host': 'example.com/evil' }, + }); + const response = await appEngine.handle(request); + expect(response).not.toBeNull(); + expect(await response?.text()).toContain('CSR page'); + expect(consoleErrorSpy).toHaveBeenCalledWith( + jasmine.stringMatching( + 'Header "host" with value "example.com/evil" contains characters that are not allowed.', + ), + ); + }); + }); + }); }); diff --git a/packages/angular/ssr/test/npm_package/THIRD_PARTY_LICENSES.txt.golden b/packages/angular/ssr/test/npm_package/THIRD_PARTY_LICENSES.txt.golden index afbe64cd3fc4..cf31c4979bcf 100644 --- a/packages/angular/ssr/test/npm_package/THIRD_PARTY_LICENSES.txt.golden +++ b/packages/angular/ssr/test/npm_package/THIRD_PARTY_LICENSES.txt.golden @@ -449,7 +449,7 @@ License: MIT The MIT License (MIT) -Copyright 2013 Andrey Sitnik +Copyright 2013 Andrey Sitnik Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/packages/angular/ssr/test/utils/redirect_spec.ts b/packages/angular/ssr/test/utils/redirect_spec.ts new file mode 100644 index 000000000000..b26edd458ac3 --- /dev/null +++ b/packages/angular/ssr/test/utils/redirect_spec.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { createRedirectResponse } from '../../src/utils/redirect'; + +describe('Redirect Utils', () => { + describe('createRedirectResponse', () => { + it('should create a redirect response with default status 302', () => { + const response = createRedirectResponse('/home'); + expect(response.status).toBe(302); + expect(response.headers.get('Location')).toBe('/home'); + expect(response.headers.get('Vary')).toBe('X-Forwarded-Prefix'); + }); + + it('should create a redirect response with a custom status', () => { + const response = createRedirectResponse('/home', 301); + expect(response.status).toBe(301); + expect(response.headers.get('Location')).toBe('/home'); + }); + + it('should allow providing additional headers', () => { + const response = createRedirectResponse('/home', 302, { 'X-Custom': 'value' }); + expect(response.headers.get('X-Custom')).toBe('value'); + expect(response.headers.get('Location')).toBe('/home'); + expect(response.headers.get('Vary')).toBe('X-Forwarded-Prefix'); + }); + + it('should append to Vary header instead of overriding it', () => { + const response = createRedirectResponse('/home', 302, { + 'Location': '/evil', + 'Vary': 'Host', + }); + expect(response.headers.get('Location')).toBe('/home'); + expect(response.headers.get('Vary')).toBe('X-Forwarded-Prefix, Host'); + }); + + it('should NOT add duplicate X-Forwarded-Prefix if already present in Vary header', () => { + const response = createRedirectResponse('/home', 302, { + 'Vary': 'X-Forwarded-Prefix, Host', + }); + expect(response.headers.get('Vary')).toBe('X-Forwarded-Prefix, Host'); + }); + + it('should warn if Location header is provided in extra headers in dev mode', () => { + // @ts-expect-error accessing global + globalThis.ngDevMode = true; + const warnSpy = spyOn(console, 'warn'); + createRedirectResponse('/home', 302, { 'Location': '/evil' }); + expect(warnSpy).toHaveBeenCalledWith( + 'Location header "/evil" will ignored and set to "/home".', + ); + }); + + it('should throw error for invalid redirect status code in dev mode', () => { + // @ts-expect-error accessing global + globalThis.ngDevMode = true; + expect(() => createRedirectResponse('/home', 200)).toThrowError( + /Invalid redirect status code: 200/, + ); + }); + }); +}); diff --git a/packages/angular/ssr/test/utils/url_spec.ts b/packages/angular/ssr/test/utils/url_spec.ts index 9a7a7cb3ad49..a108c7ff1df6 100644 --- a/packages/angular/ssr/test/utils/url_spec.ts +++ b/packages/angular/ssr/test/utils/url_spec.ts @@ -100,6 +100,18 @@ describe('URL Utils', () => { it('should handle an all-empty URL parts', () => { expect(joinUrlParts('', '')).toBe('/'); }); + + it('should normalize parts with multiple leading and trailing slashes', () => { + expect(joinUrlParts('//path//', '///to///', '//resource//')).toBe('/path/to/resource'); + }); + + it('should handle a single part', () => { + expect(joinUrlParts('path')).toBe('/path'); + }); + + it('should handle parts containing only slashes', () => { + expect(joinUrlParts('//', '///')).toBe('/'); + }); }); describe('stripIndexHtmlFromURL', () => { diff --git a/packages/angular/ssr/test/utils/validation_spec.ts b/packages/angular/ssr/test/utils/validation_spec.ts new file mode 100644 index 000000000000..ec50fb07b502 --- /dev/null +++ b/packages/angular/ssr/test/utils/validation_spec.ts @@ -0,0 +1,273 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { + getFirstHeaderValue, + normalizeTrustProxyHeaders, + sanitizeRequestHeaders, + validateRequest, + validateUrl, +} from '../../src/utils/validation'; + +describe('Validation Utils', () => { + describe('getFirstHeaderValue', () => { + it('should return the first value from a comma-separated string', () => { + expect(getFirstHeaderValue('value1, value2')).toBe('value1'); + }); + + it('should return the value if it is a single string', () => { + expect(getFirstHeaderValue('value1')).toBe('value1'); + }); + + it('should return the first value from an array of strings', () => { + expect(getFirstHeaderValue(['value1', 'value2'])).toBe('value1'); + }); + + it('should return undefined for null or undefined', () => { + expect(getFirstHeaderValue(null)).toBeUndefined(); + expect(getFirstHeaderValue(undefined)).toBeUndefined(); + }); + + it('should return empty string for empty string input', () => { + expect(getFirstHeaderValue('')).toBe(''); + }); + }); + + describe('validateUrl', () => { + const allowedHosts = new Set(['example.com', '*.google.com']); + + it('should pass for allowed hostname', () => { + expect(() => validateUrl(new URL('http://example.com'), allowedHosts)).not.toThrow(); + }); + + it('should pass for wildcard allowed hostname', () => { + expect(() => validateUrl(new URL('http://foo.google.com'), allowedHosts)).not.toThrow(); + }); + + it('should throw for disallowed hostname', () => { + expect(() => validateUrl(new URL('http://evil.com'), allowedHosts)).toThrowError( + /URL with hostname "evil.com" is not allowed/, + ); + }); + + it('should match subdomains for wildcard', () => { + expect(() => validateUrl(new URL('http://sub.foo.google.com'), allowedHosts)).not.toThrow(); + }); + + it('should not match base domain for wildcard (*.google.com vs google.com)', () => { + // Logic: hostname.endsWith('.google.com') -> 'google.com'.endsWith('.google.com') is false + expect(() => validateUrl(new URL('http://google.com'), allowedHosts)).toThrowError( + /URL with hostname "google.com" is not allowed/, + ); + }); + }); + + describe('validateRequest', () => { + const allowedHosts = new Set(['example.com']); + + it('should pass for valid request', () => { + const req = new Request('http://example.com', { + headers: { + 'x-forwarded-port': '443', + 'x-forwarded-proto': 'https', + }, + }); + + expect(() => validateRequest(req, allowedHosts, false)).not.toThrow(); + }); + + it('should throw if URL hostname is invalid', () => { + const req = new Request('http://evil.com'); + + expect(() => validateRequest(req, allowedHosts, false)).toThrowError( + /URL with hostname "evil.com" is not allowed/, + ); + }); + + it('should throw if x-forwarded-port is invalid', () => { + const req = new Request('http://example.com', { + headers: { 'x-forwarded-port': 'abc' }, + }); + + expect(() => validateRequest(req, allowedHosts, false)).toThrowError( + 'Header "x-forwarded-port" must be a numeric value.', + ); + }); + + it('should throw if x-forwarded-proto is invalid', () => { + const req = new Request('http://example.com', { + headers: { 'x-forwarded-proto': 'ftp' }, + }); + expect(() => validateRequest(req, allowedHosts, false)).toThrowError( + 'Header "x-forwarded-proto" must be either "http" or "https".', + ); + }); + + it('should throw if host contains path separators', () => { + const req = new Request('http://example.com', { + headers: { 'host': 'example.com/bad' }, + }); + expect(() => validateRequest(req, allowedHosts, false)).toThrowError( + 'Header "host" with value "example.com/bad" contains characters that are not allowed.', + ); + }); + + it('should throw if host contains invalid characters', () => { + const req = new Request('http://example.com', { + headers: { 'host': 'example.com?query=1' }, + }); + expect(() => validateRequest(req, allowedHosts, false)).toThrowError( + 'Header "host" with value "example.com?query=1" contains characters that are not allowed.', + ); + }); + + it('should throw if x-forwarded-host contains path separators', () => { + const req = new Request('http://example.com', { + headers: { 'x-forwarded-host': 'example.com/bad' }, + }); + expect(() => validateRequest(req, allowedHosts, false)).toThrowError( + 'Header "x-forwarded-host" with value "example.com/bad" contains characters that are not allowed.', + ); + }); + + it('should throw error if x-forwarded-prefix is invalid', () => { + const inputs = [ + '//evil', + '\\\\evil', + '/\\evil', + '\\/evil', + '\\evil', + '/./', + '/../', + '/foo/./bar', + '/foo/../bar', + '/.', + '/..', + './', + '../', + '.\\', + '..\\', + '/foo/.\\bar', + '/foo/..\\bar', + '.', + '..', + 'https://attacker.com', + '%2f%2f', + '%5C', + 'http://localhost', + 'foo', + ]; + + for (const prefix of inputs) { + const request = new Request('https://example.com', { + headers: { + 'x-forwarded-prefix': prefix, + }, + }); + + expect(() => validateRequest(request, allowedHosts, false)) + .withContext(`Prefix: "${prefix}"`) + .toThrowError( + 'Header "x-forwarded-prefix" is invalid. It must start with a "/" and contain ' + + 'only alphanumeric characters, hyphens, and underscores, separated by single slashes.', + ); + } + }); + + it('should validate x-forwarded-prefix with valid usage', () => { + const inputs = ['/foo', '/foo/baz', '/v1', '/app_name', '/', '/my-app']; + + for (const prefix of inputs) { + const request = new Request('https://example.com', { + headers: { + 'x-forwarded-prefix': prefix, + }, + }); + + expect(() => validateRequest(request, allowedHosts, false)) + .withContext(`Prefix: "${prefix}"`) + .not.toThrow(); + } + }); + }); + + describe('sanitizeRequestHeaders', () => { + it('should allow x-forwarded-host and x-forwarded-proto when undefined', () => { + const req = new Request('http://example.com', { + headers: { + 'host': 'example.com', + 'x-forwarded-host': 'proxy.com', + 'x-forwarded-proto': 'https', + }, + }); + const { request: secured } = sanitizeRequestHeaders( + req, + normalizeTrustProxyHeaders(undefined), + ); + + expect(secured.headers.get('host')).toBe('example.com'); + expect(secured.headers.get('x-forwarded-host')).toBe('proxy.com'); + expect(secured.headers.get('x-forwarded-proto')).toBe('https'); + }); + + it('should set deoptToCSR when x-forwarded-prefix is present and undefined', () => { + const req = new Request('http://example.com', { + headers: { + 'x-forwarded-prefix': '/prefix', + }, + }); + const { deoptToCSR } = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(undefined)); + + expect(deoptToCSR).toBeTrue(); + }); + + it('should retain allowed proxy headers when explicitly provided', () => { + const trustProxyHeaders = new Set(['x-forwarded-host']); + const req = new Request('http://example.com', { + headers: { + 'host': 'example.com', + 'x-forwarded-host': 'proxy.com', + 'x-forwarded-proto': 'https', + }, + }); + const { request: secured } = sanitizeRequestHeaders(req, trustProxyHeaders); + + expect(secured.headers.get('host')).toBe('example.com'); + expect(secured.headers.get('x-forwarded-host')).toBe('proxy.com'); + expect(secured.headers.has('x-forwarded-proto')).toBeFalse(); + }); + + it('should retain all proxy headers when trustProxyHeaders is true', () => { + const req = new Request('http://example.com', { + headers: { + 'host': 'example.com', + 'x-forwarded-host': 'proxy.com', + 'x-forwarded-proto': 'https', + }, + }); + const { request: secured } = sanitizeRequestHeaders(req, normalizeTrustProxyHeaders(true)); + + expect(secured.headers.get('host')).toBe('example.com'); + expect(secured.headers.get('x-forwarded-host')).toBe('proxy.com'); + expect(secured.headers.get('x-forwarded-proto')).toBe('https'); + }); + + it('should not clone the request if no proxy headers need to be removed', () => { + const req = new Request('http://example.com', { + headers: { 'accept': 'application/json' }, + }); + const { request: secured } = sanitizeRequestHeaders( + req, + normalizeTrustProxyHeaders(undefined), + ); + + expect(secured).toBe(req); + expect(secured.headers.get('accept')).toBe('application/json'); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index e4253b78d121..2b3acfdc12da 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -26,9 +26,9 @@ "autoprefixer": "10.4.21", "babel-loader": "10.0.0", "browserslist": "^4.21.5", - "copy-webpack-plugin": "13.0.1", + "copy-webpack-plugin": "14.0.0", "css-loader": "7.1.2", - "esbuild-wasm": "0.25.9", + "esbuild-wasm": "0.28.0", "fast-glob": "3.3.3", "http-proxy-middleware": "3.0.5", "istanbul-lib-instrument": "6.0.3", @@ -41,9 +41,9 @@ "mini-css-extract-plugin": "2.9.4", "open": "10.2.0", "ora": "8.2.0", - "picomatch": "4.0.3", + "picomatch": "4.0.4", "piscina": "5.1.3", - "postcss": "8.5.6", + "postcss": "8.5.12", "postcss-loader": "8.1.1", "resolve-url-loader": "5.0.0", "rxjs": "7.8.2", @@ -62,7 +62,7 @@ "webpack-subresource-integrity": "5.1.0" }, "optionalDependencies": { - "esbuild": "0.25.9" + "esbuild": "0.28.0" }, "devDependencies": { "@angular/ssr": "workspace:*", diff --git a/packages/angular_devkit/build_angular/src/builders/app-shell/index.ts b/packages/angular_devkit/build_angular/src/builders/app-shell/index.ts index 1cdcd213c055..2772a6e1fa9b 100644 --- a/packages/angular_devkit/build_angular/src/builders/app-shell/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/app-shell/index.ts @@ -82,10 +82,11 @@ async function _renderUniversal( localeDirectory, ); + const route = options.route; let html: string = await renderWorker.run({ serverBundlePath, document: indexHtml, - url: options.route, + url: route?.[0] === '/' ? route : '/' + route, }); // Overwrite the client index file. diff --git a/packages/angular_devkit/build_angular/src/builders/prerender/index.ts b/packages/angular_devkit/build_angular/src/builders/prerender/index.ts index b56aa9df4b26..15896c8f2db3 100644 --- a/packages/angular_devkit/build_angular/src/builders/prerender/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/prerender/index.ts @@ -209,7 +209,7 @@ async function _renderUniversal( inlineCriticalCss: !!normalizedStylesOptimization.inlineCritical, minifyCss: !!normalizedStylesOptimization.minify, outputPath, - route, + route: route[0] === '/' ? route : '/' + route, serverBundlePath, }; diff --git a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/index.ts b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/index.ts index 6219ea6a46a2..37e04171aed4 100644 --- a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/index.ts @@ -114,7 +114,13 @@ export function execute( return of([b, s]); } - return startNodeServer(s, nodeServerPort, context.logger, !!options.inspect).pipe( + return startNodeServer( + s, + nodeServerPort, + options.host, + context.logger, + !!options.inspect, + ).pipe( map(() => [b, s]), catchError((err) => { context.logger.error(`A server error has occurred.\n${mapErrorToMessage(err)}`); @@ -216,12 +222,13 @@ export function log( function startNodeServer( serverOutput: BuilderOutput, port: number, + host: string | undefined, logger: logging.LoggerApi, inspectMode = false, ): Observable { const outputPath = serverOutput.outputPath as string; const path = join(outputPath, 'main.js'); - const env = { ...process.env, PORT: '' + port }; + const env = { ...process.env, PORT: '' + port, NG_ALLOWED_HOSTS: host ?? 'localhost' }; const args = ['--enable-source-maps', `"${path}"`]; if (inspectMode) { diff --git a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/proxy_spec.ts b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/proxy_spec.ts index a4cafd66ee06..b125312653e9 100644 --- a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/proxy_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/proxy_spec.ts @@ -54,11 +54,12 @@ describe('Serve SSR Builder', () => { })); server.use((req, res, next) => { + const { protocol, originalUrl, baseUrl, headers } = req; commonEngine .render({ bootstrap: AppServerModule, documentFilePath: indexHtml, - url: req.originalUrl, + url: \`\${protocol}://\${headers.host}\${originalUrl}\`, publicPath: distFolder, }) .then((html) => res.send(html)) diff --git a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/ssl_spec.ts b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/ssl_spec.ts index 3792d87f839c..4a358ee1c123 100644 --- a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/ssl_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/ssl_spec.ts @@ -54,11 +54,12 @@ describe('Serve SSR Builder', () => { })); server.use((req, res, next) => { + const { protocol, originalUrl, baseUrl, headers } = req; commonEngine .render({ bootstrap: AppServerModule, documentFilePath: indexHtml, - url: req.originalUrl, + url: \`\${protocol}://\${headers.host}\${originalUrl}\`, publicPath: distFolder, }) .then((html) => res.send(html)) diff --git a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/works_spec.ts b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/works_spec.ts index 8e92c4d666e3..3feed5d3161a 100644 --- a/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/works_spec.ts +++ b/packages/angular_devkit/build_angular/src/builders/ssr-dev-server/specs/works_spec.ts @@ -53,11 +53,12 @@ describe('Serve SSR Builder', () => { })); server.use((req, res, next) => { + const { protocol, originalUrl, baseUrl, headers } = req; commonEngine .render({ bootstrap: AppServerModule, documentFilePath: indexHtml, - url: req.originalUrl, + url: \`\${protocol}://\${headers.host}\${originalUrl}\`, publicPath: distFolder, }) .then((html) => res.send(html)) diff --git a/packages/angular_devkit/build_angular/src/tools/webpack/utils/helpers.ts b/packages/angular_devkit/build_angular/src/tools/webpack/utils/helpers.ts index f57708663e03..72cbf96319d7 100644 --- a/packages/angular_devkit/build_angular/src/tools/webpack/utils/helpers.ts +++ b/packages/angular_devkit/build_angular/src/tools/webpack/utils/helpers.ts @@ -9,6 +9,7 @@ import type { ObjectPattern } from 'copy-webpack-plugin'; import glob from 'fast-glob'; import { createHash } from 'node:crypto'; +import { createRequire } from 'node:module'; import * as path from 'node:path'; import type { Configuration, WebpackOptionsNormalized } from 'webpack'; import { @@ -314,7 +315,8 @@ export function getStatsOptions(verbose = false): WebpackStatsOptions { */ export function isPackageInstalled(root: string, name: string): boolean { try { - require.resolve(name, { paths: [root] }); + const resolve = createRequire(root + '/').resolve; + resolve(name); return true; } catch { diff --git a/packages/angular_devkit/core/package.json b/packages/angular_devkit/core/package.json index 2fa6f3eb24a4..b0fb4f9f0572 100644 --- a/packages/angular_devkit/core/package.json +++ b/packages/angular_devkit/core/package.json @@ -25,10 +25,10 @@ "./*.js": "./*.js" }, "dependencies": { - "ajv": "8.17.1", + "ajv": "8.18.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", - "picomatch": "4.0.3", + "picomatch": "4.0.4", "rxjs": "7.8.2", "source-map": "0.7.6" }, diff --git a/packages/schematics/angular/ssr/files/server-builder/server.ts.template b/packages/schematics/angular/ssr/files/server-builder/server.ts.template index 7567fa65a81d..92272edc590d 100644 --- a/packages/schematics/angular/ssr/files/server-builder/server.ts.template +++ b/packages/schematics/angular/ssr/files/server-builder/server.ts.template @@ -15,7 +15,9 @@ export function app(): express.Express { ? join(distFolder, 'index.original.html') : join(distFolder, 'index.html'); - const commonEngine = new CommonEngine(); + const commonEngine = new CommonEngine({ + allowedHosts: [/* Configure your hosts here */] + }); server.set('view engine', 'html'); server.set('views', distFolder); diff --git a/packages/schematics/angular/ssr/index.ts b/packages/schematics/angular/ssr/index.ts index e589395dac73..90744240e43b 100644 --- a/packages/schematics/angular/ssr/index.ts +++ b/packages/schematics/angular/ssr/index.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import { isJsonObject, join, normalize, strings } from '@angular-devkit/core'; +import { JsonObject, isJsonObject, join, normalize, strings } from '@angular-devkit/core'; import { Rule, SchematicContext, @@ -204,6 +204,10 @@ function updateApplicationBuilderWorkspaceConfigRule( buildTarget.options = { ...buildTarget.options, + security: { + ...((buildTarget.options?.security as JsonObject | undefined) ?? {}), + allowedHosts: [], + }, outputPath, outputMode: 'server', ssr: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80137681a0e6..0a4490b43567 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,16 +78,16 @@ importers: version: 9.33.0 '@rollup/plugin-alias': specifier: ^5.1.1 - version: 5.1.1(rollup@4.46.2) + version: 5.1.1(rollup@4.59.0) '@rollup/plugin-commonjs': specifier: ^28.0.0 - version: 28.0.6(rollup@4.46.2) + version: 28.0.6(rollup@4.59.0) '@rollup/plugin-json': specifier: ^6.1.0 - version: 6.1.0(rollup@4.46.2) + version: 6.1.0(rollup@4.59.0) '@rollup/plugin-node-resolve': specifier: 16.0.1 - version: 16.0.1(rollup@4.46.2) + version: 16.0.1(rollup@4.59.0) '@stylistic/eslint-plugin': specifier: ^5.0.0 version: 5.4.0(eslint@9.33.0(jiti@1.21.7)) @@ -170,8 +170,8 @@ importers: specifier: 8.39.1 version: 8.39.1(eslint@9.33.0(jiti@1.21.7))(typescript@5.9.2) ajv: - specifier: 8.17.1 - version: 8.17.1 + specifier: 8.18.0 + version: 8.18.0 ansi-colors: specifier: 4.1.3 version: 4.1.3 @@ -182,11 +182,11 @@ importers: specifier: 6.0.3 version: 6.0.3 esbuild: - specifier: 0.25.9 - version: 0.25.9 + specifier: 0.28.0 + version: 0.28.0 esbuild-wasm: - specifier: 0.25.9 - version: 0.25.9 + specifier: 0.28.0 + version: 0.28.0 eslint: specifier: 9.33.0 version: 9.33.0(jiti@1.21.7) @@ -272,17 +272,17 @@ importers: specifier: 23.2.6 version: 23.2.6(encoding@0.1.13) rollup: - specifier: 4.46.2 - version: 4.46.2 + specifier: 4.59.0 + version: 4.59.0 rollup-license-plugin: specifier: ~3.0.1 version: 3.0.2 rollup-plugin-dts: specifier: 6.2.1 - version: 6.2.1(rollup@4.46.2)(typescript@5.9.2) + version: 6.2.1(rollup@4.59.0)(typescript@5.9.2) rollup-plugin-sourcemaps2: specifier: 0.5.3 - version: 0.5.3(@types/node@22.18.10)(rollup@4.46.2) + version: 0.5.3(@types/node@22.18.10)(rollup@4.59.0) semver: specifier: 7.7.2 version: 7.7.2 @@ -366,7 +366,7 @@ importers: version: 5.1.14(@types/node@24.9.1) '@vitejs/plugin-basic-ssl': specifier: 2.1.0 - version: 2.1.0(vite@7.1.11(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 2.1.0(vite@7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)) beasties: specifier: 0.3.5 version: 0.3.5 @@ -374,8 +374,8 @@ importers: specifier: ^4.23.0 version: 4.26.3 esbuild: - specifier: 0.25.9 - version: 0.25.9 + specifier: 0.28.0 + version: 0.28.0 https-proxy-agent: specifier: 7.0.6 version: 7.0.6(supports-color@10.2.2) @@ -398,14 +398,14 @@ importers: specifier: 8.0.0 version: 8.0.0 picomatch: - specifier: 4.0.3 - version: 4.0.3 + specifier: 4.0.4 + version: 4.0.4 piscina: specifier: 5.1.3 version: 5.1.3 rollup: - specifier: 4.52.3 - version: 4.52.3 + specifier: 4.59.0 + version: 4.59.0 sass: specifier: 1.90.0 version: 1.90.0 @@ -419,8 +419,8 @@ importers: specifier: 0.2.14 version: 0.2.14 vite: - specifier: 7.1.11 - version: 7.1.11(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) + specifier: 7.3.2 + version: 7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) watchpack: specifier: 2.4.4 version: 2.4.4 @@ -441,8 +441,8 @@ importers: specifier: 20.3.0 version: 20.3.0(@angular/compiler-cli@20.3.7(@angular/compiler@20.3.7)(typescript@5.9.2))(tslib@2.8.1)(typescript@5.9.2) postcss: - specifier: 8.5.6 - version: 8.5.6 + specifier: 8.5.12 + version: 8.5.12 rxjs: specifier: 7.8.2 version: 7.8.2 @@ -640,22 +640,22 @@ importers: version: 4.1.3 autoprefixer: specifier: 10.4.21 - version: 10.4.21(postcss@8.5.6) + version: 10.4.21(postcss@8.5.12) babel-loader: specifier: 10.0.0 - version: 10.0.0(@babel/core@7.28.3)(webpack@5.105.0(esbuild@0.25.9)) + version: 10.0.0(@babel/core@7.28.3)(webpack@5.105.0(esbuild@0.28.0)) browserslist: specifier: ^4.21.5 version: 4.26.3 copy-webpack-plugin: - specifier: 13.0.1 - version: 13.0.1(webpack@5.105.0(esbuild@0.25.9)) + specifier: 14.0.0 + version: 14.0.0(webpack@5.105.0(esbuild@0.28.0)) css-loader: specifier: 7.1.2 - version: 7.1.2(webpack@5.105.0(esbuild@0.25.9)) + version: 7.1.2(webpack@5.105.0(esbuild@0.28.0)) esbuild-wasm: - specifier: 0.25.9 - version: 0.25.9 + specifier: 0.28.0 + version: 0.28.0 fast-glob: specifier: 3.3.3 version: 3.3.3 @@ -676,16 +676,16 @@ importers: version: 4.4.0 less-loader: specifier: 12.3.0 - version: 12.3.0(less@4.4.0)(webpack@5.105.0(esbuild@0.25.9)) + version: 12.3.0(less@4.4.0)(webpack@5.105.0(esbuild@0.28.0)) license-webpack-plugin: specifier: 4.0.2 - version: 4.0.2(webpack@5.105.0(esbuild@0.25.9)) + version: 4.0.2(webpack@5.105.0(esbuild@0.28.0)) loader-utils: specifier: 3.3.1 version: 3.3.1 mini-css-extract-plugin: specifier: 2.9.4 - version: 2.9.4(webpack@5.105.0(esbuild@0.25.9)) + version: 2.9.4(webpack@5.105.0(esbuild@0.28.0)) open: specifier: 10.2.0 version: 10.2.0 @@ -693,17 +693,17 @@ importers: specifier: 8.2.0 version: 8.2.0 picomatch: - specifier: 4.0.3 - version: 4.0.3 + specifier: 4.0.4 + version: 4.0.4 piscina: specifier: 5.1.3 version: 5.1.3 postcss: - specifier: 8.5.6 - version: 8.5.6 + specifier: 8.5.12 + version: 8.5.12 postcss-loader: specifier: 8.1.1 - version: 8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.105.0(esbuild@0.25.9)) + version: 8.1.1(postcss@8.5.12)(typescript@5.9.2)(webpack@5.105.0(esbuild@0.28.0)) resolve-url-loader: specifier: 5.0.0 version: 5.0.0 @@ -715,13 +715,13 @@ importers: version: 1.90.0 sass-loader: specifier: 16.0.5 - version: 16.0.5(sass@1.90.0)(webpack@5.105.0(esbuild@0.25.9)) + version: 16.0.5(sass@1.90.0)(webpack@5.105.0(esbuild@0.28.0)) semver: specifier: 7.7.2 version: 7.7.2 source-map-loader: specifier: 5.0.0 - version: 5.0.0(webpack@5.105.0(esbuild@0.25.9)) + version: 5.0.0(webpack@5.105.0(esbuild@0.28.0)) source-map-support: specifier: 0.5.21 version: 0.5.21 @@ -736,19 +736,19 @@ importers: version: 2.8.1 webpack: specifier: 5.105.0 - version: 5.105.0(esbuild@0.25.9) + version: 5.105.0(esbuild@0.28.0) webpack-dev-middleware: specifier: 7.4.2 - version: 7.4.2(webpack@5.105.0(esbuild@0.25.9)) + version: 7.4.2(webpack@5.105.0(esbuild@0.28.0)) webpack-dev-server: specifier: 5.2.2 - version: 5.2.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)(webpack@5.105.0(esbuild@0.25.9)) + version: 5.2.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)(webpack@5.105.0(esbuild@0.28.0)) webpack-merge: specifier: 6.0.1 version: 6.0.1 webpack-subresource-integrity: specifier: 5.1.0 - version: 5.1.0(webpack@5.105.0(esbuild@0.25.9)) + version: 5.1.0(webpack@5.105.0(esbuild@0.28.0)) devDependencies: '@angular/ssr': specifier: workspace:* @@ -767,8 +767,8 @@ importers: version: 7.13.0 optionalDependencies: esbuild: - specifier: 0.25.9 - version: 0.25.9 + specifier: 0.28.0 + version: 0.28.0 packages/angular_devkit/build_webpack: dependencies: @@ -787,25 +787,25 @@ importers: version: link:../../ngtools/webpack webpack: specifier: 5.105.0 - version: 5.105.0(esbuild@0.25.9) + version: 5.105.0(esbuild@0.28.0) webpack-dev-server: specifier: 5.2.2 - version: 5.2.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)(webpack@5.105.0(esbuild@0.25.9)) + version: 5.2.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)(webpack@5.105.0(esbuild@0.28.0)) packages/angular_devkit/core: dependencies: ajv: - specifier: 8.17.1 - version: 8.17.1 + specifier: 8.18.0 + version: 8.18.0 ajv-formats: specifier: 3.0.1 - version: 3.0.1(ajv@8.17.1) + version: 3.0.1(ajv@8.18.0) jsonc-parser: specifier: 3.3.1 version: 3.3.1 picomatch: - specifier: 4.0.3 - version: 4.0.3 + specifier: 4.0.4 + version: 4.0.4 rxjs: specifier: 7.8.2 version: 7.8.2 @@ -869,7 +869,7 @@ importers: version: 5.9.2 webpack: specifier: 5.105.0 - version: 5.105.0(esbuild@0.25.9) + version: 5.105.0(esbuild@0.28.0) packages/schematics/angular: dependencies: @@ -1671,156 +1671,468 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.9': resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.9': resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.9': resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.9': resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.9': resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.9': resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.9': resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.9': resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.9': resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.9': resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.9': resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.9': resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.9': resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.9': resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.9': resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.9': resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.9': resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.9': resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.9': resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.9': resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.9': resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.9': resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.9': resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.9': resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.9': resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3071,235 +3383,141 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.46.2': - resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm-eabi@4.52.3': - resolution: {integrity: sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==} + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.46.2': - resolution: {integrity: sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==} + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} cpu: [arm64] os: [android] - '@rollup/rollup-android-arm64@4.52.3': - resolution: {integrity: sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==} + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.46.2': - resolution: {integrity: sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-arm64@4.52.3': - resolution: {integrity: sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.46.2': - resolution: {integrity: sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==} - cpu: [x64] os: [darwin] - '@rollup/rollup-darwin-x64@4.52.3': - resolution: {integrity: sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==} + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.46.2': - resolution: {integrity: sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==} + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-arm64@4.52.3': - resolution: {integrity: sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.46.2': - resolution: {integrity: sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==} + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} cpu: [x64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.52.3': - resolution: {integrity: sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.46.2': - resolution: {integrity: sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==} + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-gnueabihf@4.52.3': - resolution: {integrity: sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.46.2': - resolution: {integrity: sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm-musleabihf@4.52.3': - resolution: {integrity: sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==} + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.46.2': - resolution: {integrity: sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==} + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-gnu@4.52.3': - resolution: {integrity: sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm64-musl@4.46.2': - resolution: {integrity: sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-musl@4.52.3': - resolution: {integrity: sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==} + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.52.3': - resolution: {integrity: sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==} + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loongarch64-gnu@4.46.2': - resolution: {integrity: sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==} + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] - libc: [glibc] + libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.46.2': - resolution: {integrity: sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==} + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-gnu@4.52.3': - resolution: {integrity: sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==} + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-gnu@4.46.2': - resolution: {integrity: sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==} - cpu: [riscv64] - os: [linux] - libc: [glibc] + libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.52.3': - resolution: {integrity: sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==} + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.46.2': - resolution: {integrity: sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-musl@4.52.3': - resolution: {integrity: sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==} + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.46.2': - resolution: {integrity: sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==} + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-s390x-gnu@4.52.3': - resolution: {integrity: sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-gnu@4.46.2': - resolution: {integrity: sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==} + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.52.3': - resolution: {integrity: sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-musl@4.46.2': - resolution: {integrity: sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==} + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-linux-x64-musl@4.52.3': - resolution: {integrity: sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==} + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} cpu: [x64] - os: [linux] - libc: [musl] + os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.52.3': - resolution: {integrity: sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==} + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.46.2': - resolution: {integrity: sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-arm64-msvc@4.52.3': - resolution: {integrity: sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==} + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.46.2': - resolution: {integrity: sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.52.3': - resolution: {integrity: sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==} + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.52.3': - resolution: {integrity: sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==} + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.46.2': - resolution: {integrity: sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.52.3': - resolution: {integrity: sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==} + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} cpu: [x64] os: [win32] @@ -4032,6 +4250,9 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + algoliasearch@5.35.0: resolution: {integrity: sha512-Y+moNhsqgLmvJdgTsO4GZNgsaDWv8AOGAaPeIeHKlDn/XunoAqYbA+XNpBd1dW8GOXAUDyxC9Rxc7AV4kpFcIg==} engines: {node: '>= 14.0.0'} @@ -4717,9 +4938,9 @@ packages: copy-anything@2.0.6: resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==} - copy-webpack-plugin@13.0.1: - resolution: {integrity: sha512-J+YV3WfhY6W/Xf9h+J1znYuqTye2xkBUIGyTPWuBAT27qajBa5mR4f8WBmfDY3YjRftT2kqZZiLi1qf0H+UOFw==} - engines: {node: '>= 18.12.0'} + copy-webpack-plugin@14.0.0: + resolution: {integrity: sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==} + engines: {node: '>= 20.9.0'} peerDependencies: webpack: ^5.1.0 @@ -5198,8 +5419,8 @@ packages: es6-promisify@5.0.0: resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==} - esbuild-wasm@0.25.9: - resolution: {integrity: sha512-Jpv5tCSwQg18aCqCRD3oHIX/prBhXMDapIoG//A+6+dV0e7KQMGFg85ihJ5T1EeMjbZjON3TqFy0VrGAnIHLDA==} + esbuild-wasm@0.28.0: + resolution: {integrity: sha512-5TRVKExcEmeMkccIZMzUq+Az6X2RoMAJyfl6SMMO1dMVhmvt0I2mx7gAb6zYi42n4d1ETcatFXazGKzA+aW7fg==} engines: {node: '>=18'} hasBin: true @@ -5208,6 +5429,16 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -7440,6 +7671,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -7552,8 +7787,8 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + postcss@8.5.12: + resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -7663,7 +7898,6 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. - (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qjobs@1.2.0: @@ -7870,13 +8104,8 @@ packages: '@types/node': optional: true - rollup@4.46.2: - resolution: {integrity: sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - rollup@4.52.3: - resolution: {integrity: sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==} + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -8009,6 +8238,10 @@ packages: serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + serialize-javascript@7.0.3: + resolution: {integrity: sha512-h+cZ/XXarqDgCjo+YSyQU/ulDEESGGf8AMK9pPNmhNSl/FzPl6L8pMp1leca5z6NuG6tvV/auC8/43tmovowww==} + engines: {node: '>=20.0.0'} + serve-index@1.9.1: resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} engines: {node: '>= 0.8.0'} @@ -8798,48 +9031,8 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite@7.1.11: - resolution: {integrity: sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vite@7.1.5: - resolution: {integrity: sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==} + vite@7.3.2: + resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -10287,81 +10480,237 @@ snapshots: '@esbuild/aix-ppc64@0.25.9': optional: true + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/aix-ppc64@0.28.0': + optional: true + '@esbuild/android-arm64@0.25.9': optional: true + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.28.0': + optional: true + '@esbuild/android-arm@0.25.9': optional: true + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-arm@0.28.0': + optional: true + '@esbuild/android-x64@0.25.9': optional: true + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/android-x64@0.28.0': + optional: true + '@esbuild/darwin-arm64@0.25.9': optional: true + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.28.0': + optional: true + '@esbuild/darwin-x64@0.25.9': optional: true + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.28.0': + optional: true + '@esbuild/freebsd-arm64@0.25.9': optional: true + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.28.0': + optional: true + '@esbuild/freebsd-x64@0.25.9': optional: true + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.28.0': + optional: true + '@esbuild/linux-arm64@0.25.9': optional: true + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.28.0': + optional: true + '@esbuild/linux-arm@0.25.9': optional: true + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-arm@0.28.0': + optional: true + '@esbuild/linux-ia32@0.25.9': optional: true + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.28.0': + optional: true + '@esbuild/linux-loong64@0.25.9': optional: true + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.28.0': + optional: true + '@esbuild/linux-mips64el@0.25.9': optional: true + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.28.0': + optional: true + '@esbuild/linux-ppc64@0.25.9': optional: true + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.28.0': + optional: true + '@esbuild/linux-riscv64@0.25.9': optional: true + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.28.0': + optional: true + '@esbuild/linux-s390x@0.25.9': optional: true + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.28.0': + optional: true + '@esbuild/linux-x64@0.25.9': optional: true + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/linux-x64@0.28.0': + optional: true + '@esbuild/netbsd-arm64@0.25.9': optional: true + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.28.0': + optional: true + '@esbuild/netbsd-x64@0.25.9': optional: true + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.28.0': + optional: true + '@esbuild/openbsd-arm64@0.25.9': optional: true + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.28.0': + optional: true + '@esbuild/openbsd-x64@0.25.9': optional: true + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.28.0': + optional: true + '@esbuild/openharmony-arm64@0.25.9': optional: true + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.28.0': + optional: true + '@esbuild/sunos-x64@0.25.9': optional: true + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.28.0': + optional: true + '@esbuild/win32-arm64@0.25.9': optional: true + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.28.0': + optional: true + '@esbuild/win32-ia32@0.25.9': optional: true + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.28.0': + optional: true + '@esbuild/win32-x64@0.25.9': optional: true + '@esbuild/win32-x64@0.27.7': + optional: true + + '@esbuild/win32-x64@0.28.0': + optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.33.0(jiti@1.21.7))': dependencies: eslint: 9.33.0(jiti@1.21.7) @@ -11645,202 +11994,137 @@ snapshots: - react-native-b4a - supports-color - '@rollup/plugin-alias@5.1.1(rollup@4.46.2)': + '@rollup/plugin-alias@5.1.1(rollup@4.59.0)': optionalDependencies: - rollup: 4.46.2 + rollup: 4.59.0 - '@rollup/plugin-commonjs@28.0.6(rollup@4.46.2)': + '@rollup/plugin-commonjs@28.0.6(rollup@4.59.0)': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.46.2) + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) commondir: 1.0.1 estree-walker: 2.0.2 - fdir: 6.5.0(picomatch@4.0.3) + fdir: 6.5.0(picomatch@4.0.4) is-reference: 1.2.1 magic-string: 0.30.17 - picomatch: 4.0.3 - optionalDependencies: - rollup: 4.46.2 - - '@rollup/plugin-json@6.1.0(rollup@4.46.2)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.46.2) + picomatch: 4.0.4 optionalDependencies: - rollup: 4.46.2 + rollup: 4.59.0 - '@rollup/plugin-json@6.1.0(rollup@4.52.3)': + '@rollup/plugin-json@6.1.0(rollup@4.59.0)': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.52.3) + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) optionalDependencies: - rollup: 4.52.3 + rollup: 4.59.0 - '@rollup/plugin-node-resolve@15.3.1(rollup@4.52.3)': + '@rollup/plugin-node-resolve@15.3.1(rollup@4.59.0)': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.52.3) + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 resolve: 1.22.10 optionalDependencies: - rollup: 4.52.3 + rollup: 4.59.0 - '@rollup/plugin-node-resolve@16.0.1(rollup@4.46.2)': + '@rollup/plugin-node-resolve@16.0.1(rollup@4.59.0)': dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.46.2) + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 resolve: 1.22.10 optionalDependencies: - rollup: 4.46.2 - - '@rollup/pluginutils@5.2.0(rollup@4.46.2)': - dependencies: - '@types/estree': 1.0.8 - estree-walker: 2.0.2 - picomatch: 4.0.3 - optionalDependencies: - rollup: 4.46.2 + rollup: 4.59.0 - '@rollup/pluginutils@5.3.0(rollup@4.46.2)': + '@rollup/pluginutils@5.2.0(rollup@4.59.0)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 - picomatch: 4.0.3 + picomatch: 4.0.4 optionalDependencies: - rollup: 4.46.2 + rollup: 4.59.0 - '@rollup/pluginutils@5.3.0(rollup@4.52.3)': + '@rollup/pluginutils@5.3.0(rollup@4.59.0)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 - picomatch: 4.0.3 + picomatch: 4.0.4 optionalDependencies: - rollup: 4.52.3 - - '@rollup/rollup-android-arm-eabi@4.46.2': - optional: true - - '@rollup/rollup-android-arm-eabi@4.52.3': - optional: true - - '@rollup/rollup-android-arm64@4.46.2': - optional: true - - '@rollup/rollup-android-arm64@4.52.3': - optional: true - - '@rollup/rollup-darwin-arm64@4.46.2': - optional: true - - '@rollup/rollup-darwin-arm64@4.52.3': - optional: true - - '@rollup/rollup-darwin-x64@4.46.2': - optional: true - - '@rollup/rollup-darwin-x64@4.52.3': - optional: true - - '@rollup/rollup-freebsd-arm64@4.46.2': - optional: true - - '@rollup/rollup-freebsd-arm64@4.52.3': - optional: true - - '@rollup/rollup-freebsd-x64@4.46.2': - optional: true - - '@rollup/rollup-freebsd-x64@4.52.3': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.46.2': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.52.3': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.46.2': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.52.3': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.46.2': - optional: true + rollup: 4.59.0 - '@rollup/rollup-linux-arm64-gnu@4.52.3': + '@rollup/rollup-android-arm-eabi@4.59.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.46.2': + '@rollup/rollup-android-arm64@4.59.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.52.3': + '@rollup/rollup-darwin-arm64@4.59.0': optional: true - '@rollup/rollup-linux-loong64-gnu@4.52.3': + '@rollup/rollup-darwin-x64@4.59.0': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.46.2': + '@rollup/rollup-freebsd-arm64@4.59.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.46.2': + '@rollup/rollup-freebsd-x64@4.59.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.52.3': + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.46.2': + '@rollup/rollup-linux-arm-musleabihf@4.59.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.52.3': + '@rollup/rollup-linux-arm64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.46.2': + '@rollup/rollup-linux-arm64-musl@4.59.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.52.3': + '@rollup/rollup-linux-loong64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.46.2': + '@rollup/rollup-linux-loong64-musl@4.59.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.52.3': + '@rollup/rollup-linux-ppc64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.46.2': + '@rollup/rollup-linux-ppc64-musl@4.59.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.52.3': + '@rollup/rollup-linux-riscv64-gnu@4.59.0': optional: true - '@rollup/rollup-linux-x64-musl@4.46.2': + '@rollup/rollup-linux-riscv64-musl@4.59.0': optional: true - '@rollup/rollup-linux-x64-musl@4.52.3': + '@rollup/rollup-linux-s390x-gnu@4.59.0': optional: true - '@rollup/rollup-openharmony-arm64@4.52.3': + '@rollup/rollup-linux-x64-gnu@4.59.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.46.2': + '@rollup/rollup-linux-x64-musl@4.59.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.52.3': + '@rollup/rollup-openbsd-x64@4.59.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.46.2': + '@rollup/rollup-openharmony-arm64@4.59.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.52.3': + '@rollup/rollup-win32-arm64-msvc@4.59.0': optional: true - '@rollup/rollup-win32-x64-gnu@4.52.3': + '@rollup/rollup-win32-ia32-msvc@4.59.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.46.2': + '@rollup/rollup-win32-x64-gnu@4.59.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.52.3': + '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true '@rollup/wasm-node@4.52.4': @@ -11893,7 +12177,7 @@ snapshots: eslint-visitor-keys: 4.2.1 espree: 10.4.0 estraverse: 5.3.0 - picomatch: 4.0.3 + picomatch: 4.0.4 '@tootallnate/once@2.0.0': {} @@ -12534,9 +12818,9 @@ snapshots: lodash: 4.17.21 minimatch: 7.4.6 - '@vitejs/plugin-basic-ssl@2.1.0(vite@7.1.11(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))': + '@vitejs/plugin-basic-ssl@2.1.0(vite@7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))': dependencies: - vite: 7.1.11(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) '@vitest/expect@3.2.4': dependencies: @@ -12546,13 +12830,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.1.5(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(vite@7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 7.1.5(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -12613,11 +12897,11 @@ snapshots: '@web/dev-server-rollup@0.6.4(bufferutil@4.0.9)': dependencies: - '@rollup/plugin-node-resolve': 15.3.1(rollup@4.52.3) + '@rollup/plugin-node-resolve': 15.3.1(rollup@4.59.0) '@web/dev-server-core': 0.7.5(bufferutil@4.0.9) nanocolors: 0.2.13 parse5: 6.0.1 - rollup: 4.52.3 + rollup: 4.59.0 whatwg-url: 14.2.0 transitivePeerDependencies: - bufferutil @@ -12892,15 +13176,19 @@ snapshots: ajv-formats@2.1.1: dependencies: - ajv: 8.17.1 + ajv: 8.18.0 ajv-formats@3.0.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 - ajv-keywords@5.1.0(ajv@8.17.1): + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-keywords@5.1.0(ajv@8.18.0): dependencies: - ajv: 8.17.1 + ajv: 8.18.0 fast-deep-equal: 3.1.3 ajv@6.12.6: @@ -12917,6 +13205,13 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + algoliasearch@5.35.0: dependencies: '@algolia/abtesting': 1.1.0 @@ -13073,14 +13368,14 @@ snapshots: atomic-sleep@1.0.0: {} - autoprefixer@10.4.21(postcss@8.5.6): + autoprefixer@10.4.21(postcss@8.5.12): dependencies: browserslist: 4.26.3 caniuse-lite: 1.0.30001750 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 - postcss: 8.5.6 + postcss: 8.5.12 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -13093,11 +13388,11 @@ snapshots: b4a@1.7.3: {} - babel-loader@10.0.0(@babel/core@7.28.3)(webpack@5.105.0(esbuild@0.25.9)): + babel-loader@10.0.0(@babel/core@7.28.3)(webpack@5.105.0(esbuild@0.28.0)): dependencies: '@babel/core': 7.28.3 find-up: 5.0.0 - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.3): dependencies: @@ -13190,7 +13485,7 @@ snapshots: domhandler: 5.0.3 htmlparser2: 10.0.0 picocolors: 1.1.1 - postcss: 8.5.6 + postcss: 8.5.12 postcss-media-query-parser: 0.2.3 before-after-hook@4.0.0: {} @@ -13700,14 +13995,14 @@ snapshots: dependencies: is-what: 3.14.1 - copy-webpack-plugin@13.0.1(webpack@5.105.0(esbuild@0.25.9)): + copy-webpack-plugin@14.0.0(webpack@5.105.0(esbuild@0.28.0)): dependencies: glob-parent: 6.0.2 normalize-path: 3.0.0 schema-utils: 4.3.3 - serialize-javascript: 6.0.2 + serialize-javascript: 7.0.3 tinyglobby: 0.2.14 - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) core-js-compat@3.46.0: dependencies: @@ -13751,18 +14046,18 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-loader@7.1.2(webpack@5.105.0(esbuild@0.25.9)): + css-loader@7.1.2(webpack@5.105.0(esbuild@0.28.0)): dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - postcss: 8.5.6 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.6) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.6) - postcss-modules-scope: 3.2.1(postcss@8.5.6) - postcss-modules-values: 4.0.0(postcss@8.5.6) + icss-utils: 5.1.0(postcss@8.5.12) + postcss: 8.5.12 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.12) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.12) + postcss-modules-scope: 3.2.1(postcss@8.5.12) + postcss-modules-values: 4.0.0(postcss@8.5.12) postcss-value-parser: 4.2.0 semver: 7.7.2 optionalDependencies: - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) css-select@6.0.0: dependencies: @@ -14206,7 +14501,7 @@ snapshots: dependencies: es6-promise: 4.2.8 - esbuild-wasm@0.25.9: {} + esbuild-wasm@0.28.0: {} esbuild@0.25.9: optionalDependencies: @@ -14237,6 +14532,64 @@ snapshots: '@esbuild/win32-ia32': 0.25.9 '@esbuild/win32-x64': 0.25.9 + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + esbuild@0.28.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -14590,9 +14943,9 @@ snapshots: dependencies: pend: 1.2.0 - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 fetch-blob@3.2.0: dependencies: @@ -15229,9 +15582,9 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.5.6): + icss-utils@5.1.0(postcss@8.5.12): dependencies: - postcss: 8.5.6 + postcss: 8.5.12 idb@7.1.1: {} @@ -15893,11 +16246,11 @@ snapshots: picocolors: 1.1.1 shell-quote: 1.8.3 - less-loader@12.3.0(less@4.4.0)(webpack@5.105.0(esbuild@0.25.9)): + less-loader@12.3.0(less@4.4.0)(webpack@5.105.0(esbuild@0.28.0)): dependencies: less: 4.4.0 optionalDependencies: - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) less@4.4.0: dependencies: @@ -15918,11 +16271,11 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - license-webpack-plugin@4.0.2(webpack@5.105.0(esbuild@0.25.9)): + license-webpack-plugin@4.0.2(webpack@5.105.0(esbuild@0.28.0)): dependencies: webpack-sources: 3.3.3 optionalDependencies: - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) lie@3.3.0: dependencies: @@ -16155,11 +16508,11 @@ snapshots: mimic-function@5.0.1: {} - mini-css-extract-plugin@2.9.4(webpack@5.105.0(esbuild@0.25.9)): + mini-css-extract-plugin@2.9.4(webpack@5.105.0(esbuild@0.28.0)): dependencies: schema-utils: 4.3.3 tapable: 2.3.0 - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) minimalistic-assert@1.0.1: {} @@ -16303,7 +16656,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@angular/compiler-cli': 20.3.7(@angular/compiler@20.3.7)(typescript@5.9.2) - '@rollup/plugin-json': 6.1.0(rollup@4.52.3) + '@rollup/plugin-json': 6.1.0(rollup@4.59.0) '@rollup/wasm-node': 4.52.4 ajv: 8.17.1 ansi-colors: 4.1.3 @@ -16318,15 +16671,15 @@ snapshots: less: 4.4.0 ora: 8.2.0 piscina: 5.1.3 - postcss: 8.5.6 - rollup-plugin-dts: 6.2.1(rollup@4.52.3)(typescript@5.9.2) + postcss: 8.5.12 + rollup-plugin-dts: 6.2.1(rollup@4.59.0)(typescript@5.9.2) rxjs: 7.8.2 sass: 1.90.0 tinyglobby: 0.2.14 tslib: 2.8.1 typescript: 5.9.2 optionalDependencies: - rollup: 4.52.3 + rollup: 4.59.0 nock@14.0.10: dependencies: @@ -16740,6 +17093,8 @@ snapshots: picomatch@4.0.3: {} + picomatch@4.0.4: {} + pify@2.3.0: {} pify@3.0.0: {} @@ -16806,39 +17161,39 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-loader@8.1.1(postcss@8.5.6)(typescript@5.9.2)(webpack@5.105.0(esbuild@0.25.9)): + postcss-loader@8.1.1(postcss@8.5.12)(typescript@5.9.2)(webpack@5.105.0(esbuild@0.28.0)): dependencies: cosmiconfig: 9.0.0(typescript@5.9.2) jiti: 1.21.7 - postcss: 8.5.6 + postcss: 8.5.12 semver: 7.7.2 optionalDependencies: - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) transitivePeerDependencies: - typescript postcss-media-query-parser@0.2.3: {} - postcss-modules-extract-imports@3.1.0(postcss@8.5.6): + postcss-modules-extract-imports@3.1.0(postcss@8.5.12): dependencies: - postcss: 8.5.6 + postcss: 8.5.12 - postcss-modules-local-by-default@4.2.0(postcss@8.5.6): + postcss-modules-local-by-default@4.2.0(postcss@8.5.12): dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - postcss: 8.5.6 + icss-utils: 5.1.0(postcss@8.5.12) + postcss: 8.5.12 postcss-selector-parser: 7.1.0 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.1(postcss@8.5.6): + postcss-modules-scope@3.2.1(postcss@8.5.12): dependencies: - postcss: 8.5.6 + postcss: 8.5.12 postcss-selector-parser: 7.1.0 - postcss-modules-values@4.0.0(postcss@8.5.6): + postcss-modules-values@4.0.0(postcss@8.5.12): dependencies: - icss-utils: 5.1.0(postcss@8.5.6) - postcss: 8.5.6 + icss-utils: 5.1.0(postcss@8.5.12) + postcss: 8.5.12 postcss-selector-parser@7.1.0: dependencies: @@ -16847,7 +17202,7 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.5.6: + postcss@8.5.12: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -17202,7 +17557,7 @@ snapshots: adjust-sourcemap-loader: 4.0.0 convert-source-map: 1.9.0 loader-utils: 2.0.4 - postcss: 8.5.6 + postcss: 8.5.12 source-map: 0.6.1 resolve@1.22.10: @@ -17257,81 +17612,50 @@ snapshots: node-fetch: 3.3.2 spdx-expression-validate: 2.0.0 - rollup-plugin-dts@6.2.1(rollup@4.46.2)(typescript@5.9.2): + rollup-plugin-dts@6.2.1(rollup@4.59.0)(typescript@5.9.2): dependencies: magic-string: 0.30.17 - rollup: 4.46.2 + rollup: 4.59.0 typescript: 5.9.2 optionalDependencies: '@babel/code-frame': 7.27.1 - rollup-plugin-dts@6.2.1(rollup@4.52.3)(typescript@5.9.2): + rollup-plugin-sourcemaps2@0.5.3(@types/node@22.18.10)(rollup@4.59.0): dependencies: - magic-string: 0.30.17 - rollup: 4.52.3 - typescript: 5.9.2 - optionalDependencies: - '@babel/code-frame': 7.27.1 - - rollup-plugin-sourcemaps2@0.5.3(@types/node@22.18.10)(rollup@4.46.2): - dependencies: - '@rollup/pluginutils': 5.2.0(rollup@4.46.2) - rollup: 4.46.2 + '@rollup/pluginutils': 5.2.0(rollup@4.59.0) + rollup: 4.59.0 optionalDependencies: '@types/node': 22.18.10 - rollup@4.46.2: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.46.2 - '@rollup/rollup-android-arm64': 4.46.2 - '@rollup/rollup-darwin-arm64': 4.46.2 - '@rollup/rollup-darwin-x64': 4.46.2 - '@rollup/rollup-freebsd-arm64': 4.46.2 - '@rollup/rollup-freebsd-x64': 4.46.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.46.2 - '@rollup/rollup-linux-arm-musleabihf': 4.46.2 - '@rollup/rollup-linux-arm64-gnu': 4.46.2 - '@rollup/rollup-linux-arm64-musl': 4.46.2 - '@rollup/rollup-linux-loongarch64-gnu': 4.46.2 - '@rollup/rollup-linux-ppc64-gnu': 4.46.2 - '@rollup/rollup-linux-riscv64-gnu': 4.46.2 - '@rollup/rollup-linux-riscv64-musl': 4.46.2 - '@rollup/rollup-linux-s390x-gnu': 4.46.2 - '@rollup/rollup-linux-x64-gnu': 4.46.2 - '@rollup/rollup-linux-x64-musl': 4.46.2 - '@rollup/rollup-win32-arm64-msvc': 4.46.2 - '@rollup/rollup-win32-ia32-msvc': 4.46.2 - '@rollup/rollup-win32-x64-msvc': 4.46.2 - fsevents: 2.3.3 - - rollup@4.52.3: + rollup@4.59.0: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.52.3 - '@rollup/rollup-android-arm64': 4.52.3 - '@rollup/rollup-darwin-arm64': 4.52.3 - '@rollup/rollup-darwin-x64': 4.52.3 - '@rollup/rollup-freebsd-arm64': 4.52.3 - '@rollup/rollup-freebsd-x64': 4.52.3 - '@rollup/rollup-linux-arm-gnueabihf': 4.52.3 - '@rollup/rollup-linux-arm-musleabihf': 4.52.3 - '@rollup/rollup-linux-arm64-gnu': 4.52.3 - '@rollup/rollup-linux-arm64-musl': 4.52.3 - '@rollup/rollup-linux-loong64-gnu': 4.52.3 - '@rollup/rollup-linux-ppc64-gnu': 4.52.3 - '@rollup/rollup-linux-riscv64-gnu': 4.52.3 - '@rollup/rollup-linux-riscv64-musl': 4.52.3 - '@rollup/rollup-linux-s390x-gnu': 4.52.3 - '@rollup/rollup-linux-x64-gnu': 4.52.3 - '@rollup/rollup-linux-x64-musl': 4.52.3 - '@rollup/rollup-openharmony-arm64': 4.52.3 - '@rollup/rollup-win32-arm64-msvc': 4.52.3 - '@rollup/rollup-win32-ia32-msvc': 4.52.3 - '@rollup/rollup-win32-x64-gnu': 4.52.3 - '@rollup/rollup-win32-x64-msvc': 4.52.3 + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 router@2.2.0: @@ -17385,12 +17709,12 @@ snapshots: safer-buffer@2.1.2: {} - sass-loader@16.0.5(sass@1.90.0)(webpack@5.105.0(esbuild@0.25.9)): + sass-loader@16.0.5(sass@1.90.0)(webpack@5.105.0(esbuild@0.28.0)): dependencies: neo-async: 2.6.2 optionalDependencies: sass: 1.90.0 - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) sass@1.90.0: dependencies: @@ -17415,9 +17739,9 @@ snapshots: schema-utils@4.3.3: dependencies: '@types/json-schema': 7.0.15 - ajv: 8.17.1 + ajv: 8.18.0 ajv-formats: 2.1.1 - ajv-keywords: 5.1.0(ajv@8.17.1) + ajv-keywords: 5.1.0(ajv@8.18.0) select-hose@2.0.0: {} @@ -17497,6 +17821,8 @@ snapshots: dependencies: randombytes: 2.1.0 + serialize-javascript@7.0.3: {} + serve-index@1.9.1: dependencies: accepts: 1.3.8 @@ -17711,11 +18037,11 @@ snapshots: source-map-js@1.2.1: {} - source-map-loader@5.0.0(webpack@5.105.0(esbuild@0.25.9)): + source-map-loader@5.0.0(webpack@5.105.0(esbuild@0.28.0)): dependencies: iconv-lite: 0.6.3 source-map-js: 1.2.1 - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) source-map-support@0.4.18: dependencies: @@ -18014,16 +18340,16 @@ snapshots: transitivePeerDependencies: - supports-color - terser-webpack-plugin@5.3.16(esbuild@0.25.9)(webpack@5.105.0(esbuild@0.25.9)): + terser-webpack-plugin@5.3.16(esbuild@0.28.0)(webpack@5.105.0(esbuild@0.28.0)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.43.1 - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) optionalDependencies: - esbuild: 0.25.9 + esbuild: 0.28.0 terser@5.43.1: dependencies: @@ -18067,13 +18393,13 @@ snapshots: tinyglobby@0.2.14: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 tinyglobby@0.2.15: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 tinypool@1.1.1: {} @@ -18449,7 +18775,7 @@ snapshots: debug: 4.4.3(supports-color@10.2.2) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.11(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -18464,31 +18790,13 @@ snapshots: - tsx - yaml - vite@7.1.11(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1): + vite@7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: - esbuild: 0.25.9 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.52.3 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 24.9.1 - fsevents: 2.3.3 - jiti: 1.21.7 - less: 4.4.0 - sass: 1.90.0 - terser: 5.43.1 - tsx: 4.20.6 - yaml: 2.8.1 - - vite@7.1.5(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1): - dependencies: - esbuild: 0.25.9 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.52.3 + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.12 + rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 24.9.1 @@ -18504,7 +18812,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.5(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -18522,7 +18830,7 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.5(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) vite-node: 3.2.4(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.0)(sass@1.90.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: @@ -18594,7 +18902,7 @@ snapshots: webidl-conversions@7.0.0: {} - webpack-dev-middleware@7.4.2(webpack@5.105.0(esbuild@0.25.9)): + webpack-dev-middleware@7.4.2(webpack@5.105.0(esbuild@0.28.0)): dependencies: colorette: 2.0.20 memfs: 4.49.0 @@ -18603,9 +18911,9 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) - webpack-dev-server@5.2.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)(webpack@5.105.0(esbuild@0.25.9)): + webpack-dev-server@5.2.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)(webpack@5.105.0(esbuild@0.28.0)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -18633,10 +18941,10 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.2(webpack@5.105.0(esbuild@0.25.9)) + webpack-dev-middleware: 7.4.2(webpack@5.105.0(esbuild@0.28.0)) ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@6.0.5) optionalDependencies: - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) transitivePeerDependencies: - bufferutil - debug @@ -18651,12 +18959,12 @@ snapshots: webpack-sources@3.3.3: {} - webpack-subresource-integrity@5.1.0(webpack@5.105.0(esbuild@0.25.9)): + webpack-subresource-integrity@5.1.0(webpack@5.105.0(esbuild@0.28.0)): dependencies: typed-assert: 1.0.9 - webpack: 5.105.0(esbuild@0.25.9) + webpack: 5.105.0(esbuild@0.28.0) - webpack@5.105.0(esbuild@0.25.9): + webpack@5.105.0(esbuild@0.28.0): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -18680,7 +18988,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.16(esbuild@0.25.9)(webpack@5.105.0(esbuild@0.25.9)) + terser-webpack-plugin: 5.3.16(esbuild@0.28.0)(webpack@5.105.0(esbuild@0.28.0)) watchpack: 2.5.1 webpack-sources: 3.3.3 transitivePeerDependencies: diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts index 19e7dcd28b60..22befac99f9b 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts @@ -144,6 +144,7 @@ export default async function () { { ...process.env, 'PORT': String(port), + 'NG_ALLOWED_HOSTS': 'localhost', }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-ngmodule.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-ngmodule.ts index f05d2182bbd2..f21690c47df9 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-ngmodule.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-ngmodule.ts @@ -157,6 +157,7 @@ export default async function () { /Node Express server listening on/, { ...process.env, + 'NG_ALLOWED_HOSTS': 'localhost', 'PORT': String(port), }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-standalone.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-standalone.ts index 7c819e67693a..b8df3812f05c 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-standalone.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-standalone.ts @@ -114,6 +114,7 @@ export default async function () { { ...process.env, 'PORT': String(port), + 'NG_ALLOWED_HOSTS': 'localhost', }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-base-href.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-base-href.ts index 6d0a45459a16..aa6f7e3426ee 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-base-href.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-base-href.ts @@ -111,6 +111,7 @@ async function spawnServer(): Promise { { ...process.env, 'PORT': String(port), + 'NG_ALLOWED_HOSTS': 'localhost', }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-sub-path.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-sub-path.ts index 79fc755c4477..7bc323311b4f 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-sub-path.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-sub-path.ts @@ -146,6 +146,7 @@ async function spawnServer(): Promise { { ...process.env, 'PORT': String(port), + 'NG_ALLOWED_HOSTS': 'localhost', }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n.ts index 994d77343d1e..efbff9871bbc 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n.ts @@ -122,6 +122,7 @@ async function spawnServer(): Promise { { ...process.env, 'PORT': String(port), + 'NG_ALLOWED_HOSTS': 'localhost', }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-platform-neutral.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-platform-neutral.ts index 130ade10ba9f..6fdd1998cdd2 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-platform-neutral.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-platform-neutral.ts @@ -11,7 +11,6 @@ import { import { updateJsonFile, useSha } from '../../../utils/project'; import { getGlobalVariable } from '../../../utils/env'; import { findFreePort } from '../../../utils/network'; -import { readFile } from 'node:fs/promises'; export default async function () { assert( @@ -98,6 +97,8 @@ export default async function () { const options = buildTarget['options']; options['ssr']['experimentalPlatform'] = 'neutral'; options['outputMode'] = 'server'; + options['security'] ??= {}; + options['security']['allowedHosts'] = ['localhost']; }); await noSilentNg('build'); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts index 5205d20eeb0a..ff72cb8e8df2 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts @@ -206,6 +206,7 @@ async function spawnServer(): Promise { { ...process.env, 'PORT': String(port), + 'NG_ALLOWED_HOSTS': 'localhost', }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-preload-links.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-preload-links.ts index f1437392492d..fe316e3cd157 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-preload-links.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-preload-links.ts @@ -196,6 +196,7 @@ async function spawnServer(): Promise { { ...process.env, 'PORT': String(port), + 'NG_ALLOWED_HOSTS': 'localhost', }, );