From 1af17672b8adbf21c8ba28d628c01a8aa923014b Mon Sep 17 00:00:00 2001 From: Jonathan <64296013+Joncallim@users.noreply.github.com> Date: Thu, 3 Sep 2026 01:37:31 +0800 Subject: [PATCH 1/6] fix: retain telemetry claim ownership through timeout --- crates/dockermap-daemon/src/cache_refresh.rs | 230 +++++++++++++++++-- 1 file changed, 215 insertions(+), 15 deletions(-) diff --git a/crates/dockermap-daemon/src/cache_refresh.rs b/crates/dockermap-daemon/src/cache_refresh.rs index 703737ab..2e0e98d4 100644 --- a/crates/dockermap-daemon/src/cache_refresh.rs +++ b/crates/dockermap-daemon/src/cache_refresh.rs @@ -103,7 +103,16 @@ struct ResourceTelemetryCache { collection_state: ObservedResourceTelemetryCollectionState, samples: BTreeMap, rotation_cursor: usize, - in_flight: usize, + next_flight_id: u64, + in_flight: BTreeMap, +} + +/// Private cancellation-recovery record. This never reaches a response: it +/// lets a subsequent refresh reclaim a cache slot if the entire refresh task +/// was aborted while an attempt was pending. +#[derive(Clone)] +struct ResourceTelemetryFlight { + deadline: Duration, } #[derive(Clone)] @@ -124,6 +133,7 @@ struct TelemetryCounters { #[derive(Clone)] struct ResourceTelemetryClaim { + flight_id: u64, raw_container_id: String, public_container_id: String, source_generation: u64, @@ -137,7 +147,8 @@ impl Default for ResourceTelemetryCache { collection_state: ObservedResourceTelemetryCollectionState::Unavailable, samples: BTreeMap::new(), rotation_cursor: 0, - in_flight: 0, + next_flight_id: 0, + in_flight: BTreeMap::new(), } } } @@ -146,6 +157,24 @@ impl ResourceTelemetryCache { fn source_reset() -> Self { Self::default() } + + /// A timeout normally completes in the refresh-owned future. This is the + /// secondary recovery path for cancellation of that entire future: stale + /// claims cannot consume the fixed two-slot budget forever, and a late + /// completion cannot remove a newer claim because every flight is opaque + /// and unique. + fn reclaim_expired_flights(&mut self, now: Duration) { + self.in_flight.retain(|_, flight| flight.deadline > now); + if self.in_flight.is_empty() + && self.collection_state == ObservedResourceTelemetryCollectionState::Collecting + { + self.collection_state = if self.samples.is_empty() { + ObservedResourceTelemetryCollectionState::Stale + } else { + ObservedResourceTelemetryCollectionState::Fresh + }; + } + } } /// Daemon-lifetime inventory deltas only. This contains no Docker event stream @@ -737,9 +766,16 @@ pub(crate) async fn refresh_cache(state: &AppState) { } } -/// Launch at most two finite, snapshot-selected stats requests per refresh. +/// Complete at most two finite, snapshot-selected stats requests per refresh. /// The source generation and both revisions are captured before any I/O; a /// completion may publish only if all three still attest the same Docker view. +/// +/// These futures deliberately remain owned by this refresh call rather than +/// being detached. A detached task can be cancelled during shutdown or become +/// non-cooperative below the HTTP boundary after its cache claim increments; +/// in either case there would be no owner left to release the fixed in-flight +/// slot. Waiting for the bounded attempts keeps the claim and its completion +/// in one lifetime while the two requests still run concurrently. async fn refresh_resource_telemetry( state: &AppState, snapshot: DockerSnapshot, @@ -758,10 +794,9 @@ async fn refresh_resource_telemetry( return; } let claims = claim_resource_telemetry(&state.cache, &snapshot, source_generation).await; - for claim in claims { - let state = state.clone(); + let completions = futures_util::future::join_all(claims.into_iter().map(|claim| { let collector = collector.clone(); - tokio::spawn(async move { + async move { let observed_at_ms = wall_clock_millis(); let result = timeout( RESOURCE_TELEMETRY_TIMEOUT, @@ -771,8 +806,12 @@ async fn refresh_resource_telemetry( .ok() .and_then(Result::ok) .and_then(|stats| observed_at_ms.map(|at| (at, stats))); - apply_resource_telemetry(&state.cache, claim, result).await; - }); + (claim, result) + } + })) + .await; + for (claim, result) in completions { + apply_resource_telemetry(&state.cache, claim, result).await; } } @@ -788,7 +827,10 @@ async fn claim_resource_telemetry( let model_revision = cache.snapshot.model_revision.clone(); let observation_revision = cache.docker_observation_token(); let telemetry = &mut cache.resource_telemetry; - let remaining = MAX_CONCURRENT_RESOURCE_TELEMETRY_REQUESTS.saturating_sub(telemetry.in_flight); + let now = monotonic_now(); + telemetry.reclaim_expired_flights(now); + let remaining = + MAX_CONCURRENT_RESOURCE_TELEMETRY_REQUESTS.saturating_sub(telemetry.in_flight.len()); if remaining == 0 { return Vec::new(); } @@ -814,7 +856,21 @@ async fn claim_resource_telemetry( .map(|offset| { let (public_container_id, raw_container_id) = containers[(telemetry.rotation_cursor + offset) % containers.len()].clone(); + telemetry.next_flight_id = telemetry + .next_flight_id + .checked_add(1) + .expect("resource telemetry flight identifier overflow"); + let flight_id = telemetry.next_flight_id; + telemetry.in_flight.insert( + flight_id, + ResourceTelemetryFlight { + deadline: now + .checked_add(RESOURCE_TELEMETRY_TIMEOUT) + .expect("resource telemetry flight deadline overflow"), + }, + ); ResourceTelemetryClaim { + flight_id, raw_container_id, public_container_id, source_generation, @@ -824,7 +880,6 @@ async fn claim_resource_telemetry( }) .collect::>(); telemetry.rotation_cursor = (telemetry.rotation_cursor + count) % containers.len(); - telemetry.in_flight += claims.len(); telemetry.collection_state = ObservedResourceTelemetryCollectionState::Collecting; claims } @@ -849,12 +904,17 @@ async fn apply_resource_telemetry( let revision_changed = cache.snapshot.model_revision != claim.model_revision || cache.docker_observation_token() != claim.observation_revision; let telemetry = &mut cache.resource_telemetry; - telemetry.in_flight = telemetry.in_flight.saturating_sub(1); + // An expired claim was reclaimed after a cancelled refresh. Do not let + // its delayed response publish into a newer flight or consume that + // flight's capacity. + if telemetry.in_flight.remove(&claim.flight_id).is_none() { + return; + } // A newer Docker publication supersedes this sample. Release the fixed // concurrency claim, but never merge measurements from a different model // or observation revision into the current public envelope. if revision_changed || !still_current { - telemetry.collection_state = if telemetry.in_flight == 0 { + telemetry.collection_state = if telemetry.in_flight.is_empty() { ObservedResourceTelemetryCollectionState::Stale } else { ObservedResourceTelemetryCollectionState::Collecting @@ -872,7 +932,7 @@ async fn apply_resource_telemetry( telemetry.samples.insert(claim.public_container_id, record); } } - telemetry.collection_state = if telemetry.in_flight == 0 { + telemetry.collection_state = if telemetry.in_flight.is_empty() { if telemetry.samples.is_empty() { ObservedResourceTelemetryCollectionState::Stale } else { @@ -3969,6 +4029,147 @@ mod scheduler_tests { } } + #[tokio::test] + async fn resource_telemetry_timeout_is_completed_before_the_refresh_releases_its_claim() { + let directory = tempfile::tempdir().unwrap(); + let socket = directory.path().join("telemetry-timeout.sock"); + let listener = UnixListener::bind(&socket).unwrap(); + let server = tokio::spawn(async move { + let (mut connection, _) = listener.accept().await.unwrap(); + let mut request = Vec::new(); + let mut bytes = [0; 1_024]; + loop { + let read = connection.read(&mut bytes).await.unwrap(); + assert!(read > 0, "client closed before the fixed request head"); + request.extend_from_slice(&bytes[..read]); + if request.windows(4).any(|window| window == b"\r\n\r\n") { + break; + } + } + assert!( + request.starts_with(b"GET /containers/"), + "only the snapshot-selected stats route is attempted" + ); + // Headers prove the fixed request reached the gateway-like peer, + // while the intentionally absent response body keeps Bollard's + // stream pending until the daemon's own 750 ms bound cancels it. + connection + .write_all( + b"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ntransfer-encoding: chunked\r\n\r\n", + ) + .await + .unwrap(); + tokio::time::sleep(Duration::from_secs(5)).await; + }); + + let snapshot = mock_snapshot(); + let state = AppState { + cache: Arc::new(RwLock::new(docker_cache(snapshot.clone()))), + docker: Arc::new(RwLock::new(Some(DockerCollector::with_client( + Docker::connect_with_unix(socket.to_str().unwrap(), 5, API_DEFAULT_VERSION) + .unwrap(), + None, + )))), + provider_slot_in_flight: Arc::new(ProviderSlotFlights::default()), + }; + + let started = tokio::time::Instant::now(); + refresh_resource_telemetry(&state, snapshot, 0).await; + assert!( + started.elapsed() >= RESOURCE_TELEMETRY_TIMEOUT, + "the refresh owns the request until its timeout completes" + ); + assert!( + started.elapsed() < Duration::from_secs(2), + "a non-cooperative response body cannot outlive the finite bound" + ); + let cache = state.cache.read().await; + assert!(cache.resource_telemetry.in_flight.is_empty()); + assert!(cache.resource_telemetry.samples.is_empty()); + assert_eq!( + cache.resource_telemetry.collection_state, + ObservedResourceTelemetryCollectionState::Stale + ); + drop(cache); + server.abort(); + assert!(server.await.unwrap_err().is_cancelled()); + } + + #[tokio::test] + async fn aborted_telemetry_refresh_reclaims_only_its_expired_claim_before_retrying() { + let directory = tempfile::tempdir().unwrap(); + let socket = directory.path().join("telemetry-abort.sock"); + let listener = UnixListener::bind(&socket).unwrap(); + let (claimed_tx, mut claimed_rx) = mpsc::unbounded_channel(); + let server = tokio::spawn(async move { + let (mut connection, _) = listener.accept().await.unwrap(); + let mut request = Vec::new(); + let mut bytes = [0; 1_024]; + loop { + let read = connection.read(&mut bytes).await.unwrap(); + assert!(read > 0, "client closed before the fixed request head"); + request.extend_from_slice(&bytes[..read]); + if request.windows(4).any(|window| window == b"\r\n\r\n") { + break; + } + } + claimed_tx.send(()).unwrap(); + connection + .write_all( + b"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ntransfer-encoding: chunked\r\n\r\n", + ) + .await + .unwrap(); + tokio::time::sleep(Duration::from_secs(5)).await; + }); + + let snapshot = mock_snapshot(); + let state = AppState { + cache: Arc::new(RwLock::new(docker_cache(snapshot.clone()))), + docker: Arc::new(RwLock::new(Some(DockerCollector::with_client( + Docker::connect_with_unix(socket.to_str().unwrap(), 5, API_DEFAULT_VERSION) + .unwrap(), + None, + )))), + provider_slot_in_flight: Arc::new(ProviderSlotFlights::default()), + }; + let refresh_state = state.clone(); + let refresh_snapshot = snapshot.clone(); + let refresh = tokio::spawn(async move { + refresh_resource_telemetry(&refresh_state, refresh_snapshot, 0).await; + }); + tokio::time::timeout(Duration::from_secs(2), claimed_rx.recv()) + .await + .expect("the fixed stats request is claimed before cancellation") + .expect("test gateway reports the claim"); + refresh.abort(); + assert!(refresh.await.unwrap_err().is_cancelled()); + assert_eq!( + state.cache.read().await.resource_telemetry.in_flight.len(), + MAX_CONCURRENT_RESOURCE_TELEMETRY_REQUESTS + ); + + tokio::time::sleep(RESOURCE_TELEMETRY_TIMEOUT + Duration::from_millis(25)).await; + let retry = claim_resource_telemetry(&state.cache, &snapshot, 0).await; + assert_eq!( + retry.len(), + MAX_CONCURRENT_RESOURCE_TELEMETRY_REQUESTS, + "the abandoned claim no longer consumes capacity" + ); + let cache = state.cache.read().await; + assert_eq!( + cache.resource_telemetry.in_flight.len(), + MAX_CONCURRENT_RESOURCE_TELEMETRY_REQUESTS + ); + assert_eq!( + cache.resource_telemetry.next_flight_id, + (MAX_CONCURRENT_RESOURCE_TELEMETRY_REQUESTS * 2) as u64 + ); + drop(cache); + server.abort(); + assert!(server.await.unwrap_err().is_cancelled()); + } + #[test] fn telemetry_sanitizer_retains_only_numeric_metrics_and_derives_rates() { let first = sanitize_resource_telemetry( @@ -4077,10 +4278,9 @@ mod scheduler_tests { telemetry .samples .insert(record.public.container_id.clone(), record); - telemetry.in_flight = 1; let reset = ResourceTelemetryCache::source_reset(); assert!(reset.samples.is_empty()); - assert_eq!(reset.in_flight, 0); + assert!(reset.in_flight.is_empty()); assert_eq!( reset.collection_state, ObservedResourceTelemetryCollectionState::Unavailable From 048e9d563a2777f4d1fe0a0abcead2298110321c Mon Sep 17 00:00:00 2001 From: Jonathan <64296013+Joncallim@users.noreply.github.com> Date: Thu, 3 Sep 2026 01:56:48 +0800 Subject: [PATCH 2/6] test: make live telemetry evidence race-safe --- tests/e2e/resource-telemetry-live-docker.spec.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tests/e2e/resource-telemetry-live-docker.spec.ts b/tests/e2e/resource-telemetry-live-docker.spec.ts index 02f2531e..c4d204b0 100644 --- a/tests/e2e/resource-telemetry-live-docker.spec.ts +++ b/tests/e2e/resource-telemetry-live-docker.spec.ts @@ -58,22 +58,12 @@ test("collects bounded opaque Docker telemetry through the unfiltered fixture ga const v1 = await getJson(`${stack.apiUrl}${telemetryPaths[1]}`, headers); assertBoundedOpaqueTelemetry(v1); - // Every public sample must still be attached to the current live model; - // only opaque node IDs cross this assertion boundary. - const runtimeMap = await getJson<{ source: string; nodes: Array<{ id: string }> }>( - `${stack.apiUrl}/api/runtime/map`, - headers, - ); - expect(runtimeMap.source).toBe("docker"); - const publicNodeIds = new Set(runtimeMap.nodes.map((node) => node.id)); - for (const sample of current.samples) expect(publicNodeIds.has(sample.containerId)).toBe(true); - // Closing only this fixture's gateway forces Docker -> mock fallback. // The response must clear retained samples and revision anchors instead of // relabeling live observations as mock data. await stack.stopDockerGateway?.(); await expect.poll( - async () => (await getJson<{ mode: string }>(`${stack.apiUrl}/api/health`, headers)).mode, + async () => (await getJson<{ daemon: { mode: string } }>(`${stack.apiUrl}/api/health`, headers)).daemon.mode, { timeout: 15_000 }, ).toBe("mock"); for (const path of telemetryPaths) { @@ -127,6 +117,7 @@ function assertBoundedOpaqueTelemetry(value: Telemetry) { expect(/^\S{1,64}$/.test(value.currentObservationRevision ?? "")).toBe(true); expect(value.samples.length).toBeGreaterThan(0); expect(value.samples.length).toBeLessThanOrEqual(16); + const sampleIds = new Set(); for (const sample of value.samples) { expect(hasExactKeys(sample, [ "containerId", @@ -137,6 +128,8 @@ function assertBoundedOpaqueTelemetry(value: Telemetry) { "networkTxBytesPerSecond", ])).toBe(true); expect(/^docker_container_[0-9a-f]{64}$/.test(sample.containerId)).toBe(true); + expect(sampleIds.has(sample.containerId)).toBe(false); + sampleIds.add(sample.containerId); const metrics = [ sample.cpuPercent, sample.memoryUsedBytes, From 03bee1df19e4effe51539bd0ab24e39f762dfac0 Mon Sep 17 00:00:00 2001 From: Jonathan <64296013+Joncallim@users.noreply.github.com> Date: Thu, 3 Sep 2026 01:58:00 +0800 Subject: [PATCH 3/6] docs: record bounded live telemetry evidence --- docs/architecture/ARCHITECTURE.md | 11 ++++++++--- docs/release/RELEASE_CHECKLIST.md | 14 +++++++++----- docs/security/THREAT_MODEL.md | 9 +++++++-- docs/testing/TESTING_PLAN.md | 21 +++++++++++++++++---- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md index 9a57314d..903716e2 100644 --- a/docs/architecture/ARCHITECTURE.md +++ b/docs/architecture/ARCHITECTURE.md @@ -188,9 +188,14 @@ This is deliberately bounded current-state evidence, not proof of workload health, demand, performance cause, traffic destination, restart behavior, or a complete account of host resource use. The labelled live-Docker fixture uses a Docker label scope, so it intentionally exercises the unavailable path rather -than real stats collection. Before release claims include live telemetry, the -project still needs isolated **unfiltered** live-Docker evidence for the finite -request, cap/timeout behavior, expiry, and cleanup/resource-budget effects. +than real stats collection. Isolated **unfiltered** live-Docker evidence is a +separate exact dual opt-in (`DOCKERMAP_E2E_LIVE_DOCKER=1` and +`DOCKERMAP_E2E_UNFILTERED_TELEMETRY=1`): it proves authenticated finite +collection, public 16-row and 8-second-expiry bounds, source reset, and owned +fixture cleanup without exposing raw Docker data. The deterministic daemon +suite proves the two-request and 750-ms bounds. This focused proof does not +make the ordinary label-scoped live browser suite green; that suite remains an +independent release gate. #### Current finding policy diff --git a/docs/release/RELEASE_CHECKLIST.md b/docs/release/RELEASE_CHECKLIST.md index 6786261a..b0fbf80f 100644 --- a/docs/release/RELEASE_CHECKLIST.md +++ b/docs/release/RELEASE_CHECKLIST.md @@ -77,11 +77,15 @@ These tasks must be complete before tagging `v0.1.0-alpha`. ## Follow-up evidence and product work -- [ ] Capture isolated unfiltered live-Docker evidence for current resource - telemetry. It must demonstrate the exact finite stats authority, current-only - expiry, bounded request/concurrency behavior, and fixture cleanup/resource - budget. The normal labelled live-Docker harness cannot supply this evidence: - its label scope correctly denies every stats request. +- [x] Capture isolated unfiltered live-Docker evidence for current resource + telemetry. The focused exact dual-opt-in proof records authenticated finite + stats authority, current-only 16-row/8-second-expiry publication, + Docker-to-mock reset, and owned-fixture cleanup. Deterministic daemon tests + cover the two-request and 750-ms limits. The normal labelled live-Docker + harness cannot supply this evidence because its label scope correctly denies + every stats request. At #247's integration head the focused proof passed; + the separate normal live browser suite has an unrelated Service Detail + failure and remains an unverified release gate. - [x] Add provider-specific redaction fixtures for systemd, tmux, npm/package metadata, native process inspection, reverse-proxy config, and DNS collectors. diff --git a/docs/security/THREAT_MODEL.md b/docs/security/THREAT_MODEL.md index ec79aac6..647d62d9 100644 --- a/docs/security/THREAT_MODEL.md +++ b/docs/security/THREAT_MODEL.md @@ -137,8 +137,13 @@ Protections: Current limitations: this is numeric current-state evidence only. It does not authorize streaming stats, prove performance cause or workload health, or make -the label-scoped fixture eligible for stats collection. Real unfiltered -live-Docker collection and resource-budget evidence remain a release gap. +the label-scoped fixture eligible for stats collection. The focused unfiltered +live-Docker proof is deliberately separate and requires both exact opt-ins +(`DOCKERMAP_E2E_LIVE_DOCKER=1` and +`DOCKERMAP_E2E_UNFILTERED_TELEMETRY=1`); it checks the fixed request, +authentication, opaque bounded publication, source reset, and owned-fixture +cleanup. It does not turn the ordinary label-scoped live browser suite into a +green release gate. ### Host Provider Expansion diff --git a/docs/testing/TESTING_PLAN.md b/docs/testing/TESTING_PLAN.md index e1e4bae9..02a7668e 100644 --- a/docs/testing/TESTING_PLAN.md +++ b/docs/testing/TESTING_PLAN.md @@ -169,10 +169,23 @@ DockerMap excludes unrelated Docker resources. That label-scoped setup intentionally makes current resource telemetry unavailable: the gateway must deny per-container stats when a label filter is configured. It is evidence for scoped denial, not evidence that Docker stats -were collected. A separate isolated unfiltered live-Docker run is still needed -to record finite stats collection, expiry, bounded concurrency/cadence, and -its resource-budget/cleanup behavior before a release can claim live telemetry -evidence. +were collected. Finite unfiltered collection is covered by the separately +gated focused proof: + +```bash +DOCKERMAP_E2E_LIVE_DOCKER=1 DOCKERMAP_E2E_UNFILTERED_TELEMETRY=1 \ + npx playwright test --config tests/e2e/playwright.config.ts resource-telemetry-live-docker.spec.ts +``` + +Both values must be exactly `1`; the normal `npm run test:live-docker` command +does not opt in to host-wide unfiltered inventory. The focused proof records +authenticated canonical/v1 access, the fixed finite gateway request, opaque +current telemetry capped at 16 rows, 8-second metric expiry, Docker-to-mock +reset, and fixture cleanup. Deterministic daemon tests remain the evidence for +the two-request and 750-ms bounds. At #247's integration head, this focused +proof passed; the separate normal label-scoped live suite still has an +unrelated Service Detail browser failure, so that broad suite is not recorded +as green. ## Sandbox Fixture From 0c54e086e9c5a21a40cf70e076f12bebeef8dc6b Mon Sep 17 00:00:00 2001 From: Jonathan <64296013+Joncallim@users.noreply.github.com> Date: Thu, 3 Sep 2026 02:05:06 +0800 Subject: [PATCH 4/6] test: verify owned live fixture cleanup --- docs/release/RELEASE_CHECKLIST.md | 9 ++-- docs/testing/TESTING_PLAN.md | 4 +- tests/e2e/dockermapHarness.ts | 50 ++++++++++++++++++- .../resource-telemetry-live-docker.spec.ts | 7 ++- 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/docs/release/RELEASE_CHECKLIST.md b/docs/release/RELEASE_CHECKLIST.md index b0fbf80f..f7bf2893 100644 --- a/docs/release/RELEASE_CHECKLIST.md +++ b/docs/release/RELEASE_CHECKLIST.md @@ -82,10 +82,11 @@ These tasks must be complete before tagging `v0.1.0-alpha`. stats authority, current-only 16-row/8-second-expiry publication, Docker-to-mock reset, and owned-fixture cleanup. Deterministic daemon tests cover the two-request and 750-ms limits. The normal labelled live-Docker - harness cannot supply this evidence because its label scope correctly denies - every stats request. At #247's integration head the focused proof passed; - the separate normal live browser suite has an unrelated Service Detail - failure and remains an unverified release gate. +harness cannot supply this evidence because its label scope correctly denies +every stats request. At #247's integration head the focused proof passed; +the separate normal live browser suite has a Service Detail failure outside +this focused proof, whose cause has not been established, and remains an +unverified release gate. - [x] Add provider-specific redaction fixtures for systemd, tmux, npm/package metadata, native process inspection, reverse-proxy config, and DNS collectors. diff --git a/docs/testing/TESTING_PLAN.md b/docs/testing/TESTING_PLAN.md index 02a7668e..78241218 100644 --- a/docs/testing/TESTING_PLAN.md +++ b/docs/testing/TESTING_PLAN.md @@ -184,8 +184,8 @@ current telemetry capped at 16 rows, 8-second metric expiry, Docker-to-mock reset, and fixture cleanup. Deterministic daemon tests remain the evidence for the two-request and 750-ms bounds. At #247's integration head, this focused proof passed; the separate normal label-scoped live suite still has an -unrelated Service Detail browser failure, so that broad suite is not recorded -as green. +Service Detail browser failure outside this focused proof. Its cause has not +been established, so that broad suite is not recorded as green. ## Sandbox Fixture diff --git a/tests/e2e/dockermapHarness.ts b/tests/e2e/dockermapHarness.ts index 98821a15..98223991 100644 --- a/tests/e2e/dockermapHarness.ts +++ b/tests/e2e/dockermapHarness.ts @@ -27,6 +27,8 @@ export type Stack = { * the raw Docker response, target, or socket to a test. */ requestOwnedFixtureStats?: () => Promise; + /** True only when this fixture's generated Docker resources are absent. */ + ownedFixtureResourcesAbsent?: () => boolean; postProductionSessionBurst?: (client: "a" | "b", spoofedXForwardedForPrefix: string) => { elapsedMs: number; responses: Array<{ status: number; body: string }>; @@ -329,6 +331,7 @@ export async function startLiveDockerStack(options: { `/containers/${containerId}/stats?stream=false&one-shot=true`, ); }, + ownedFixtureResourcesAbsent: () => ownedFixtureResourcesAbsent(docker, fixture), stop: async () => { await stopProcesses(processes); cleanupLiveDocker(docker, fixture); @@ -337,8 +340,16 @@ export async function startLiveDockerStack(options: { } catch (error) { // A failed readiness/build step may have started the gateway or daemon. // Stop those owned processes before deleting the fixture socket directory. - await stopProcesses(processes); - cleanupLiveDocker(docker, fixture); + let cleanupError: unknown; + try { + await stopProcesses(processes); + cleanupLiveDocker(docker, fixture); + } catch (failure) { + cleanupError = failure; + } + if (cleanupError) { + throw new AggregateError([error, cleanupError], "Live fixture startup and cleanup both failed."); + } throw error; } } @@ -1068,7 +1079,42 @@ function cleanupLiveDocker(docker: string[], fixture: Fixture) { } catch { // Best-effort cleanup should not hide the original test result. } + const resourcesAbsent = ownedFixtureResourcesAbsent(docker, fixture); rmSync(fixture.dir, { recursive: true, force: true }); + if (!resourcesAbsent) { + // Keep this category fixed: Docker stderr can contain host identities and + // must not be copied into test output. + throw new Error("Owned live Docker fixture cleanup could not be verified."); + } +} + +/** + * Inspect only names and labels generated by this fixture. No broad Docker + * scan, user-selected target, raw response, or diagnostic crosses this helper. + */ +function ownedFixtureResourcesAbsent(docker: string[], fixture: Fixture): boolean { + const controlStillExists = dockerQuiet(docker, ["container", "inspect", fixture.controlContainerName], fixture.dir) !== null; + if (controlStillExists) return false; + for (const kind of ["container", "network", "volume"] as const) { + const args = kind === "container" + ? ["container", "ls", "--all", "--quiet", "--filter", `label=${fixture.labelFilter}`] + : [kind, "ls", "--quiet", "--filter", `label=${fixture.labelFilter}`]; + const output = dockerQuiet(docker, args, fixture.dir); + if (output === null || output.trim() !== "") return false; + } + return true; +} + +function dockerQuiet(docker: string[], args: string[], _cwd: string): string | null { + const result = spawnSync(docker[0], [...docker.slice(1), ...args], { + // Fixture cleanup removes fixture.dir before the focused spec performs its + // independent, closed post-stop check. Use the repository root so that + // check still observes only the generated name/label filters above. + cwd: repoRoot, + encoding: "utf8", + timeout: 120_000, + }); + return result.status === 0 ? result.stdout : null; } function cleanupProductionImage( diff --git a/tests/e2e/resource-telemetry-live-docker.spec.ts b/tests/e2e/resource-telemetry-live-docker.spec.ts index c4d204b0..212a3215 100644 --- a/tests/e2e/resource-telemetry-live-docker.spec.ts +++ b/tests/e2e/resource-telemetry-live-docker.spec.ts @@ -86,7 +86,12 @@ test("collects bounded opaque Docker telemetry through the unfiltered fixture ga ).toBe(true); } } finally { - await stack?.stop(); + if (stack) { + await stack.stop(); + // Cleanup inspection is limited to the fixture's generated control name + // and exact fixture label; it never enumerates unrelated resources. + expect(stack.ownedFixtureResourcesAbsent?.()).toBe(true); + } } }); From f76cc68f719f8d2382f61104381c5828f7a82947 Mon Sep 17 00:00:00 2001 From: Jonathan <64296013+Joncallim@users.noreply.github.com> Date: Thu, 3 Sep 2026 02:09:20 +0800 Subject: [PATCH 5/6] test: preserve live cleanup failures --- tests/e2e/dockermapHarness.ts | 12 ++++++++-- .../resource-telemetry-live-docker.spec.ts | 22 +++++++++++++++---- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/tests/e2e/dockermapHarness.ts b/tests/e2e/dockermapHarness.ts index 98223991..1b242b1c 100644 --- a/tests/e2e/dockermapHarness.ts +++ b/tests/e2e/dockermapHarness.ts @@ -1093,8 +1093,16 @@ function cleanupLiveDocker(docker: string[], fixture: Fixture) { * scan, user-selected target, raw response, or diagnostic crosses this helper. */ function ownedFixtureResourcesAbsent(docker: string[], fixture: Fixture): boolean { - const controlStillExists = dockerQuiet(docker, ["container", "inspect", fixture.controlContainerName], fixture.dir) !== null; - if (controlStillExists) return false; + // `inspect` cannot distinguish an absent container from an inaccessible + // Docker daemon without exposing stderr. A successful, anchored generated + // name query fails closed on either condition and cannot match another + // fixture's controls. + const controlMatches = dockerQuiet( + docker, + ["container", "ls", "--all", "--quiet", "--filter", `name=^/${fixture.controlContainerName}$`], + fixture.dir, + ); + if (controlMatches === null || controlMatches.trim() !== "") return false; for (const kind of ["container", "network", "volume"] as const) { const args = kind === "container" ? ["container", "ls", "--all", "--quiet", "--filter", `label=${fixture.labelFilter}`] diff --git a/tests/e2e/resource-telemetry-live-docker.spec.ts b/tests/e2e/resource-telemetry-live-docker.spec.ts index 212a3215..9ce96b6e 100644 --- a/tests/e2e/resource-telemetry-live-docker.spec.ts +++ b/tests/e2e/resource-telemetry-live-docker.spec.ts @@ -28,6 +28,7 @@ test("collects bounded opaque Docker telemetry through the unfiltered fixture ga ); let stack: Stack | undefined; + let testFailure: unknown; try { try { stack = await startLiveDockerStack({ apiToken: token, fixtureProfile: "unfiltered-telemetry" }); @@ -85,12 +86,25 @@ test("collects bounded opaque Docker telemetry through the unfiltered fixture ga && reset.samples.length === 0, ).toBe(true); } + } catch (error) { + testFailure = error; + throw error; } finally { if (stack) { - await stack.stop(); - // Cleanup inspection is limited to the fixture's generated control name - // and exact fixture label; it never enumerates unrelated resources. - expect(stack.ownedFixtureResourcesAbsent?.()).toBe(true); + try { + await stack.stop(); + // Cleanup inspection is limited to the fixture's generated control name + // and exact fixture label; it never enumerates unrelated resources. + expect(stack.ownedFixtureResourcesAbsent?.()).toBe(true); + } catch (cleanupFailure) { + if (testFailure) { + throw new AggregateError( + [testFailure, cleanupFailure], + "Live telemetry assertion and owned fixture cleanup both failed.", + ); + } + throw cleanupFailure; + } } } }); From efab33cb825a65fdf92381f98b198fb9dcc652a3 Mon Sep 17 00:00:00 2001 From: Jonathan <64296013+Joncallim@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:50:19 +0800 Subject: [PATCH 6/6] fix: upgrade express security dependency chain --- .github/workflows/ci.yml | 19 +- Dockerfile | 20 +- apps/api/package.json | 4 +- apps/api/src/index.ts | 10 +- apps/api/test/security.test.ts | 4 +- package-lock.json | 850 +++++++++++++++++---------------- 6 files changed, 482 insertions(+), 425 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2594142e..50e789d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,12 +147,25 @@ jobs: - name: Checkout uses: actions/checkout@v6 - # Issue #42: the runtime stage must mirror the lockfile's nested - # workspace layout (apps/api/node_modules) or the API cannot resolve - # express. Build the image on every PR so regressions ship nowhere. + # The runtime stage copies the pruned workspace dependency closure from + # the resolver's root. Build on every PR so hoisting or closure drift + # cannot ship unnoticed. - name: Build image run: docker build -t dockermap:ci . + - name: Assert runtime dependency boundary + run: | + set -euo pipefail + docker run --rm --entrypoint sh dockermap:ci -ec ' + ! command -v npm + ! command -v npx + test ! -e /opt/dockermap/node_modules/.bin/tsx + test ! -e /opt/dockermap/node_modules/.bin/vite + test ! -d /opt/dockermap/node_modules/typescript + test ! -e /opt/dockermap/node_modules/@playwright/test/package.json + node -e "import(\"express\").then(() => import(\"@dockermap/contracts\"))" + ' + - name: Smoke-test runtime image run: | set -euo pipefail diff --git a/Dockerfile b/Dockerfile index 330b2e5c..395ddb3e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,6 +44,13 @@ RUN npm run check:version && npm run check:contracts && npm run build # Build and assert the entire package artifact, rather than relying on a # source-tree module that happened to be copied into the image. RUN test -f packages/contracts/dist/index.js && test -f packages/contracts/dist/nodeSchemas.js +# The runtime image needs the API's production dependency closure only. Prune +# after all builders have finished, so compiler/test tooling never crosses the +# runtime boundary. npm's workspace resolver may hoist that closure to the +# repository root, which is the only node_modules tree copied below. +RUN npm prune --omit=dev \ + && test ! -e node_modules/.bin/tsx && test ! -e node_modules/.bin/vite \ + && test ! -d node_modules/typescript && test ! -e node_modules/@playwright/test/package.json # ---- Runtime image ---------------------------------------------------------- FROM node:22-bookworm-slim AS runtime @@ -62,10 +69,6 @@ RUN groupadd --gid 10003 dockermap && \ WORKDIR /opt/dockermap COPY --from=js-builder /src/node_modules ./node_modules -# npm nests workspace deps in the lockfile layout (apps/api/node_modules/express -# etc.); the runtime image must mirror that layout or the API cannot resolve -# its deps. -COPY --from=js-builder /src/apps/api/node_modules ./apps/api/node_modules COPY --from=js-builder /src/package.json ./package.json COPY --from=js-builder /src/apps/api/dist ./apps/api/dist COPY --from=js-builder /src/apps/api/package.json ./apps/api/package.json @@ -81,6 +84,15 @@ COPY deploy/docker/entrypoint.sh /entrypoint.sh COPY deploy/docker/frontend-entrypoint.sh /frontend-entrypoint.sh COPY deploy/docker/healthcheck.sh /usr/local/bin/dockermap-healthcheck RUN chmod +x /entrypoint.sh /frontend-entrypoint.sh /usr/local/bin/dockermap-healthcheck +# The Node base image includes package-manager CLIs that DockerMap never uses +# at runtime. Remove them after staging the already-pruned closure; `node` +# remains available for the compiled API, while npm/npx cannot become an +# in-container mutation surface. +RUN rm -rf /usr/local/lib/node_modules/npm \ + && rm -f /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack \ + && ! command -v npm && ! command -v npx \ + && test ! -e node_modules/.bin/tsx && test ! -e node_modules/.bin/vite \ + && test ! -d node_modules/typescript && test ! -e node_modules/@playwright/test/package.json ENV NODE_ENV=production \ PORT=4000 \ diff --git a/apps/api/package.json b/apps/api/package.json index 3528a8d4..b3229c81 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -14,13 +14,13 @@ "@dockermap/contracts": "0.1.0", "ajv": "^8.20.0", "cors": "^2.8.5", - "express": "^4.21.2", + "express": "^5.2.1", "helmet": "^8.2.0" }, "devDependencies": { "@seriousme/openapi-schema-validator": "^2.9.1", "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", + "@types/express": "^5.0.6", "tsx": "^4.20.5", "typescript": "^5.9.2" } diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 54123454..fbfbde8a 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -715,7 +715,13 @@ type ExpressLayer = { }; export function registeredRoutes(appInstance: express.Express): RegisteredRoute[] { - const router = appInstance as express.Express & { _router?: { stack?: ExpressLayer[] } }; + // Express 5 exposes the live router at `router`; Express 4 used the + // underscored `_router` property. This inspection is test/startup-policy + // evidence only: runtime request routing remains wholly Express-owned. + const router = appInstance as express.Express & { + router?: { stack?: ExpressLayer[] }; + _router?: { stack?: ExpressLayer[] }; + }; const routes: RegisteredRoute[] = []; const unknownLayers: string[] = []; const walk = (stack: readonly ExpressLayer[]) => { @@ -736,7 +742,7 @@ export function registeredRoutes(appInstance: express.Express): RegisteredRoute[ } } }; - walk(router._router?.stack ?? []); + walk(router.router?.stack ?? router._router?.stack ?? []); if (unknownLayers.length) throw new Error(`Unknown Express layer(s): ${unknownLayers.join(", ")}`); return routes; } diff --git a/apps/api/test/security.test.ts b/apps/api/test/security.test.ts index b6e2edae..d534ea71 100644 --- a/apps/api/test/security.test.ts +++ b/apps/api/test/security.test.ts @@ -462,7 +462,7 @@ test("route manifest completeness rejects every untracked response-capable layer 'app.use("/api/outside-path", (_req, res) => res.status(204).end());', 'app.use((_req, res, next) => process.env.DOCKERMAP_TEST_CONDITION === "respond" ? res.status(204).end() : next());', 'const router = express.Router(); router.get("/outside-mounted", (_req, res) => res.status(204).end()); app.use("/api", router);', - 'app.use("/api/outside-preauth", (_req, res) => res.status(204).end()); const planted = app._router.stack.pop(); app._router.stack.splice(app._router.stack.findIndex((layer) => layer.handle?.name === "limitSessionAttempts"), 0, planted);' + 'app.use("/api/outside-preauth", (_req, res) => res.status(204).end()); const stack = (app.router ?? app._router).stack; const planted = stack.pop(); stack.splice(stack.findIndex((layer) => layer.handle?.name === "limitSessionAttempts"), 0, planted);' ]; for (const mutation of mutations) { const result = await inspectLiveRoutes(mutation); @@ -2409,7 +2409,7 @@ test("SSE error payloads and invalid log service names cannot reflect hostile in async function inspectLiveRoutes(mutation = "") { const port = await freePort(); const script = ` - import express from "./apps/api/node_modules/express/lib/express.js"; + import express from "express"; import { app, registeredRoutes } from "./apps/api/src/index.ts"; import { assertRouteManifestComplete } from "./apps/api/src/routes.ts"; ${mutation} diff --git a/package-lock.json b/package-lock.json index 1332cf00..0fd26ab2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,342 +26,17 @@ "@dockermap/contracts": "0.1.0", "ajv": "^8.20.0", "cors": "^2.8.5", - "express": "^4.21.2", + "express": "^5.2.1", "helmet": "^8.2.0" }, "devDependencies": { "@seriousme/openapi-schema-validator": "^2.9.1", "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", + "@types/express": "^5.0.6", "tsx": "^4.20.5", "typescript": "^5.9.2" } }, - "apps/api/node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" - } - }, - "apps/api/node_modules/@types/express-serve-static-core": { - "version": "4.19.9", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", - "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "apps/api/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "apps/api/node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "apps/api/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "apps/api/node_modules/body-parser": { - "version": "1.20.6", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", - "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.15.1", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "apps/api/node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "apps/api/node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "apps/api/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "apps/api/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "apps/api/node_modules/express": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", - "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.5", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.15.1", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "apps/api/node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "apps/api/node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "apps/api/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "apps/api/node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "apps/api/node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "apps/api/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "apps/api/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "apps/api/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "apps/api/node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "license": "MIT" - }, - "apps/api/node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "apps/api/node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "apps/api/node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "apps/api/node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, "apps/web": { "name": "@dockermap/web", "version": "0.1.0", @@ -1985,6 +1660,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -2006,13 +1706,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { "version": "25.5.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", @@ -2024,9 +1717,9 @@ } }, "node_modules/@types/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", "dev": true, "license": "MIT" }, @@ -2057,6 +1750,27 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, "node_modules/@vitejs/plugin-react": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", @@ -2191,6 +1905,19 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -2247,12 +1974,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2296,6 +2017,43 @@ "require-from-string": "^2.0.2" } }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/browserslist": { "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", @@ -2399,6 +2157,19 @@ "node": ">=18" } }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/content-type": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", @@ -2424,6 +2195,15 @@ "node": ">= 0.6" } }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -2495,7 +2275,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2525,16 +2304,6 @@ "node": ">= 0.8" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2620,9 +2389,9 @@ "license": "MIT" }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2718,6 +2487,49 @@ "node": ">=12.0.0" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2758,6 +2570,27 @@ } } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2767,6 +2600,15 @@ "node": ">= 0.6" } }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2863,9 +2705,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -2919,6 +2761,22 @@ "url": "https://opencollective.com/express" } }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -2964,6 +2822,12 @@ "dev": true, "license": "MIT" }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3405,25 +3269,54 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", - "bin": { - "mime": "cli.js" + "dependencies": { + "mime-db": "^1.54.0" }, "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/minimist": { @@ -3461,6 +3354,35 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/node-releases": { "version": "2.0.47", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", @@ -3518,6 +3440,15 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/parse5": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", @@ -3540,6 +3471,16 @@ "node": ">= 0.8" } }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -3683,12 +3624,13 @@ } }, "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -3706,6 +3648,21 @@ "node": ">= 0.6" } }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -3787,25 +3744,21 @@ "dev": true, "license": "MIT" }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } }, "node_modules/safer-buffer": { "version": "2.1.2", @@ -3842,6 +3795,51 @@ "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", @@ -3855,14 +3853,14 @@ "license": "ISC" }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -3874,13 +3872,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -4099,6 +4097,37 @@ "fsevents": "~2.3.3" } }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -4170,15 +4199,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -4421,6 +4441,12 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",