From 2d95229e1ba29a6c804d53968b5269de0d3c30a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:33:45 +0000 Subject: [PATCH 1/9] Initial plan From 20ed5b79eba17250fc6119fff34919517aa2d37f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:49:27 +0000 Subject: [PATCH 2/9] feat: implement Phase 1 - CLI proxy sidecar with mcpg DIFC proxy - Add containers/cli-proxy/ with Dockerfile, server.js, entrypoint.sh, healthcheck.sh, package.json, and server.test.js (49 unit tests) - Add CLI_PROXY_PORT constant and enableCliProxy/cliProxyWritable/ cliProxyPolicy/githubToken config fields to src/types.ts - Add cli-proxy service definition in src/docker-manager.ts: - IP 172.30.0.50 on awf-net - Passes GH_TOKEN to mcpg DIFC proxy; never exposed to agent - AWF_CLI_PROXY_URL set in agent environment - GITHUB_TOKEN/GH_TOKEN excluded from agent when cli-proxy enabled - AWF_CLI_PROXY_IP propagated to iptables-init - Log preservation in cleanup - Add --enable-cli-proxy, --cli-proxy-writable, --cli-proxy-policy flags to src/cli.ts; add --enable-cli-proxy to predownload subcommand - Add AWF_CLI_PROXY_IP iptables RETURN rule in setup-iptables.sh - Add gh-cli-proxy-wrapper.sh to agent container; activated at runtime via AWF_CLI_PROXY_URL in both chroot and non-chroot modes - Add 18 new docker-manager tests for CLI proxy service; update existing container removal test to include awf-cli-proxy Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/b5f7487a-4993-4737-a01e-8f091a8e84c7 --- containers/agent/Dockerfile | 6 +- containers/agent/entrypoint.sh | 32 + containers/agent/gh-cli-proxy-wrapper.sh | 57 + containers/agent/setup-iptables.sh | 7 + containers/cli-proxy/Dockerfile | 55 + containers/cli-proxy/entrypoint.sh | 80 + containers/cli-proxy/healthcheck.sh | 4 + containers/cli-proxy/package-lock.json | 4374 ++++++++++++++++++++++ containers/cli-proxy/package.json | 16 + containers/cli-proxy/server.js | 335 ++ containers/cli-proxy/server.test.js | 260 ++ src/cli.ts | 36 +- src/commands/predownload.ts | 6 + src/docker-manager.test.ts | 148 +- src/docker-manager.ts | 150 +- src/types.ts | 70 + 16 files changed, 5629 insertions(+), 7 deletions(-) create mode 100644 containers/agent/gh-cli-proxy-wrapper.sh create mode 100644 containers/cli-proxy/Dockerfile create mode 100644 containers/cli-proxy/entrypoint.sh create mode 100644 containers/cli-proxy/healthcheck.sh create mode 100644 containers/cli-proxy/package-lock.json create mode 100644 containers/cli-proxy/package.json create mode 100644 containers/cli-proxy/server.js create mode 100644 containers/cli-proxy/server.test.js diff --git a/containers/agent/Dockerfile b/containers/agent/Dockerfile index e41de634d..8b6ab2251 100644 --- a/containers/agent/Dockerfile +++ b/containers/agent/Dockerfile @@ -75,13 +75,15 @@ RUN if ! getent group awfuser >/dev/null 2>&1; then \ mkdir -p /home/awfuser/.copilot/logs && \ chown -R awfuser:awfuser /home/awfuser -# Copy iptables setup script, PID logger, API proxy health check, and Claude key helper +# Copy iptables setup script, PID logger, API proxy health check, Claude key helper, +# and gh CLI proxy wrapper (used when --enable-cli-proxy is active) COPY setup-iptables.sh /usr/local/bin/setup-iptables.sh COPY entrypoint.sh /usr/local/bin/entrypoint.sh COPY pid-logger.sh /usr/local/bin/pid-logger.sh COPY api-proxy-health-check.sh /usr/local/bin/api-proxy-health-check.sh COPY get-claude-key.sh /usr/local/bin/get-claude-key.sh -RUN chmod +x /usr/local/bin/setup-iptables.sh /usr/local/bin/entrypoint.sh /usr/local/bin/pid-logger.sh /usr/local/bin/api-proxy-health-check.sh /usr/local/bin/get-claude-key.sh +COPY gh-cli-proxy-wrapper.sh /usr/local/bin/gh-cli-proxy-wrapper.sh +RUN chmod +x /usr/local/bin/setup-iptables.sh /usr/local/bin/entrypoint.sh /usr/local/bin/pid-logger.sh /usr/local/bin/api-proxy-health-check.sh /usr/local/bin/get-claude-key.sh /usr/local/bin/gh-cli-proxy-wrapper.sh # Copy pre-built one-shot-token library from rust-builder stage # This prevents tokens from being read multiple times (e.g., by malicious code) diff --git a/containers/agent/entrypoint.sh b/containers/agent/entrypoint.sh index 8787d786b..abd6d19b4 100644 --- a/containers/agent/entrypoint.sh +++ b/containers/agent/entrypoint.sh @@ -478,6 +478,23 @@ if [ "${AWF_CHROOT_ENABLED}" = "true" ]; then fi fi + # Activate gh CLI proxy wrapper when CLI proxy sidecar is enabled. + # The wrapper at /usr/local/bin/gh-cli-proxy-wrapper.sh (baked into the image) + # is copied to /tmp/awf-lib/gh so it is accessible inside the chroot at a + # location that takes precedence over the host's /usr/bin/gh mount. + if [ -n "$AWF_CLI_PROXY_URL" ] && [ -f /usr/local/bin/gh-cli-proxy-wrapper.sh ]; then + if mkdir -p /host/tmp/awf-lib 2>/dev/null; then + if cp /usr/local/bin/gh-cli-proxy-wrapper.sh /host/tmp/awf-lib/gh 2>/dev/null && \ + chmod +x /host/tmp/awf-lib/gh 2>/dev/null; then + echo "[entrypoint] gh CLI proxy wrapper installed at /tmp/awf-lib/gh" + # Prepend /tmp/awf-lib to PATH so the wrapper takes precedence over host gh + export AWF_HOST_PATH="/tmp/awf-lib:${AWF_HOST_PATH:-$PATH}" + else + echo "[entrypoint][WARN] Could not install gh CLI proxy wrapper" + fi + fi + fi + # Copy AWF CA certificate to chroot-accessible path for ssl-bump TLS trust. # NODE_EXTRA_CA_CERTS points to /usr/local/share/ca-certificates/awf-ca.crt which # is a Docker volume mount on the container's overlay filesystem. After chroot /host, @@ -782,6 +799,21 @@ AWFEOF else # Original behavior - run in container filesystem # Drop capabilities and privileges, then execute the user command + + # Activate gh CLI proxy wrapper in non-chroot mode. + # Copy the wrapper to /tmp/awf-lib/gh so it takes precedence over + # the system gh at /usr/bin/gh (since /tmp/awf-lib is prepended to PATH). + if [ -n "$AWF_CLI_PROXY_URL" ] && [ -f /usr/local/bin/gh-cli-proxy-wrapper.sh ]; then + mkdir -p /tmp/awf-lib + if cp /usr/local/bin/gh-cli-proxy-wrapper.sh /tmp/awf-lib/gh 2>/dev/null && \ + chmod +x /tmp/awf-lib/gh 2>/dev/null; then + export PATH="/tmp/awf-lib:${PATH}" + echo "[entrypoint] gh CLI proxy wrapper installed at /tmp/awf-lib/gh" + else + echo "[entrypoint][WARN] Could not install gh CLI proxy wrapper" + fi + fi + # This prevents malicious code from modifying iptables rules or using chroot # Security note: capsh --drop removes capabilities from the bounding set, # preventing any process (even if it escalates to root) from acquiring them diff --git a/containers/agent/gh-cli-proxy-wrapper.sh b/containers/agent/gh-cli-proxy-wrapper.sh new file mode 100644 index 000000000..400f7a141 --- /dev/null +++ b/containers/agent/gh-cli-proxy-wrapper.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# /usr/local/bin/gh-cli-proxy-wrapper +# Forwards gh CLI invocations to the CLI proxy sidecar over HTTP. +# This wrapper is installed at /usr/local/bin/gh in the agent container +# when --enable-cli-proxy is active, so it takes precedence over any +# host-mounted gh binary at /host/usr/bin/gh. +# +# Dependencies: curl, jq (both available in the agent container) +set -e + +CLI_PROXY="${AWF_CLI_PROXY_URL:-http://172.30.0.50:11000}" + +# Build JSON array from all positional arguments +ARGS_JSON='[]' +if [ $# -gt 0 ]; then + ARGS_JSON=$(printf '%s\n' "$@" | jq -R . | jq -s .) +fi + +# Capture working directory +CWD=$(pwd) + +# Read stdin if data is available (non-interactive) +STDIN_DATA="" +if [ ! -t 0 ]; then + STDIN_DATA=$(cat | base64 | tr -d '\n') +fi + +# Send the request to the CLI proxy +RESPONSE=$(curl -sf \ + --max-time 60 \ + -X POST "${CLI_PROXY}/exec" \ + -H "Content-Type: application/json" \ + --data-binary "$(printf '{"args":%s,"cwd":%s,"stdin":"%s"}' \ + "$ARGS_JSON" \ + "$(printf '%s' "$CWD" | jq -Rs .)" \ + "$STDIN_DATA")") + +if [ $? -ne 0 ] || [ -z "$RESPONSE" ]; then + echo "gh: CLI proxy unavailable at ${CLI_PROXY}" >&2 + exit 1 +fi + +# Extract and emit stdout/stderr +STDOUT=$(printf '%s' "$RESPONSE" | jq -r '.stdout // empty' 2>/dev/null) +STDERR=$(printf '%s' "$RESPONSE" | jq -r '.stderr // empty' 2>/dev/null) +EXIT_CODE=$(printf '%s' "$RESPONSE" | jq -r '.exitCode // 1' 2>/dev/null) + +# Check for error response (403 blocked, 404, 500) +ERROR=$(printf '%s' "$RESPONSE" | jq -r '.error // empty' 2>/dev/null) +if [ -n "$ERROR" ]; then + echo "gh: ${ERROR}" >&2 + exit 1 +fi + +printf '%s' "$STDOUT" +printf '%s' "$STDERR" >&2 +exit "${EXIT_CODE:-1}" diff --git a/containers/agent/setup-iptables.sh b/containers/agent/setup-iptables.sh index 28fb0d49b..3fa0f5689 100644 --- a/containers/agent/setup-iptables.sh +++ b/containers/agent/setup-iptables.sh @@ -173,6 +173,13 @@ if [ -n "$AWF_API_PROXY_IP" ]; then iptables -t nat -A OUTPUT -d "$AWF_API_PROXY_IP" -j RETURN fi +# Allow traffic to CLI proxy sidecar (when enabled) +# AWF_CLI_PROXY_IP is set by docker-manager.ts when --enable-cli-proxy is used +if [ -n "$AWF_CLI_PROXY_IP" ]; then + echo "[iptables] Allow traffic to CLI proxy sidecar (${AWF_CLI_PROXY_IP})..." + iptables -t nat -A OUTPUT -d "$AWF_CLI_PROXY_IP" -j RETURN +fi + # Validate port specification (single port 1-65535 or range N-M) # Rejects leading zeros (e.g., 080) to align with TypeScript isValidPortSpec() is_valid_port_spec() { diff --git a/containers/cli-proxy/Dockerfile b/containers/cli-proxy/Dockerfile new file mode 100644 index 000000000..8235f9a7b --- /dev/null +++ b/containers/cli-proxy/Dockerfile @@ -0,0 +1,55 @@ +# CLI Proxy sidecar for AWF - provides gh CLI access with mcpg DIFC proxy +# +# Multi-stage build: +# Stage 1: Extract mcpg binary from ghcr.io/github/gh-aw-mcpg image +# Stage 2: Assemble final image with gh CLI, Node.js, and mcpg +# +# This container runs two processes: +# 1. mcpg proxy (TLS, port 18443) - holds GH_TOKEN, enforces guard policies +# 2. HTTP server (port 11000) - receives gh invocations from the agent container + +# Stage 1: Extract the mcpg binary from the official gh-aw-mcpg image +FROM ghcr.io/github/gh-aw-mcpg:latest AS mcpg-source + +# Stage 2: Build the CLI proxy image +FROM node:22-alpine + +# Install gh CLI and curl for healthchecks/wrapper +# gh CLI is available in the Alpine community repository +RUN apk add --no-cache \ + curl \ + github-cli \ + ca-certificates \ + bash + +# Copy the mcpg binary from the mcpg-source stage +COPY --from=mcpg-source /usr/local/bin/mcpg /usr/local/bin/mcpg +RUN chmod +x /usr/local/bin/mcpg + +# Create app directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies from lockfile (deterministic) +RUN npm ci --omit=dev + +# Copy application files +COPY server.js ./ +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +COPY healthcheck.sh /usr/local/bin/healthcheck.sh + +RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/healthcheck.sh + +# Create log directory with open permissions so Node server (non-root) can write +RUN mkdir -p /var/log/cli-proxy/mcpg && chmod 777 /var/log/cli-proxy /var/log/cli-proxy/mcpg + +# Create non-root user +RUN addgroup -S cliproxy && adduser -S cliproxy -G cliproxy + +# Expose port for agent→cli-proxy HTTP communication +# 11000 - gh exec endpoint and health check +EXPOSE 11000 + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/containers/cli-proxy/entrypoint.sh b/containers/cli-proxy/entrypoint.sh new file mode 100644 index 000000000..107e8856f --- /dev/null +++ b/containers/cli-proxy/entrypoint.sh @@ -0,0 +1,80 @@ +#!/bin/bash +# CLI Proxy sidecar entrypoint +# Starts the mcpg DIFC proxy (if GH_TOKEN is set), then starts the Node.js HTTP server. +set -e + +echo "[cli-proxy] Starting CLI proxy sidecar..." + +MCPG_PID="" + +# Start mcpg proxy if GH_TOKEN is available +if [ -n "$GH_TOKEN" ]; then + echo "[cli-proxy] GH_TOKEN present - starting mcpg DIFC proxy..." + + mkdir -p /tmp/proxy-tls /var/log/cli-proxy/mcpg + + # Build the guard policy JSON if not explicitly provided + if [ -z "$AWF_GH_GUARD_POLICY" ]; then + if [ -n "$GITHUB_REPOSITORY" ]; then + AWF_GH_GUARD_POLICY="{\"repos\":[\"${GITHUB_REPOSITORY}\"],\"min-integrity\":\"public\"}" + else + AWF_GH_GUARD_POLICY="{\"min-integrity\":\"public\"}" + fi + echo "[cli-proxy] Using default guard policy: ${AWF_GH_GUARD_POLICY}" + else + echo "[cli-proxy] Using provided guard policy" + fi + + # Start mcpg proxy in background + # mcpg proxy holds GH_TOKEN and applies DIFC guard policies before forwarding + mcpg proxy \ + --policy "${AWF_GH_GUARD_POLICY}" \ + --listen 127.0.0.1:18443 \ + --tls \ + --tls-dir /tmp/proxy-tls \ + --guards-mode filter \ + --log-dir /var/log/cli-proxy/mcpg & + MCPG_PID=$! + echo "[cli-proxy] mcpg proxy started (PID: ${MCPG_PID})" + + # Wait for TLS cert to be generated (max 30s) + echo "[cli-proxy] Waiting for mcpg TLS certificate..." + i=0 + while [ $i -lt 30 ]; do + if [ -f /tmp/proxy-tls/ca.crt ]; then + echo "[cli-proxy] TLS certificate available" + break + fi + sleep 1 + i=$((i + 1)) + done + + if [ ! -f /tmp/proxy-tls/ca.crt ]; then + echo "[cli-proxy] ERROR: mcpg TLS certificate not generated within 30s" + kill "$MCPG_PID" 2>/dev/null || true + exit 1 + fi + + # Configure gh CLI to route through the mcpg proxy (TLS, self-signed CA) + export GH_HOST="localhost:18443" + export NODE_EXTRA_CA_CERTS="/tmp/proxy-tls/ca.crt" + export GH_REPO="${GH_REPO:-$GITHUB_REPOSITORY}" + + echo "[cli-proxy] gh CLI configured to route through mcpg proxy at ${GH_HOST}" +else + echo "[cli-proxy] WARNING: GH_TOKEN not set - mcpg proxy disabled, gh CLI will not authenticate" +fi + +# Cleanup handler: kill mcpg when the server exits or receives a signal +cleanup() { + echo "[cli-proxy] Shutting down..." + if [ -n "$MCPG_PID" ]; then + kill "$MCPG_PID" 2>/dev/null || true + fi + exit 0 +} +trap cleanup INT TERM + +# Start the Node.js HTTP server (foreground) +echo "[cli-proxy] Starting HTTP server on port 11000..." +exec node /app/server.js diff --git a/containers/cli-proxy/healthcheck.sh b/containers/cli-proxy/healthcheck.sh new file mode 100644 index 000000000..2f4e135d1 --- /dev/null +++ b/containers/cli-proxy/healthcheck.sh @@ -0,0 +1,4 @@ +#!/bin/sh +# Healthcheck for the CLI proxy sidecar +# Verifies the HTTP server is responsive +curl -sf --max-time 3 http://localhost:11000/health > /dev/null diff --git a/containers/cli-proxy/package-lock.json b/containers/cli-proxy/package-lock.json new file mode 100644 index 000000000..4b9e97d66 --- /dev/null +++ b/containers/cli-proxy/package-lock.json @@ -0,0 +1,4374 @@ +{ + "name": "awf-cli-proxy", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "awf-cli-proxy", + "version": "1.0.0", + "devDependencies": { + "jest": "^30.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", + "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz", + "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.3.0", + "jest-config": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-resolve-dependencies": "30.3.0", + "jest-runner": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "jest-watcher": "30.3.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.3.0", + "jest-snapshot": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", + "@types/node": "*", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", + "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/types": "30.3.0", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz", + "integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", + "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", + "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/types": "30.3.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz", + "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", + "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.0.tgz", + "integrity": "sha512-m2xozxSfCIxjDdvbhIWazlP2i2aha/iUmbl94alpsIbd3iLTfeXgfBVbwyWogB6l++istyGZqamgA/EcqYf+Bg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", + "integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.3.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz", + "integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz", + "integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", + "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001786", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001786.tgz", + "integrity": "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "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" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.331", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", + "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz", + "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.3.0", + "@jest/types": "30.3.0", + "import-local": "^3.2.0", + "jest-cli": "30.3.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.3.0.tgz", + "integrity": "sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.3.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.3.0.tgz", + "integrity": "sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "p-limit": "^3.1.0", + "pretty-format": "30.3.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.3.0.tgz", + "integrity": "sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz", + "integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.3.0", + "@jest/types": "30.3.0", + "babel-jest": "30.3.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.3.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-runner": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "parse-json": "^5.2.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.3.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.3.0.tgz", + "integrity": "sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "jest-util": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.3.0.tgz", + "integrity": "sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", + "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "picomatch": "^4.0.3", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz", + "integrity": "sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.3.0.tgz", + "integrity": "sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.3.0.tgz", + "integrity": "sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz", + "integrity": "sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/environment": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-leak-detector": "30.3.0", + "jest-message-util": "30.3.0", + "jest-resolve": "30.3.0", + "jest-runtime": "30.3.0", + "jest-util": "30.3.0", + "jest-watcher": "30.3.0", + "jest-worker": "30.3.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.3.0.tgz", + "integrity": "sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/globals": "30.3.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", + "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "pretty-format": "30.3.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", + "integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz", + "integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.3.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.3.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=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==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/containers/cli-proxy/package.json b/containers/cli-proxy/package.json new file mode 100644 index 000000000..c542eadef --- /dev/null +++ b/containers/cli-proxy/package.json @@ -0,0 +1,16 @@ +{ + "name": "awf-cli-proxy", + "version": "1.0.0", + "description": "CLI proxy sidecar for AWF - forwards gh CLI invocations through mcpg DIFC proxy", + "main": "server.js", + "scripts": { + "start": "node server.js", + "test": "jest --verbose --ci" + }, + "devDependencies": { + "jest": "^30.2.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/containers/cli-proxy/server.js b/containers/cli-proxy/server.js new file mode 100644 index 000000000..4cf63721c --- /dev/null +++ b/containers/cli-proxy/server.js @@ -0,0 +1,335 @@ +'use strict'; +/** + * CLI Proxy HTTP server + * + * Listens on port 11000 and provides two endpoints: + * GET /health - Health check (returns 200 JSON) + * POST /exec - Execute a gh CLI command and return stdout/stderr/exitCode + * + * Security: + * - Subcommand allowlist enforced (read-only mode by default) + * - Args are exec'd directly via execFile (no shell, no injection) + * - Per-command timeout (default 30s) + * - Max output size limit to prevent memory exhaustion + * + * The gh CLI running inside this container has GH_HOST set to the mcpg proxy + * (localhost:18443), so it never sees GH_TOKEN directly. + */ + +const http = require('http'); +const { execFile } = require('child_process'); + +const CLI_PROXY_PORT = parseInt(process.env.AWF_CLI_PROXY_PORT || '11000', 10); +const COMMAND_TIMEOUT_MS = parseInt(process.env.AWF_CLI_PROXY_TIMEOUT_MS || '30000', 10); +const MAX_OUTPUT_BYTES = parseInt(process.env.AWF_CLI_PROXY_MAX_OUTPUT_BYTES || String(10 * 1024 * 1024), 10); + +// When AWF_CLI_PROXY_WRITABLE=true, allow write operations +const WRITABLE_MODE = process.env.AWF_CLI_PROXY_WRITABLE === 'true'; + +/** + * Subcommands allowed in read-only mode. + * These commands only retrieve data and do not modify any GitHub resources. + */ +const ALLOWED_SUBCOMMANDS_READONLY = new Set([ + 'api', + 'browse', + 'cache', + 'codespace', + 'gist', + 'issue', + 'label', + 'org', + 'pr', + 'release', + 'repo', + 'run', + 'search', + 'secret', + 'variable', + 'workflow', +]); + +/** + * Actions that are blocked within their parent subcommand in read-only mode. + * Maps subcommand -> Set of blocked action verbs. + */ +const BLOCKED_ACTIONS_READONLY = new Map([ + ['gist', new Set(['create', 'delete', 'edit'])], + ['issue', new Set(['create', 'close', 'delete', 'edit', 'lock', 'pin', 'reopen', 'transfer', 'unpin'])], + ['label', new Set(['create', 'delete', 'edit'])], + ['pr', new Set(['checkout', 'close', 'create', 'edit', 'lock', 'merge', 'ready', 'reopen', 'review', 'update-branch'])], + ['release', new Set(['create', 'delete', 'delete-asset', 'edit', 'upload'])], + ['repo', new Set(['archive', 'create', 'delete', 'edit', 'fork', 'rename', 'set-default', 'sync', 'unarchive'])], + ['run', new Set(['cancel', 'delete', 'download', 'rerun'])], + ['secret', new Set(['delete', 'set'])], + ['variable', new Set(['delete', 'set'])], + ['workflow', new Set(['disable', 'enable', 'run'])], +]); + +/** + * Meta-commands that are always denied, even in write mode. + * These modify gh itself rather than GitHub resources. + */ +const ALWAYS_DENIED_SUBCOMMANDS = new Set([ + 'auth', + 'config', + 'extension', +]); + +/** + * Validates the gh CLI arguments against the subcommand allowlist. + * + * @param {string[]} args - The argument array (excluding 'gh' itself) + * @param {boolean} writable - Whether write operations are permitted + * @returns {{ valid: boolean, error?: string }} + */ +function validateArgs(args, writable) { + if (!Array.isArray(args)) { + return { valid: false, error: 'args must be an array' }; + } + + for (const arg of args) { + if (typeof arg !== 'string') { + return { valid: false, error: 'All args must be strings' }; + } + } + + // Find the subcommand by scanning through args, skipping flags and their values. + // Handles patterns like: gh --repo owner/repo pr list + // Strategy: when we see --flag (without =), assume the next non-flag-like arg is its value. + let subcommand = null; + let i = 0; + while (i < args.length) { + const arg = args[i]; + if (arg.startsWith('-')) { + if (!arg.includes('=') && i + 1 < args.length && !args[i + 1].startsWith('-')) { + // Flag with a separate value (e.g., --repo owner/repo): skip both + i += 2; + } else { + // Boolean flag or --flag=value form: skip just the flag + i += 1; + } + } else { + subcommand = arg; + break; + } + } + + // No subcommand means flags-only invocation (e.g., --version, --help) — allow + if (!subcommand) { + return { valid: true }; + } + + // Always deny meta-commands + if (ALWAYS_DENIED_SUBCOMMANDS.has(subcommand)) { + return { valid: false, error: `Subcommand '${subcommand}' is not permitted` }; + } + + if (!writable) { + // Read-only mode: check allowlist + if (!ALLOWED_SUBCOMMANDS_READONLY.has(subcommand)) { + return { valid: false, error: `Subcommand '${subcommand}' is not allowed in read-only mode. Enable write mode with --cli-proxy-writable.` }; + } + + // Check action-level blocklist + const blockedActions = BLOCKED_ACTIONS_READONLY.get(subcommand); + if (blockedActions) { + // The action is the first non-flag argument after the subcommand + const subcommandIndex = args.indexOf(subcommand); + const action = args.slice(subcommandIndex + 1).find(a => !a.startsWith('-')); + if (action && blockedActions.has(action)) { + return { + valid: false, + error: `Action '${subcommand} ${action}' is not allowed in read-only mode. Enable write mode with --cli-proxy-writable.`, + }; + } + } + } + + return { valid: true }; +} + +/** + * Read the full request body as a Buffer. + * + * @param {import('http').IncomingMessage} req + * @returns {Promise} + */ +function readBody(req) { + return new Promise((resolve, reject) => { + const chunks = []; + req.on('data', chunk => chunks.push(chunk)); + req.on('end', () => resolve(Buffer.concat(chunks))); + req.on('error', reject); + }); +} + +/** + * Send a JSON error response. + * + * @param {import('http').ServerResponse} res + * @param {number} statusCode + * @param {string} message + */ +function sendError(res, statusCode, message) { + const body = JSON.stringify({ error: message }); + res.writeHead(statusCode, { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body), + }); + res.end(body); +} + +/** + * Handle GET /health + */ +function handleHealth(res) { + const body = JSON.stringify({ status: 'ok', service: 'cli-proxy', writable: WRITABLE_MODE }); + res.writeHead(200, { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body), + }); + res.end(body); +} + +/** + * Handle POST /exec + * + * Expected request body (JSON): + * { + * "args": ["pr", "list", "--repo", "owner/repo", "--json", "number,title"], + * "cwd": "/home/runner/work/repo/repo", // optional + * "stdin": null, // optional, base64-encoded or null + * "env": { "GH_REPO": "owner/repo" } // optional extra env vars + * } + * + * Response body (JSON): + * { + * "stdout": "...", + * "stderr": "...", + * "exitCode": 0 + * } + */ +async function handleExec(req, res) { + let body; + try { + const raw = await readBody(req); + body = JSON.parse(raw.toString('utf8')); + } catch { + return sendError(res, 400, 'Invalid JSON body'); + } + + const { args, cwd, stdin, env: extraEnv } = body; + + // Validate args + const validation = validateArgs(args, WRITABLE_MODE); + if (!validation.valid) { + return sendError(res, 403, validation.error); + } + + // Build environment for the subprocess + // Inherit server environment (includes GH_HOST, NODE_EXTRA_CA_CERTS, GH_REPO, etc.) + const childEnv = Object.assign({}, process.env); + if (extraEnv && typeof extraEnv === 'object') { + // Only allow safe string env overrides; never allow overriding GH_HOST or GH_TOKEN + const PROTECTED_KEYS = new Set(['GH_HOST', 'GH_TOKEN', 'GITHUB_TOKEN', 'NODE_EXTRA_CA_CERTS']); + for (const [key, value] of Object.entries(extraEnv)) { + if (typeof key === 'string' && typeof value === 'string' && !PROTECTED_KEYS.has(key)) { + childEnv[key] = value; + } + } + } + + // Execute gh directly (no shell — prevents injection attacks) + let stdout = ''; + let stderr = ''; + let exitCode = 0; + + try { + const result = await new Promise((resolve, reject) => { + const child = execFile('gh', args, { + cwd: cwd || process.cwd(), + env: childEnv, + timeout: COMMAND_TIMEOUT_MS, + maxBuffer: MAX_OUTPUT_BYTES, + encoding: 'utf8', + }, (err, childStdout, childStderr) => { + if (err && err.code === undefined && err.signal) { + // Killed by timeout or signal + reject(err); + return; + } + resolve({ + stdout: childStdout || '', + stderr: childStderr || '', + exitCode: err ? (err.code || 1) : 0, + }); + }); + + // Feed stdin if provided (base64-encoded) + if (stdin) { + try { + const stdinBuf = Buffer.from(stdin, 'base64'); + child.stdin.write(stdinBuf); + } catch { + // Ignore stdin errors + } + } + if (child.stdin) { + child.stdin.end(); + } + }); + + stdout = result.stdout; + stderr = result.stderr; + exitCode = result.exitCode; + } catch (err) { + stderr = err.message || String(err); + exitCode = 1; + } + + const responseBody = JSON.stringify({ stdout, stderr, exitCode }); + res.writeHead(200, { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(responseBody), + }); + res.end(responseBody); +} + +/** + * Main HTTP request handler. + */ +async function requestHandler(req, res) { + if (req.method === 'GET' && req.url === '/health') { + return handleHealth(res); + } + + if (req.method === 'POST' && req.url === '/exec') { + return handleExec(req, res); + } + + return sendError(res, 404, `Not found: ${req.method} ${req.url}`); +} + +// Only start the server when run directly (not when imported for testing) +if (require.main === module) { + const server = http.createServer((req, res) => { + requestHandler(req, res).catch(err => { + console.error('[cli-proxy] Unhandled request error:', err); + if (!res.headersSent) { + sendError(res, 500, 'Internal server error'); + } + }); + }); + + server.listen(CLI_PROXY_PORT, '0.0.0.0', () => { + console.log(`[cli-proxy] HTTP server listening on port ${CLI_PROXY_PORT} (writable=${WRITABLE_MODE})`); + }); + + server.on('error', err => { + console.error('[cli-proxy] Server error:', err); + process.exit(1); + }); +} + +module.exports = { validateArgs, ALLOWED_SUBCOMMANDS_READONLY, BLOCKED_ACTIONS_READONLY, ALWAYS_DENIED_SUBCOMMANDS }; diff --git a/containers/cli-proxy/server.test.js b/containers/cli-proxy/server.test.js new file mode 100644 index 000000000..f5649c38d --- /dev/null +++ b/containers/cli-proxy/server.test.js @@ -0,0 +1,260 @@ +'use strict'; +/** + * Tests for cli-proxy server.js + */ + +const { validateArgs, ALLOWED_SUBCOMMANDS_READONLY, BLOCKED_ACTIONS_READONLY, ALWAYS_DENIED_SUBCOMMANDS } = require('./server'); + +describe('validateArgs', () => { + describe('input validation', () => { + it('should reject non-array args', () => { + const result = validateArgs('pr list', false); + expect(result.valid).toBe(false); + expect(result.error).toContain('array'); + }); + + it('should reject args with non-string elements', () => { + const result = validateArgs(['pr', 42], false); + expect(result.valid).toBe(false); + expect(result.error).toContain('strings'); + }); + + it('should allow empty args array', () => { + const result = validateArgs([], false); + expect(result.valid).toBe(true); + }); + + it('should allow flags-only args (e.g. --version)', () => { + const result = validateArgs(['--version'], false); + expect(result.valid).toBe(true); + }); + + it('should allow --help flag', () => { + const result = validateArgs(['--help'], false); + expect(result.valid).toBe(true); + }); + }); + + describe('always-denied subcommands', () => { + for (const cmd of ALWAYS_DENIED_SUBCOMMANDS) { + it(`should deny '${cmd}' even in writable mode`, () => { + const result = validateArgs([cmd], true); + expect(result.valid).toBe(false); + expect(result.error).toContain(cmd); + }); + + it(`should deny '${cmd}' in read-only mode`, () => { + const result = validateArgs([cmd], false); + expect(result.valid).toBe(false); + }); + } + }); + + describe('read-only mode', () => { + it('should allow all subcommands in the allowlist', () => { + for (const cmd of ALLOWED_SUBCOMMANDS_READONLY) { + // Use 'list' as the action (safe for all) + const result = validateArgs([cmd, 'list'], false); + expect(result.valid).toBe(true); + } + }); + + it('should deny unknown subcommands', () => { + const result = validateArgs(['unknown-subcommand'], false); + expect(result.valid).toBe(false); + expect(result.error).toContain('read-only mode'); + }); + + it('should deny pr create', () => { + const result = validateArgs(['pr', 'create', '--title', 'My PR'], false); + expect(result.valid).toBe(false); + expect(result.error).toContain('pr create'); + }); + + it('should deny pr merge', () => { + const result = validateArgs(['pr', 'merge', '42'], false); + expect(result.valid).toBe(false); + }); + + it('should allow pr list', () => { + const result = validateArgs(['pr', 'list', '--json', 'number,title'], false); + expect(result.valid).toBe(true); + }); + + it('should allow pr view', () => { + const result = validateArgs(['pr', 'view', '42'], false); + expect(result.valid).toBe(true); + }); + + it('should deny issue create', () => { + const result = validateArgs(['issue', 'create', '--title', 'Bug'], false); + expect(result.valid).toBe(false); + }); + + it('should allow issue list', () => { + const result = validateArgs(['issue', 'list'], false); + expect(result.valid).toBe(true); + }); + + it('should allow issue view', () => { + const result = validateArgs(['issue', 'view', '1'], false); + expect(result.valid).toBe(true); + }); + + it('should deny repo create', () => { + const result = validateArgs(['repo', 'create'], false); + expect(result.valid).toBe(false); + }); + + it('should allow repo view', () => { + const result = validateArgs(['repo', 'view', 'owner/repo'], false); + expect(result.valid).toBe(true); + }); + + it('should allow api (raw API calls)', () => { + const result = validateArgs(['api', 'repos/owner/repo'], false); + expect(result.valid).toBe(true); + }); + + it('should allow search', () => { + const result = validateArgs(['search', 'issues', '--query', 'bug'], false); + expect(result.valid).toBe(true); + }); + + it('should allow workflow list', () => { + const result = validateArgs(['workflow', 'list'], false); + expect(result.valid).toBe(true); + }); + + it('should deny workflow run', () => { + const result = validateArgs(['workflow', 'run', 'ci.yml'], false); + expect(result.valid).toBe(false); + }); + + it('should deny workflow enable', () => { + const result = validateArgs(['workflow', 'enable', 'ci.yml'], false); + expect(result.valid).toBe(false); + }); + + it('should deny secret set', () => { + const result = validateArgs(['secret', 'set', 'MY_SECRET'], false); + expect(result.valid).toBe(false); + }); + + it('should allow secret list', () => { + const result = validateArgs(['secret', 'list'], false); + expect(result.valid).toBe(true); + }); + + it('should allow run list', () => { + const result = validateArgs(['run', 'list'], false); + expect(result.valid).toBe(true); + }); + + it('should deny run cancel', () => { + const result = validateArgs(['run', 'cancel', '123'], false); + expect(result.valid).toBe(false); + }); + + it('should allow release list', () => { + const result = validateArgs(['release', 'list'], false); + expect(result.valid).toBe(true); + }); + + it('should deny release create', () => { + const result = validateArgs(['release', 'create', 'v1.0.0'], false); + expect(result.valid).toBe(false); + }); + + it('should deny gist create', () => { + const result = validateArgs(['gist', 'create', 'file.txt'], false); + expect(result.valid).toBe(false); + }); + + it('should allow gist view', () => { + const result = validateArgs(['gist', 'view', 'abc123'], false); + expect(result.valid).toBe(true); + }); + + it('should handle flags before subcommand gracefully', () => { + // e.g.: gh --repo owner/repo pr list + const result = validateArgs(['--repo', 'owner/repo', 'pr', 'list'], false); + expect(result.valid).toBe(true); + }); + + it('should handle flags before action gracefully', () => { + // e.g.: gh pr --json number list + const result = validateArgs(['pr', '--json', 'number', 'list'], false); + expect(result.valid).toBe(true); + }); + }); + + describe('writable mode', () => { + it('should allow pr create in writable mode', () => { + const result = validateArgs(['pr', 'create', '--title', 'My PR'], true); + expect(result.valid).toBe(true); + }); + + it('should allow issue create in writable mode', () => { + const result = validateArgs(['issue', 'create', '--title', 'Bug'], true); + expect(result.valid).toBe(true); + }); + + it('should allow repo create in writable mode', () => { + const result = validateArgs(['repo', 'create', 'new-repo'], true); + expect(result.valid).toBe(true); + }); + + it('should allow secret set in writable mode', () => { + const result = validateArgs(['secret', 'set', 'MY_SECRET'], true); + expect(result.valid).toBe(true); + }); + + it('should still deny auth in writable mode', () => { + const result = validateArgs(['auth', 'login'], true); + expect(result.valid).toBe(false); + }); + + it('should still deny config in writable mode', () => { + const result = validateArgs(['config', 'set', 'editor', 'vim'], true); + expect(result.valid).toBe(false); + }); + + it('should still deny extension in writable mode', () => { + const result = validateArgs(['extension', 'install', 'owner/ext'], true); + expect(result.valid).toBe(false); + }); + + it('should allow all read-only subcommands in writable mode', () => { + for (const cmd of ALLOWED_SUBCOMMANDS_READONLY) { + const result = validateArgs([cmd, 'list'], true); + expect(result.valid).toBe(true); + } + }); + + it('should allow previously blocked actions in writable mode', () => { + for (const [subcommand, blockedActions] of BLOCKED_ACTIONS_READONLY) { + for (const action of blockedActions) { + const result = validateArgs([subcommand, action], true); + expect(result.valid).toBe(true); + } + } + }); + }); + + describe('allowlist completeness', () => { + it('should have ALLOWED_SUBCOMMANDS_READONLY as a non-empty Set', () => { + expect(ALLOWED_SUBCOMMANDS_READONLY.size).toBeGreaterThan(0); + }); + + it('should have ALWAYS_DENIED_SUBCOMMANDS as a non-empty Set', () => { + expect(ALWAYS_DENIED_SUBCOMMANDS.size).toBeGreaterThan(0); + }); + + it('should have no overlap between ALLOWED_SUBCOMMANDS_READONLY and ALWAYS_DENIED_SUBCOMMANDS', () => { + for (const cmd of ALWAYS_DENIED_SUBCOMMANDS) { + expect(ALLOWED_SUBCOMMANDS_READONLY.has(cmd)).toBe(false); + } + }); + }); +}); diff --git a/src/cli.ts b/src/cli.ts index eb6fd494f..eff56681e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1415,7 +1415,24 @@ program 'Disable rate limiting in the API proxy (requires --enable-api-proxy)', ) - // -- Logging & Debug -- + // -- CLI Proxy -- + .option( + '--enable-cli-proxy', + 'Enable gh CLI proxy sidecar for secure GitHub CLI access.\n' + + ' Routes gh commands through mcpg DIFC proxy with guard policies.\n' + + ' GH_TOKEN is held in the sidecar; never exposed to the agent.', + false + ) + .option( + '--cli-proxy-writable', + 'Allow write operations through the CLI proxy (default: read-only)', + false + ) + .option( + '--cli-proxy-policy ', + 'Guard policy JSON for the mcpg DIFC proxy inside the CLI proxy sidecar\n' + + ' (e.g. \'{"repos":["owner/repo"],"min-integrity":"public"}\')', + ) .option( '--log-level ', 'Log level: debug, info, warn, error', @@ -1786,6 +1803,10 @@ program anthropicApiBasePath: options.anthropicApiBasePath || process.env.ANTHROPIC_API_BASE_PATH, geminiApiTarget: options.geminiApiTarget || process.env.GEMINI_API_TARGET, geminiApiBasePath: options.geminiApiBasePath || process.env.GEMINI_API_BASE_PATH, + enableCliProxy: options.enableCliProxy, + cliProxyWritable: options.cliProxyWritable, + cliProxyPolicy: options.cliProxyPolicy, + githubToken: process.env.GITHUB_TOKEN, }; // Parse and validate --agent-timeout @@ -1882,6 +1903,16 @@ program // Warn if custom API targets are not in --allow-domains emitApiProxyTargetWarnings(config, allowedDomains, logger.warn.bind(logger)); + // Log CLI proxy status + if (config.enableCliProxy) { + if (config.githubToken) { + logger.info(`CLI proxy enabled: GH_TOKEN present, writable=${!!config.cliProxyWritable}`); + } else { + logger.warn('⚠️ CLI proxy enabled but GITHUB_TOKEN not found in environment'); + logger.warn(' Set GITHUB_TOKEN to enable authenticated gh CLI access through the proxy'); + } + } + // Log config with redacted secrets - remove API keys entirely // to prevent sensitive data from flowing to logger (CodeQL sensitive data logging) const redactedConfig: Record = {}; @@ -2005,6 +2036,7 @@ export async function handlePredownloadAction(options: { imageTag: string; agentImage: string; enableApiProxy: boolean; + enableCliProxy?: boolean; }): Promise { const { predownloadCommand } = await import('./commands/predownload'); try { @@ -2013,6 +2045,7 @@ export async function handlePredownloadAction(options: { imageTag: options.imageTag, agentImage: options.agentImage, enableApiProxy: options.enableApiProxy, + enableCliProxy: options.enableCliProxy, }); } catch (error) { const exitCode = (error as Error & { exitCode?: number }).exitCode ?? 1; @@ -2036,6 +2069,7 @@ program 'default' ) .option('--enable-api-proxy', 'Also download the API proxy image', false) + .option('--enable-cli-proxy', 'Also download the CLI proxy image', false) .action(handlePredownloadAction); // Logs subcommand - view Squid proxy logs diff --git a/src/commands/predownload.ts b/src/commands/predownload.ts index d79ffa6c3..ffdeea03d 100644 --- a/src/commands/predownload.ts +++ b/src/commands/predownload.ts @@ -6,6 +6,7 @@ export interface PredownloadOptions { imageTag: string; agentImage: string; enableApiProxy: boolean; + enableCliProxy?: boolean; } /** @@ -47,6 +48,11 @@ export function resolveImages(options: PredownloadOptions): string[] { images.push(`${imageRegistry}/api-proxy:${imageTag}`); } + // Optionally pull cli-proxy + if (options.enableCliProxy) { + images.push(`${imageRegistry}/cli-proxy:${imageTag}`); + } + return images; } diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index a38001cef..af2cc5563 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -2632,6 +2632,152 @@ describe('docker-manager', () => { expect(result.services['doh-proxy']).toBeUndefined(); }); }); + + describe('CLI proxy sidecar', () => { + const mockNetworkConfigWithCliProxy = { + ...mockNetworkConfig, + cliProxyIp: '172.30.0.50', + }; + + it('should not include cli-proxy service when enableCliProxy is false', () => { + const result = generateDockerCompose(mockConfig, mockNetworkConfigWithCliProxy); + expect(result.services['cli-proxy']).toBeUndefined(); + }); + + it('should not include cli-proxy service when enableCliProxy is true but no cliProxyIp', () => { + const configWithCliProxy = { ...mockConfig, enableCliProxy: true, githubToken: 'ghp_test_token' }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfig); + expect(result.services['cli-proxy']).toBeUndefined(); + }); + + it('should include cli-proxy service when enableCliProxy is true with cliProxyIp', () => { + const configWithCliProxy = { ...mockConfig, enableCliProxy: true, githubToken: 'ghp_test_token' }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfigWithCliProxy); + expect(result.services['cli-proxy']).toBeDefined(); + const proxy = result.services['cli-proxy']; + expect(proxy.container_name).toBe('awf-cli-proxy'); + expect((proxy.networks as any)['awf-net'].ipv4_address).toBe('172.30.0.50'); + }); + + it('should pass GH_TOKEN to cli-proxy environment', () => { + const configWithCliProxy = { ...mockConfig, enableCliProxy: true, githubToken: 'ghp_test_token' }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfigWithCliProxy); + const proxy = result.services['cli-proxy']; + const env = proxy.environment as Record; + expect(env.GH_TOKEN).toBe('ghp_test_token'); + }); + + it('should route cli-proxy traffic through Squid', () => { + const configWithCliProxy = { ...mockConfig, enableCliProxy: true, githubToken: 'ghp_test_token' }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfigWithCliProxy); + const proxy = result.services['cli-proxy']; + const env = proxy.environment as Record; + expect(env.HTTP_PROXY).toContain('172.30.0.10:3128'); + expect(env.HTTPS_PROXY).toContain('172.30.0.10:3128'); + }); + + it('should configure healthcheck for cli-proxy', () => { + const configWithCliProxy = { ...mockConfig, enableCliProxy: true, githubToken: 'ghp_test_token' }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfigWithCliProxy); + const proxy = result.services['cli-proxy']; + expect(proxy.healthcheck).toBeDefined(); + expect((proxy.healthcheck as any).test).toEqual(['CMD', 'curl', '-f', 'http://localhost:11000/health']); + }); + + it('should drop all capabilities from cli-proxy', () => { + const configWithCliProxy = { ...mockConfig, enableCliProxy: true, githubToken: 'ghp_test_token' }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfigWithCliProxy); + const proxy = result.services['cli-proxy']; + expect(proxy.cap_drop).toEqual(['ALL']); + expect(proxy.security_opt).toContain('no-new-privileges:true'); + }); + + it('should update agent depends_on to wait for cli-proxy', () => { + const configWithCliProxy = { ...mockConfig, enableCliProxy: true, githubToken: 'ghp_test_token' }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfigWithCliProxy); + const dependsOn = result.services['agent'].depends_on as Record; + expect(dependsOn['cli-proxy']).toBeDefined(); + expect(dependsOn['cli-proxy'].condition).toBe('service_healthy'); + }); + + it('should set AWF_CLI_PROXY_URL in agent environment', () => { + const configWithCliProxy = { ...mockConfig, enableCliProxy: true, githubToken: 'ghp_test_token' }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfigWithCliProxy); + const agent = result.services['agent']; + const env = agent.environment as Record; + expect(env.AWF_CLI_PROXY_URL).toBe('http://172.30.0.50:11000'); + }); + + it('should set AWF_CLI_PROXY_IP in agent environment', () => { + const configWithCliProxy = { ...mockConfig, enableCliProxy: true, githubToken: 'ghp_test_token' }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfigWithCliProxy); + const agent = result.services['agent']; + const env = agent.environment as Record; + expect(env.AWF_CLI_PROXY_IP).toBe('172.30.0.50'); + }); + + it('should pass AWF_CLI_PROXY_IP to iptables-init environment', () => { + const configWithCliProxy = { ...mockConfig, enableCliProxy: true, githubToken: 'ghp_test_token' }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfigWithCliProxy); + const initEnv = result.services['iptables-init'].environment as Record; + expect(initEnv.AWF_CLI_PROXY_IP).toBe('172.30.0.50'); + }); + + it('should set AWF_CLI_PROXY_WRITABLE=false by default', () => { + const configWithCliProxy = { ...mockConfig, enableCliProxy: true, githubToken: 'ghp_test_token' }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfigWithCliProxy); + const proxy = result.services['cli-proxy']; + const env = proxy.environment as Record; + expect(env.AWF_CLI_PROXY_WRITABLE).toBe('false'); + }); + + it('should set AWF_CLI_PROXY_WRITABLE=true when cliProxyWritable is true', () => { + const configWithCliProxy = { ...mockConfig, enableCliProxy: true, cliProxyWritable: true, githubToken: 'ghp_test_token' }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfigWithCliProxy); + const proxy = result.services['cli-proxy']; + const env = proxy.environment as Record; + expect(env.AWF_CLI_PROXY_WRITABLE).toBe('true'); + }); + + it('should pass guard policy JSON when cliProxyPolicy is set', () => { + const policy = '{"repos":["owner/repo"],"min-integrity":"public"}'; + const configWithCliProxy = { ...mockConfig, enableCliProxy: true, githubToken: 'ghp_test_token', cliProxyPolicy: policy }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfigWithCliProxy); + const proxy = result.services['cli-proxy']; + const env = proxy.environment as Record; + expect(env.AWF_GH_GUARD_POLICY).toBe(policy); + }); + + it('should not set AWF_GH_GUARD_POLICY when cliProxyPolicy is not set', () => { + const configWithCliProxy = { ...mockConfig, enableCliProxy: true, githubToken: 'ghp_test_token' }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfigWithCliProxy); + const proxy = result.services['cli-proxy']; + const env = proxy.environment as Record; + expect(env.AWF_GH_GUARD_POLICY).toBeUndefined(); + }); + + it('should use GHCR image by default', () => { + const configWithCliProxy = { ...mockConfig, enableCliProxy: true, githubToken: 'ghp_test_token', buildLocal: false }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfigWithCliProxy); + const proxy = result.services['cli-proxy']; + expect(proxy.image).toContain('cli-proxy'); + expect(proxy.build).toBeUndefined(); + }); + + it('should use local build when buildLocal is true', () => { + const configWithCliProxy = { ...mockConfig, enableCliProxy: true, githubToken: 'ghp_test_token', buildLocal: true }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfigWithCliProxy); + const proxy = result.services['cli-proxy']; + expect((proxy.build as any).context).toContain('containers/cli-proxy'); + expect(proxy.image).toBeUndefined(); + }); + + it('should not include cli-proxy when dohProxyIp is missing from networkConfig', () => { + const configWithCliProxy = { ...mockConfig, enableCliProxy: true, githubToken: 'ghp_test_token' }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfig); + expect(result.services['cli-proxy']).toBeUndefined(); + }); + }); }); describe('writeConfigs', () => { @@ -2956,7 +3102,7 @@ describe('docker-manager', () => { expect(mockExecaFn).toHaveBeenCalledWith( 'docker', - ['rm', '-f', 'awf-squid', 'awf-agent', 'awf-iptables-init', 'awf-api-proxy'], + ['rm', '-f', 'awf-squid', 'awf-agent', 'awf-iptables-init', 'awf-api-proxy', 'awf-cli-proxy'], { reject: false } ); }); diff --git a/src/docker-manager.ts b/src/docker-manager.ts index feb64bd92..043196298 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import * as os from 'os'; import * as yaml from 'js-yaml'; import execa from 'execa'; -import { DockerComposeConfig, WrapperConfig, BlockedTarget, API_PROXY_PORTS, API_PROXY_HEALTH_PORT } from './types'; +import { DockerComposeConfig, WrapperConfig, BlockedTarget, API_PROXY_PORTS, API_PROXY_HEALTH_PORT, CLI_PROXY_PORT } from './types'; import { logger } from './logger'; import { generateSquidConfig, generatePolicyManifest } from './squid-config'; import { generateSessionCa, initSslDb, CaFiles, parseUrlPatterns, cleanupSslKeyMaterial, unmountSslTmpfs } from './ssl-bump'; @@ -21,6 +21,7 @@ const SQUID_CONTAINER_NAME = 'awf-squid'; const IPTABLES_INIT_CONTAINER_NAME = 'awf-iptables-init'; const API_PROXY_CONTAINER_NAME = 'awf-api-proxy'; const DOH_PROXY_CONTAINER_NAME = 'awf-doh-proxy'; +const CLI_PROXY_CONTAINER_NAME = 'awf-cli-proxy'; /** * Flag set by fastKillAgentContainer() to signal runAgentCommand() that @@ -376,7 +377,7 @@ export interface SslConfig { */ export function generateDockerCompose( config: WrapperConfig, - networkConfig: { subnet: string; squidIp: string; agentIp: string; proxyIp?: string; dohProxyIp?: string }, + networkConfig: { subnet: string; squidIp: string; agentIp: string; proxyIp?: string; dohProxyIp?: string; cliProxyIp?: string }, sslConfig?: SslConfig, squidConfigContent?: string ): DockerComposeConfig { @@ -416,6 +417,11 @@ export function generateDockerCompose( ? path.join(config.proxyLogsDir, 'api-proxy-logs') : path.join(config.workDir, 'api-proxy-logs'); + // CLI proxy logs path: write to workDir/cli-proxy-logs (will be moved to /tmp after cleanup) + const cliProxyLogsPath = config.proxyLogsDir + ? path.join(config.proxyLogsDir, 'cli-proxy-logs') + : path.join(config.workDir, 'cli-proxy-logs'); + // Build Squid volumes list // Note: squid.conf is NOT bind-mounted. Instead, it's passed as a base64-encoded // environment variable (AWF_SQUID_CONFIG_B64) and decoded by the entrypoint override. @@ -538,6 +544,13 @@ export function generateDockerCompose( // See: github/gh-aw#20875 } + // When cli-proxy is enabled, exclude GitHub tokens from agent environment + // (they are held securely in the cli-proxy sidecar's mcpg process instead) + if (config.enableCliProxy) { + EXCLUDED_ENV_VARS.add('GITHUB_TOKEN'); + EXCLUDED_ENV_VARS.add('GH_TOKEN'); + } + // Start with required/overridden environment variables // Use the real user's home (not /root when running with sudo) const homeDir = getRealUserHome(); @@ -1343,6 +1356,12 @@ export function generateDockerCompose( environment.AWF_API_PROXY_IP = networkConfig.proxyIp; } + // Pre-set CLI proxy IP in environment before the init container definition + // for the same reason as AWF_API_PROXY_IP above. + if (config.enableCliProxy && networkConfig.cliProxyIp) { + environment.AWF_CLI_PROXY_IP = networkConfig.cliProxyIp; + } + // SECURITY: iptables init container - sets up NAT rules in a separate container // that shares the agent's network namespace but NEVER gives NET_ADMIN to the agent. // This eliminates the window where the agent holds NET_ADMIN during startup. @@ -1368,6 +1387,7 @@ export function generateDockerCompose( AWF_HOST_SERVICE_PORTS: environment.AWF_HOST_SERVICE_PORTS || '', AWF_API_PROXY_IP: environment.AWF_API_PROXY_IP || '', AWF_DOH_PROXY_IP: environment.AWF_DOH_PROXY_IP || '', + AWF_CLI_PROXY_IP: environment.AWF_CLI_PROXY_IP || '', AWF_SSL_BUMP_ENABLED: environment.AWF_SSL_BUMP_ENABLED || '', AWF_SSL_BUMP_INTERCEPT_PORT: environment.AWF_SSL_BUMP_INTERCEPT_PORT || '', }, @@ -1595,6 +1615,94 @@ export function generateDockerCompose( logger.info(`DNS-over-HTTPS proxy sidecar enabled - DNS queries encrypted via ${config.dnsOverHttps}`); } + // Add CLI proxy sidecar if enabled + if (config.enableCliProxy && networkConfig.cliProxyIp) { + const cliProxyService: any = { + container_name: CLI_PROXY_CONTAINER_NAME, + networks: { + 'awf-net': { + ipv4_address: networkConfig.cliProxyIp, + }, + }, + volumes: [ + // Mount log directory for mcpg DIFC proxy audit logs + `${cliProxyLogsPath}:/var/log/cli-proxy:rw`, + ], + environment: { + // Pass GH_TOKEN to the mcpg DIFC proxy (never exposed to agent) + ...(config.githubToken && { GH_TOKEN: config.githubToken }), + // Pass GITHUB_REPOSITORY so the default guard policy restricts to the current repo + ...(process.env.GITHUB_REPOSITORY && { GITHUB_REPOSITORY: process.env.GITHUB_REPOSITORY }), + ...(process.env.GITHUB_SERVER_URL && { GITHUB_SERVER_URL: process.env.GITHUB_SERVER_URL }), + // Guard policy JSON for mcpg proxy (optional; default generated from GITHUB_REPOSITORY) + ...(config.cliProxyPolicy && { AWF_GH_GUARD_POLICY: config.cliProxyPolicy }), + // Enable write mode when --cli-proxy-writable is passed + AWF_CLI_PROXY_WRITABLE: String(!!config.cliProxyWritable), + // Route through Squid to respect domain whitelisting + HTTP_PROXY: `http://${networkConfig.squidIp}:${SQUID_PORT}`, + HTTPS_PROXY: `http://${networkConfig.squidIp}:${SQUID_PORT}`, + https_proxy: `http://${networkConfig.squidIp}:${SQUID_PORT}`, + // Prevent curl health check from routing localhost through Squid + NO_PROXY: `localhost,127.0.0.1,::1`, + no_proxy: `localhost,127.0.0.1,::1`, + }, + healthcheck: { + test: ['CMD', 'curl', '-f', `http://localhost:${CLI_PROXY_PORT}/health`], + interval: '5s', + timeout: '3s', + retries: 5, + start_period: '30s', // Extra time for mcpg TLS cert generation + }, + // Depend on Squid for routing outbound API traffic + depends_on: { + 'squid-proxy': { + condition: 'service_healthy', + }, + }, + // Security hardening: Drop all capabilities + cap_drop: ['ALL'], + security_opt: [ + 'no-new-privileges:true', + ], + // Resource limits to prevent DoS attacks + mem_limit: '256m', + memswap_limit: '256m', + pids_limit: 50, + cpu_shares: 256, + stop_grace_period: '2s', + }; + + // Use GHCR image or build locally + if (useGHCR) { + cliProxyService.image = `${registry}/cli-proxy:${tag}`; + } else { + cliProxyService.build = { + context: path.join(projectRoot, 'containers/cli-proxy'), + dockerfile: 'Dockerfile', + }; + } + + services['cli-proxy'] = cliProxyService; + + // Update agent dependencies to wait for cli-proxy + agentService.depends_on['cli-proxy'] = { + condition: 'service_healthy', + }; + + // Tell the agent how to reach the CLI proxy + // Use IP address instead of hostname since Docker DNS may not resolve in chroot mode + environment.AWF_CLI_PROXY_URL = `http://${networkConfig.cliProxyIp}:${CLI_PROXY_PORT}`; + environment.AWF_CLI_PROXY_IP = networkConfig.cliProxyIp; + + // Install the gh wrapper in the agent's PATH by symlinking to the pre-installed wrapper + // The agent entrypoint uses AWF_CLI_PROXY_URL to know it should activate the wrapper + logger.info('CLI proxy sidecar enabled - gh CLI will route through mcpg DIFC proxy'); + logger.info('CLI proxy will route through Squid to respect domain whitelisting'); + if (config.cliProxyWritable) { + logger.info('CLI proxy running in writable mode - write operations permitted'); + } + } + return { services, networks: { @@ -1705,6 +1813,17 @@ export async function writeConfigs(config: WrapperConfig): Promise { } logger.debug(`API proxy logs directory created at: ${apiProxyLogsDir}`); + // Create CLI proxy logs directory for persistence + // Note: CLI proxy runs as user 'cliproxy' (non-root) + const cliProxyLogsDir = config.proxyLogsDir + ? path.join(config.proxyLogsDir, 'cli-proxy-logs') + : path.join(config.workDir, 'cli-proxy-logs'); + if (!fs.existsSync(cliProxyLogsDir)) { + fs.mkdirSync(cliProxyLogsDir, { recursive: true, mode: 0o777 }); + fs.chmodSync(cliProxyLogsDir, 0o777); + } + logger.debug(`CLI proxy logs directory created at: ${cliProxyLogsDir}`); + // Create /tmp/gh-aw/mcp-logs directory // This directory exists on the HOST for MCP gateway to write logs // Inside the AWF container, it's hidden via tmpfs mount (see generateDockerCompose) @@ -1764,6 +1883,7 @@ export async function writeConfigs(config: WrapperConfig): Promise { agentIp: '172.30.0.20', proxyIp: '172.30.0.30', // Envoy API proxy sidecar dohProxyIp: '172.30.0.40', // DoH proxy sidecar + cliProxyIp: '172.30.0.50', // CLI proxy sidecar }; logger.debug(`Using network config: ${networkConfig.subnet} (squid: ${networkConfig.squidIp}, agent: ${networkConfig.agentIp}, api-proxy: ${networkConfig.proxyIp})`); @@ -1967,7 +2087,7 @@ export async function startContainers(workDir: string, allowedDomains: string[], // This handles orphaned containers from failed/interrupted previous runs logger.debug('Removing any existing containers with conflicting names...'); try { - await execa('docker', ['rm', '-f', SQUID_CONTAINER_NAME, AGENT_CONTAINER_NAME, IPTABLES_INIT_CONTAINER_NAME, API_PROXY_CONTAINER_NAME], { + await execa('docker', ['rm', '-f', SQUID_CONTAINER_NAME, AGENT_CONTAINER_NAME, IPTABLES_INIT_CONTAINER_NAME, API_PROXY_CONTAINER_NAME, CLI_PROXY_CONTAINER_NAME], { reject: false, }); } catch { @@ -2331,6 +2451,30 @@ export async function cleanup(workDir: string, keepFiles: boolean, proxyLogsDir? } } + // Preserve cli-proxy (mcpg DIFC proxy audit) logs before cleanup + if (proxyLogsDir) { + const cliProxyLogsDir = path.join(proxyLogsDir, 'cli-proxy-logs'); + if (fs.existsSync(cliProxyLogsDir)) { + try { + execa.sync('chmod', ['-R', 'a+rX', cliProxyLogsDir]); + logger.info(`CLI proxy logs available at: ${cliProxyLogsDir}`); + } catch (error) { + logger.debug('Could not fix cli-proxy log permissions:', error); + } + } + } else { + const cliProxyLogsDir = path.join(workDir, 'cli-proxy-logs'); + const cliProxyLogsDestination = path.join(os.tmpdir(), `cli-proxy-logs-${timestamp}`); + if (fs.existsSync(cliProxyLogsDir) && fs.readdirSync(cliProxyLogsDir).length > 0) { + try { + fs.renameSync(cliProxyLogsDir, cliProxyLogsDestination); + logger.info(`CLI proxy logs preserved at: ${cliProxyLogsDestination}`); + } catch (error) { + logger.debug('Could not preserve cli-proxy logs:', error); + } + } + } + // Handle squid logs if (proxyLogsDir) { // Logs were written directly to proxyLogsDir during runtime (timeout-safe) diff --git a/src/types.ts b/src/types.ts index 78c16ae77..4d289fa1b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -53,6 +53,20 @@ export const API_PROXY_PORTS = { */ export const API_PROXY_HEALTH_PORT = API_PROXY_PORTS.OPENAI; +/** + * Port for the CLI proxy sidecar HTTP server. + * + * The CLI proxy sidecar listens on this port for gh CLI invocations forwarded + * from the agent container. Port 11000 is chosen to avoid collision with the + * api-proxy ports (10000-10004). + * + * All ports must be allowed in: + * - containers/cli-proxy/Dockerfile (EXPOSE directive) + * - containers/agent/setup-iptables.sh (NAT rules) + * @see containers/cli-proxy/server.js + */ +export const CLI_PROXY_PORT = 11000; + /** * Main configuration interface for the firewall wrapper * @@ -769,6 +783,62 @@ export interface WrapperConfig { */ geminiApiBasePath?: string; + /** + * Enable CLI proxy sidecar for secure gh CLI access + * + * When true, deploys a CLI proxy sidecar container that: + * - Holds GH_TOKEN securely (never exposed to the agent) + * - Routes gh CLI invocations through an mcpg DIFC proxy + * - Enforces guard policies (min-integrity, repo restrictions) + * - Generates audit logs via mcpg's JSONL output + * + * The agent container gets a /usr/local/bin/gh wrapper script that + * forwards invocations to the CLI proxy sidecar at http://172.30.0.50:11000. + * + * @default false + * @example + * ```bash + * export GITHUB_TOKEN="ghp_..." + * awf --enable-cli-proxy --allow-domains api.github.com,github.com -- command + * ``` + */ + enableCliProxy?: boolean; + + /** + * Allow write operations through the CLI proxy sidecar + * + * When true, the CLI proxy allows write operations (pr create, issue create, etc.) + * in addition to read-only operations. + * When false (default), only read-only subcommands and actions are permitted. + * + * @default false + */ + cliProxyWritable?: boolean; + + /** + * Guard policy JSON for the mcpg DIFC proxy inside the CLI proxy sidecar + * + * This JSON string is passed to the mcpg proxy's --policy flag to enforce + * DIFC guard policies (repository restrictions, minimum integrity level). + * If not specified, a default policy is generated based on GITHUB_REPOSITORY. + * + * @example '{"repos":["owner/repo"],"min-integrity":"public"}' + */ + cliProxyPolicy?: string; + + /** + * GitHub token for the CLI proxy sidecar + * + * When enableCliProxy is true, this token is injected into the CLI proxy + * container and passed to the mcpg DIFC proxy. The token is never exposed + * to the agent container directly. + * + * Read from GITHUB_TOKEN environment variable when not specified. + * + * @default undefined + */ + githubToken?: string; + /** * Enable Data Loss Prevention (DLP) scanning * From e02f2fb9dc072a066de52d543f46d04c24146c25 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:52:20 +0000 Subject: [PATCH 3/9] fix: address code review and CodeQL feedback - Fix test name: 'should not include cli-proxy when cliProxyIp is missing' - Fix gh-cli-proxy-wrapper.sh: capture curl exit code in CURL_EXIT variable before checking it (avoids $? being overwritten by shell); remove set -e - Fix entrypoint.sh: clarify log message that /tmp/awf-lib/gh is the path inside the chroot (not the host-prefixed /host/tmp/awf-lib/gh path) - Fix server.js: sanitize error message in catch block to avoid stack trace exposure (CodeQL js/stack-trace-exposure) Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/b5f7487a-4993-4737-a01e-8f091a8e84c7 --- containers/agent/entrypoint.sh | 3 ++- containers/agent/gh-cli-proxy-wrapper.sh | 6 ++++-- containers/cli-proxy/server.js | 4 +++- src/docker-manager.test.ts | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/containers/agent/entrypoint.sh b/containers/agent/entrypoint.sh index abd6d19b4..83ea642cb 100644 --- a/containers/agent/entrypoint.sh +++ b/containers/agent/entrypoint.sh @@ -486,7 +486,8 @@ if [ "${AWF_CHROOT_ENABLED}" = "true" ]; then if mkdir -p /host/tmp/awf-lib 2>/dev/null; then if cp /usr/local/bin/gh-cli-proxy-wrapper.sh /host/tmp/awf-lib/gh 2>/dev/null && \ chmod +x /host/tmp/awf-lib/gh 2>/dev/null; then - echo "[entrypoint] gh CLI proxy wrapper installed at /tmp/awf-lib/gh" + # The chroot will see this as /tmp/awf-lib/gh (the /host prefix is the bind mount) + echo "[entrypoint] gh CLI proxy wrapper installed at /tmp/awf-lib/gh (inside chroot)" # Prepend /tmp/awf-lib to PATH so the wrapper takes precedence over host gh export AWF_HOST_PATH="/tmp/awf-lib:${AWF_HOST_PATH:-$PATH}" else diff --git a/containers/agent/gh-cli-proxy-wrapper.sh b/containers/agent/gh-cli-proxy-wrapper.sh index 400f7a141..0b6123543 100644 --- a/containers/agent/gh-cli-proxy-wrapper.sh +++ b/containers/agent/gh-cli-proxy-wrapper.sh @@ -6,7 +6,6 @@ # host-mounted gh binary at /host/usr/bin/gh. # # Dependencies: curl, jq (both available in the agent container) -set -e CLI_PROXY="${AWF_CLI_PROXY_URL:-http://172.30.0.50:11000}" @@ -26,6 +25,8 @@ if [ ! -t 0 ]; then fi # Send the request to the CLI proxy +# Capture curl exit code separately since $? after a $() substitution may +# be from the last command inside the subshell, not from curl itself. RESPONSE=$(curl -sf \ --max-time 60 \ -X POST "${CLI_PROXY}/exec" \ @@ -34,8 +35,9 @@ RESPONSE=$(curl -sf \ "$ARGS_JSON" \ "$(printf '%s' "$CWD" | jq -Rs .)" \ "$STDIN_DATA")") +CURL_EXIT=$? -if [ $? -ne 0 ] || [ -z "$RESPONSE" ]; then +if [ "$CURL_EXIT" -ne 0 ] || [ -z "$RESPONSE" ]; then echo "gh: CLI proxy unavailable at ${CLI_PROXY}" >&2 exit 1 fi diff --git a/containers/cli-proxy/server.js b/containers/cli-proxy/server.js index 4cf63721c..06f7d4008 100644 --- a/containers/cli-proxy/server.js +++ b/containers/cli-proxy/server.js @@ -284,7 +284,9 @@ async function handleExec(req, res) { stderr = result.stderr; exitCode = result.exitCode; } catch (err) { - stderr = err.message || String(err); + // Only expose a safe message, not a full stack trace + const errMsg = err instanceof Error ? err.message : 'Command execution failed'; + stderr = errMsg; exitCode = 1; } diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index af2cc5563..959eee0c8 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -2772,7 +2772,7 @@ describe('docker-manager', () => { expect(proxy.image).toBeUndefined(); }); - it('should not include cli-proxy when dohProxyIp is missing from networkConfig', () => { + it('should not include cli-proxy when cliProxyIp is missing from networkConfig', () => { const configWithCliProxy = { ...mockConfig, enableCliProxy: true, githubToken: 'ghp_test_token' }; const result = generateDockerCompose(configWithCliProxy, mockNetworkConfig); expect(result.services['cli-proxy']).toBeUndefined(); From 1b3964eca146657e62ba483d07bf393192b599a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:09:06 +0000 Subject: [PATCH 4/9] fix: address all review feedback on cli-proxy - Dockerfile: Add USER cliproxy so mcpg and HTTP server run as non-root; fix log and TLS dir ownership to be writable by cliproxy before USER switch - entrypoint.sh: Fail closed when GH_TOKEN is missing (exit 1) instead of starting an unenforced server; fix signal handling by running Node in the background with wait/trap so cleanup kills both Node and mcpg on SIGTERM - server.js: Add blocked write actions for codespace (create/delete/edit/stop/ ports), cache (delete), and org (invite) in BLOCKED_ACTIONS_READONLY; track subcommandIndex during the scan instead of using args.indexOf() to prevent bypass when the subcommand string appears earlier as a flag value; add MAX_REQUEST_BODY_BYTES limit in readBody (returns null + 413 on overflow) - gh-cli-proxy-wrapper.sh: Remove curl -f; capture HTTP status code with -w and response body with -o so 4xx policy errors (403) surface the actual .error message instead of "CLI proxy unavailable" - cli.ts: Read GITHUB_TOKEN || GH_TOKEN for githubToken; update warning to mention both env var names - build.yml: Add 'Run CLI proxy unit tests' step parallel to api-proxy step - server.test.js: Add 11 new tests covering cache/codespace/org blocked actions and the subcommandIndex indexOf-bypass fix (58 tests total) Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/4d1512c6-a5af-49dc-9693-2f2c1af0b173 --- .github/workflows/build.yml | 6 ++ containers/agent/gh-cli-proxy-wrapper.sh | 36 ++++--- containers/cli-proxy/Dockerfile | 13 ++- containers/cli-proxy/entrypoint.sh | 130 +++++++++++++---------- containers/cli-proxy/server.js | 51 +++++++-- containers/cli-proxy/server.test.js | 51 +++++++++ src/cli.ts | 8 +- 7 files changed, 212 insertions(+), 83 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index da3da275c..7fb447792 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,3 +64,9 @@ jobs: cd containers/api-proxy npm ci npm test + + - name: Run CLI proxy unit tests + run: | + cd containers/cli-proxy + npm ci + npm test diff --git a/containers/agent/gh-cli-proxy-wrapper.sh b/containers/agent/gh-cli-proxy-wrapper.sh index 0b6123543..3d120b719 100644 --- a/containers/agent/gh-cli-proxy-wrapper.sh +++ b/containers/agent/gh-cli-proxy-wrapper.sh @@ -24,11 +24,13 @@ if [ ! -t 0 ]; then STDIN_DATA=$(cat | base64 | tr -d '\n') fi -# Send the request to the CLI proxy -# Capture curl exit code separately since $? after a $() substitution may -# be from the last command inside the subshell, not from curl itself. -RESPONSE=$(curl -sf \ +# Use a temporary file to capture the response body without -f, +# so we can read the body even on 4xx/5xx responses (e.g., 403 policy block). +RESPONSE_FILE=$(mktemp) +HTTP_STATUS=$(curl -s \ --max-time 60 \ + -o "$RESPONSE_FILE" \ + -w "%{http_code}" \ -X POST "${CLI_PROXY}/exec" \ -H "Content-Type: application/json" \ --data-binary "$(printf '{"args":%s,"cwd":%s,"stdin":"%s"}' \ @@ -36,24 +38,30 @@ RESPONSE=$(curl -sf \ "$(printf '%s' "$CWD" | jq -Rs .)" \ "$STDIN_DATA")") CURL_EXIT=$? +RESPONSE=$(cat "$RESPONSE_FILE") +rm -f "$RESPONSE_FILE" -if [ "$CURL_EXIT" -ne 0 ] || [ -z "$RESPONSE" ]; then - echo "gh: CLI proxy unavailable at ${CLI_PROXY}" >&2 +if [ "$CURL_EXIT" -ne 0 ]; then + echo "gh: CLI proxy unavailable at ${CLI_PROXY} (curl exit ${CURL_EXIT})" >&2 exit 1 fi -# Extract and emit stdout/stderr +# Surface policy errors (403), request errors (400/413), and server errors (5xx) +if [ "$HTTP_STATUS" != "200" ]; then + ERROR=$(printf '%s' "$RESPONSE" | jq -r '.error // empty' 2>/dev/null) + if [ -n "$ERROR" ]; then + echo "gh: ${ERROR}" >&2 + else + echo "gh: CLI proxy returned HTTP ${HTTP_STATUS}" >&2 + fi + exit 1 +fi + +# Extract and emit stdout/stderr from a successful 200 response STDOUT=$(printf '%s' "$RESPONSE" | jq -r '.stdout // empty' 2>/dev/null) STDERR=$(printf '%s' "$RESPONSE" | jq -r '.stderr // empty' 2>/dev/null) EXIT_CODE=$(printf '%s' "$RESPONSE" | jq -r '.exitCode // 1' 2>/dev/null) -# Check for error response (403 blocked, 404, 500) -ERROR=$(printf '%s' "$RESPONSE" | jq -r '.error // empty' 2>/dev/null) -if [ -n "$ERROR" ]; then - echo "gh: ${ERROR}" >&2 - exit 1 -fi - printf '%s' "$STDOUT" printf '%s' "$STDERR" >&2 exit "${EXIT_CODE:-1}" diff --git a/containers/cli-proxy/Dockerfile b/containers/cli-proxy/Dockerfile index 8235f9a7b..f5df14366 100644 --- a/containers/cli-proxy/Dockerfile +++ b/containers/cli-proxy/Dockerfile @@ -42,12 +42,19 @@ COPY healthcheck.sh /usr/local/bin/healthcheck.sh RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/healthcheck.sh -# Create log directory with open permissions so Node server (non-root) can write -RUN mkdir -p /var/log/cli-proxy/mcpg && chmod 777 /var/log/cli-proxy /var/log/cli-proxy/mcpg - # Create non-root user RUN addgroup -S cliproxy && adduser -S cliproxy -G cliproxy +# Create log directory owned by cliproxy (so non-root process can write) +RUN mkdir -p /var/log/cli-proxy/mcpg && \ + chown -R cliproxy:cliproxy /var/log/cli-proxy + +# Create /tmp/proxy-tls directory owned by cliproxy for mcpg TLS cert generation +RUN mkdir -p /tmp/proxy-tls && chown cliproxy:cliproxy /tmp/proxy-tls + +# Switch to non-root user +USER cliproxy + # Expose port for agent→cli-proxy HTTP communication # 11000 - gh exec endpoint and health check EXPOSE 11000 diff --git a/containers/cli-proxy/entrypoint.sh b/containers/cli-proxy/entrypoint.sh index 107e8856f..36fe523e6 100644 --- a/containers/cli-proxy/entrypoint.sh +++ b/containers/cli-proxy/entrypoint.sh @@ -1,80 +1,102 @@ #!/bin/bash # CLI Proxy sidecar entrypoint -# Starts the mcpg DIFC proxy (if GH_TOKEN is set), then starts the Node.js HTTP server. +# Starts the mcpg DIFC proxy (GH_TOKEN required), then starts the Node.js HTTP server +# under a supervisor loop so signals are properly handled and mcpg is cleaned up. set -e echo "[cli-proxy] Starting CLI proxy sidecar..." MCPG_PID="" +NODE_PID="" -# Start mcpg proxy if GH_TOKEN is available -if [ -n "$GH_TOKEN" ]; then - echo "[cli-proxy] GH_TOKEN present - starting mcpg DIFC proxy..." +# GH_TOKEN is required: without it, mcpg cannot authenticate and DIFC guard policies +# cannot be enforced. Fail closed rather than starting an unenforced server. +if [ -z "$GH_TOKEN" ]; then + echo "[cli-proxy] ERROR: GH_TOKEN not set - refusing to start without mcpg DIFC enforcement" + exit 1 +fi + +echo "[cli-proxy] GH_TOKEN present - starting mcpg DIFC proxy..." - mkdir -p /tmp/proxy-tls /var/log/cli-proxy/mcpg +mkdir -p /tmp/proxy-tls /var/log/cli-proxy/mcpg - # Build the guard policy JSON if not explicitly provided - if [ -z "$AWF_GH_GUARD_POLICY" ]; then - if [ -n "$GITHUB_REPOSITORY" ]; then - AWF_GH_GUARD_POLICY="{\"repos\":[\"${GITHUB_REPOSITORY}\"],\"min-integrity\":\"public\"}" - else - AWF_GH_GUARD_POLICY="{\"min-integrity\":\"public\"}" - fi - echo "[cli-proxy] Using default guard policy: ${AWF_GH_GUARD_POLICY}" +# Build the guard policy JSON if not explicitly provided +if [ -z "$AWF_GH_GUARD_POLICY" ]; then + if [ -n "$GITHUB_REPOSITORY" ]; then + AWF_GH_GUARD_POLICY="{\"repos\":[\"${GITHUB_REPOSITORY}\"],\"min-integrity\":\"public\"}" else - echo "[cli-proxy] Using provided guard policy" + AWF_GH_GUARD_POLICY="{\"min-integrity\":\"public\"}" fi + echo "[cli-proxy] Using default guard policy: ${AWF_GH_GUARD_POLICY}" +else + echo "[cli-proxy] Using provided guard policy" +fi - # Start mcpg proxy in background - # mcpg proxy holds GH_TOKEN and applies DIFC guard policies before forwarding - mcpg proxy \ - --policy "${AWF_GH_GUARD_POLICY}" \ - --listen 127.0.0.1:18443 \ - --tls \ - --tls-dir /tmp/proxy-tls \ - --guards-mode filter \ - --log-dir /var/log/cli-proxy/mcpg & - MCPG_PID=$! - echo "[cli-proxy] mcpg proxy started (PID: ${MCPG_PID})" - - # Wait for TLS cert to be generated (max 30s) - echo "[cli-proxy] Waiting for mcpg TLS certificate..." - i=0 - while [ $i -lt 30 ]; do - if [ -f /tmp/proxy-tls/ca.crt ]; then - echo "[cli-proxy] TLS certificate available" - break - fi - sleep 1 - i=$((i + 1)) - done +# Start mcpg proxy in background +# mcpg proxy holds GH_TOKEN and applies DIFC guard policies before forwarding +mcpg proxy \ + --policy "${AWF_GH_GUARD_POLICY}" \ + --listen 127.0.0.1:18443 \ + --tls \ + --tls-dir /tmp/proxy-tls \ + --guards-mode filter \ + --log-dir /var/log/cli-proxy/mcpg & +MCPG_PID=$! +echo "[cli-proxy] mcpg proxy started (PID: ${MCPG_PID})" - if [ ! -f /tmp/proxy-tls/ca.crt ]; then - echo "[cli-proxy] ERROR: mcpg TLS certificate not generated within 30s" - kill "$MCPG_PID" 2>/dev/null || true - exit 1 +# Wait for TLS cert to be generated (max 30s) +echo "[cli-proxy] Waiting for mcpg TLS certificate..." +i=0 +while [ $i -lt 30 ]; do + if [ -f /tmp/proxy-tls/ca.crt ]; then + echo "[cli-proxy] TLS certificate available" + break fi + sleep 1 + i=$((i + 1)) +done - # Configure gh CLI to route through the mcpg proxy (TLS, self-signed CA) - export GH_HOST="localhost:18443" - export NODE_EXTRA_CA_CERTS="/tmp/proxy-tls/ca.crt" - export GH_REPO="${GH_REPO:-$GITHUB_REPOSITORY}" - - echo "[cli-proxy] gh CLI configured to route through mcpg proxy at ${GH_HOST}" -else - echo "[cli-proxy] WARNING: GH_TOKEN not set - mcpg proxy disabled, gh CLI will not authenticate" +if [ ! -f /tmp/proxy-tls/ca.crt ]; then + echo "[cli-proxy] ERROR: mcpg TLS certificate not generated within 30s" + kill "$MCPG_PID" 2>/dev/null || true + exit 1 fi -# Cleanup handler: kill mcpg when the server exits or receives a signal +# Configure gh CLI to route through the mcpg proxy (TLS, self-signed CA) +export GH_HOST="localhost:18443" +export NODE_EXTRA_CA_CERTS="/tmp/proxy-tls/ca.crt" +export GH_REPO="${GH_REPO:-$GITHUB_REPOSITORY}" + +echo "[cli-proxy] gh CLI configured to route through mcpg proxy at ${GH_HOST}" + +# Cleanup handler: stop both the Node HTTP server and mcpg when we receive a signal +# or when the server exits. This runs correctly because we do NOT exec Node — we +# start it in the background and wait, so the shell (and its traps) remain active. cleanup() { echo "[cli-proxy] Shutting down..." + if [ -n "$NODE_PID" ]; then + kill "$NODE_PID" 2>/dev/null || true + wait "$NODE_PID" 2>/dev/null || true + fi if [ -n "$MCPG_PID" ]; then kill "$MCPG_PID" 2>/dev/null || true + wait "$MCPG_PID" 2>/dev/null || true fi - exit 0 } -trap cleanup INT TERM +trap 'cleanup; exit 0' INT TERM -# Start the Node.js HTTP server (foreground) +# Start the Node.js HTTP server in the background so the shell keeps running +# and traps remain active for graceful shutdown. echo "[cli-proxy] Starting HTTP server on port 11000..." -exec node /app/server.js +node /app/server.js & +NODE_PID=$! + +# Wait for Node to exit and propagate its exit code +if wait "$NODE_PID"; then + NODE_EXIT=0 +else + NODE_EXIT=$? +fi + +cleanup +exit "$NODE_EXIT" diff --git a/containers/cli-proxy/server.js b/containers/cli-proxy/server.js index 06f7d4008..8091ad508 100644 --- a/containers/cli-proxy/server.js +++ b/containers/cli-proxy/server.js @@ -54,9 +54,15 @@ const ALLOWED_SUBCOMMANDS_READONLY = new Set([ * Maps subcommand -> Set of blocked action verbs. */ const BLOCKED_ACTIONS_READONLY = new Map([ + // cache: delete is a write operation + ['cache', new Set(['delete'])], + // codespace: create, delete, edit, stop, ports forward are write operations + ['codespace', new Set(['create', 'delete', 'edit', 'stop', 'ports'])], ['gist', new Set(['create', 'delete', 'edit'])], ['issue', new Set(['create', 'close', 'delete', 'edit', 'lock', 'pin', 'reopen', 'transfer', 'unpin'])], ['label', new Set(['create', 'delete', 'edit'])], + // org: invite changes org membership + ['org', new Set(['invite'])], ['pr', new Set(['checkout', 'close', 'create', 'edit', 'lock', 'merge', 'ready', 'reopen', 'review', 'update-branch'])], ['release', new Set(['create', 'delete', 'delete-asset', 'edit', 'upload'])], ['repo', new Set(['archive', 'create', 'delete', 'edit', 'fork', 'rename', 'set-default', 'sync', 'unarchive'])], @@ -97,7 +103,11 @@ function validateArgs(args, writable) { // Find the subcommand by scanning through args, skipping flags and their values. // Handles patterns like: gh --repo owner/repo pr list // Strategy: when we see --flag (without =), assume the next non-flag-like arg is its value. + // We also track the subcommand's index so that subsequent action detection doesn't + // accidentally pick up a flag value that happens to equal the subcommand string + // (e.g. gh --repo pr pr merge 1 would be wrongly parsed by indexOf). let subcommand = null; + let subcommandIndex = -1; let i = 0; while (i < args.length) { const arg = args[i]; @@ -111,6 +121,7 @@ function validateArgs(args, writable) { } } else { subcommand = arg; + subcommandIndex = i; break; } } @@ -134,8 +145,9 @@ function validateArgs(args, writable) { // Check action-level blocklist const blockedActions = BLOCKED_ACTIONS_READONLY.get(subcommand); if (blockedActions) { - // The action is the first non-flag argument after the subcommand - const subcommandIndex = args.indexOf(subcommand); + // The action is the first non-flag argument after the subcommand. + // Use the tracked subcommandIndex (not indexOf) to avoid false matches when + // the subcommand string also appears as a flag value earlier in the args array. const action = args.slice(subcommandIndex + 1).find(a => !a.startsWith('-')); if (action && blockedActions.has(action)) { return { @@ -150,16 +162,37 @@ function validateArgs(args, writable) { } /** - * Read the full request body as a Buffer. + * Maximum size for the /exec request body (1 MB). + * Prevents memory exhaustion from oversized POST bodies. + */ +const MAX_REQUEST_BODY_BYTES = parseInt(process.env.AWF_CLI_PROXY_MAX_REQUEST_BYTES || String(1024 * 1024), 10); + +/** + * Read the full request body as a Buffer, rejecting bodies over MAX_REQUEST_BODY_BYTES. * * @param {import('http').IncomingMessage} req - * @returns {Promise} + * @param {import('http').ServerResponse} res + * @returns {Promise} Buffer on success, null if size limit exceeded (response already sent) */ -function readBody(req) { +function readBody(req, res) { return new Promise((resolve, reject) => { const chunks = []; - req.on('data', chunk => chunks.push(chunk)); - req.on('end', () => resolve(Buffer.concat(chunks))); + let totalBytes = 0; + req.on('data', chunk => { + totalBytes += chunk.length; + if (totalBytes > MAX_REQUEST_BODY_BYTES) { + req.destroy(); + sendError(res, 413, `Request body exceeds maximum size of ${MAX_REQUEST_BODY_BYTES} bytes`); + resolve(null); + return; + } + chunks.push(chunk); + }); + req.on('end', () => { + if (totalBytes <= MAX_REQUEST_BODY_BYTES) { + resolve(Buffer.concat(chunks)); + } + }); req.on('error', reject); }); } @@ -213,7 +246,9 @@ function handleHealth(res) { async function handleExec(req, res) { let body; try { - const raw = await readBody(req); + const raw = await readBody(req, res); + // null means readBody already sent a 413 error response + if (raw === null) return; body = JSON.parse(raw.toString('utf8')); } catch { return sendError(res, 400, 'Invalid JSON body'); diff --git a/containers/cli-proxy/server.test.js b/containers/cli-proxy/server.test.js index f5649c38d..1772bf346 100644 --- a/containers/cli-proxy/server.test.js +++ b/containers/cli-proxy/server.test.js @@ -187,6 +187,57 @@ describe('validateArgs', () => { const result = validateArgs(['pr', '--json', 'number', 'list'], false); expect(result.valid).toBe(true); }); + + it('should deny cache delete', () => { + const result = validateArgs(['cache', 'delete', 'some-key'], false); + expect(result.valid).toBe(false); + }); + + it('should allow cache list', () => { + const result = validateArgs(['cache', 'list'], false); + expect(result.valid).toBe(true); + }); + + it('should deny codespace create', () => { + const result = validateArgs(['codespace', 'create'], false); + expect(result.valid).toBe(false); + }); + + it('should deny codespace delete', () => { + const result = validateArgs(['codespace', 'delete', '--all'], false); + expect(result.valid).toBe(false); + }); + + it('should allow codespace list', () => { + const result = validateArgs(['codespace', 'list'], false); + expect(result.valid).toBe(true); + }); + + it('should deny org invite', () => { + const result = validateArgs(['org', 'invite', '--org', 'myorg', 'user'], false); + expect(result.valid).toBe(false); + }); + + it('should allow org list', () => { + const result = validateArgs(['org', 'list'], false); + expect(result.valid).toBe(true); + }); + + it('should not bypass blocked action when subcommand appears as a flag value (indexOf bypass)', () => { + // Without the subcommandIndex fix, 'args.indexOf("pr")' would return index 1 (the flag value), + // and args.slice(2) = ['pr', 'merge', '1'], finding 'merge' as the action → blocked. + // With the correct index (3), slice(4) = ['merge', '1'] → still blocked. But if the + // subcommand were a read-only action, the old code would use the wrong index. + // Here we verify that gh --repo pr pr list is still allowed (subcommand is at index 3). + const result = validateArgs(['--repo', 'pr', 'pr', 'list'], false); + expect(result.valid).toBe(true); + }); + + it('should correctly detect blocked action even when subcommand appears earlier as flag value', () => { + // gh --repo pr pr merge 1: subcommand 'pr' is at index 2 (flag value 'pr' at index 1 is skipped) + const result = validateArgs(['--repo', 'pr', 'pr', 'merge', '1'], false); + expect(result.valid).toBe(false); + }); }); describe('writable mode', () => { diff --git a/src/cli.ts b/src/cli.ts index eff56681e..db6408fd9 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1806,7 +1806,7 @@ program enableCliProxy: options.enableCliProxy, cliProxyWritable: options.cliProxyWritable, cliProxyPolicy: options.cliProxyPolicy, - githubToken: process.env.GITHUB_TOKEN, + githubToken: process.env.GITHUB_TOKEN || process.env.GH_TOKEN, }; // Parse and validate --agent-timeout @@ -1906,10 +1906,10 @@ program // Log CLI proxy status if (config.enableCliProxy) { if (config.githubToken) { - logger.info(`CLI proxy enabled: GH_TOKEN present, writable=${!!config.cliProxyWritable}`); + logger.info(`CLI proxy enabled: token present (GITHUB_TOKEN/GH_TOKEN), writable=${!!config.cliProxyWritable}`); } else { - logger.warn('⚠️ CLI proxy enabled but GITHUB_TOKEN not found in environment'); - logger.warn(' Set GITHUB_TOKEN to enable authenticated gh CLI access through the proxy'); + logger.warn('⚠️ CLI proxy enabled but no GitHub token found in environment'); + logger.warn(' Set GITHUB_TOKEN or GH_TOKEN to enable authenticated gh CLI access through the proxy'); } } From d99431d9f6415b6388cc903555aa2f17c29e728b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:23:20 +0000 Subject: [PATCH 5/9] fix: api read-only deny, trusted-bots, pin mcpg image, comments - server.js: Remove 'api' from ALLOWED_SUBCOMMANDS_READONLY; raw HTTP passthrough can mutate via -X POST/PUT/DELETE. 'api' allowed in writable mode. Add explanatory comment. (61 tests total) - entrypoint.sh: Add --trusted-bots flag to mcpg so guard policies don't reject bot-authored content (Dependabot, Copilot, github-actions) - Dockerfile: Pin ghcr.io/github/gh-aw-mcpg :latest -> :v0.2.2 for reproducible builds - docker-manager.ts: Expand GITHUB_TOKEN exclusion comment explaining design intent and why it is safe in practice - cli.ts: Restore // -- Logging & Debug -- section header before --log-level - server.test.js: Add 3 new tests (61 total) Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/2e1f5421-3e78-44f2-a1c0-d2920f65a173 --- containers/cli-proxy/Dockerfile | 2 +- containers/cli-proxy/entrypoint.sh | 1 + containers/cli-proxy/server.js | 6 +++++- containers/cli-proxy/server.test.js | 19 +++++++++++++++++-- src/cli.ts | 1 + src/docker-manager.ts | 11 +++++++++-- 6 files changed, 34 insertions(+), 6 deletions(-) diff --git a/containers/cli-proxy/Dockerfile b/containers/cli-proxy/Dockerfile index f5df14366..318641034 100644 --- a/containers/cli-proxy/Dockerfile +++ b/containers/cli-proxy/Dockerfile @@ -9,7 +9,7 @@ # 2. HTTP server (port 11000) - receives gh invocations from the agent container # Stage 1: Extract the mcpg binary from the official gh-aw-mcpg image -FROM ghcr.io/github/gh-aw-mcpg:latest AS mcpg-source +FROM ghcr.io/github/gh-aw-mcpg:v0.2.2 AS mcpg-source # Stage 2: Build the CLI proxy image FROM node:22-alpine diff --git a/containers/cli-proxy/entrypoint.sh b/containers/cli-proxy/entrypoint.sh index 36fe523e6..1f4202422 100644 --- a/containers/cli-proxy/entrypoint.sh +++ b/containers/cli-proxy/entrypoint.sh @@ -40,6 +40,7 @@ mcpg proxy \ --tls \ --tls-dir /tmp/proxy-tls \ --guards-mode filter \ + --trusted-bots "github-actions[bot],github-actions,dependabot[bot],copilot" \ --log-dir /var/log/cli-proxy/mcpg & MCPG_PID=$! echo "[cli-proxy] mcpg proxy started (PID: ${MCPG_PID})" diff --git a/containers/cli-proxy/server.js b/containers/cli-proxy/server.js index 8091ad508..71505cbfb 100644 --- a/containers/cli-proxy/server.js +++ b/containers/cli-proxy/server.js @@ -29,9 +29,13 @@ const WRITABLE_MODE = process.env.AWF_CLI_PROXY_WRITABLE === 'true'; /** * Subcommands allowed in read-only mode. * These commands only retrieve data and do not modify any GitHub resources. + * + * Note: 'api' is intentionally excluded even in read-only mode because it is a raw + * HTTP passthrough that can perform arbitrary POST/PUT/DELETE mutations via -X/--method. + * Agents should use typed subcommands (gh issue list, gh pr view, etc.) instead. + * In writable mode, 'api' is permitted since the operator has explicitly opted in. */ const ALLOWED_SUBCOMMANDS_READONLY = new Set([ - 'api', 'browse', 'cache', 'codespace', diff --git a/containers/cli-proxy/server.test.js b/containers/cli-proxy/server.test.js index 1772bf346..64782cbf0 100644 --- a/containers/cli-proxy/server.test.js +++ b/containers/cli-proxy/server.test.js @@ -111,9 +111,14 @@ describe('validateArgs', () => { expect(result.valid).toBe(true); }); - it('should allow api (raw API calls)', () => { + it('should deny api in read-only mode (raw passthrough can mutate via -X POST)', () => { const result = validateArgs(['api', 'repos/owner/repo'], false); - expect(result.valid).toBe(true); + expect(result.valid).toBe(false); + }); + + it('should deny api POST in read-only mode', () => { + const result = validateArgs(['api', '-X', 'POST', '/repos/owner/repo/issues', '-f', 'title=Test'], false); + expect(result.valid).toBe(false); }); it('should allow search', () => { @@ -276,6 +281,16 @@ describe('validateArgs', () => { expect(result.valid).toBe(false); }); + it('should allow api in writable mode (operator opted in to writes)', () => { + const result = validateArgs(['api', 'repos/owner/repo'], true); + expect(result.valid).toBe(true); + }); + + it('should allow api POST in writable mode', () => { + const result = validateArgs(['api', '-X', 'POST', '/repos/owner/repo/issues'], true); + expect(result.valid).toBe(true); + }); + it('should allow all read-only subcommands in writable mode', () => { for (const cmd of ALLOWED_SUBCOMMANDS_READONLY) { const result = validateArgs([cmd, 'list'], true); diff --git a/src/cli.ts b/src/cli.ts index db6408fd9..b706397f0 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1433,6 +1433,7 @@ program 'Guard policy JSON for the mcpg DIFC proxy inside the CLI proxy sidecar\n' + ' (e.g. \'{"repos":["owner/repo"],"min-integrity":"public"}\')', ) + // -- Logging & Debug -- .option( '--log-level ', 'Log level: debug, info, warn, error', diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 043196298..31987c521 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -544,8 +544,15 @@ export function generateDockerCompose( // See: github/gh-aw#20875 } - // When cli-proxy is enabled, exclude GitHub tokens from agent environment - // (they are held securely in the cli-proxy sidecar's mcpg process instead) + // When cli-proxy is enabled, exclude GitHub tokens from agent environment. + // These tokens are held securely in the cli-proxy sidecar's mcpg process instead, + // so the agent can invoke gh commands without ever seeing the raw token. + // + // Design note: unlike api-proxy (which excludes LLM API keys), this excludes a + // token that many GitHub Actions tools also use. In practice this is safe because + // actions/checkout runs before awf starts, and tools that need GITHUB_TOKEN + // (e.g. gh-mcp-server) should use GITHUB_MCP_SERVER_TOKEN (a separate env var) + // rather than GITHUB_TOKEN. if (config.enableCliProxy) { EXCLUDED_ENV_VARS.add('GITHUB_TOKEN'); EXCLUDED_ENV_VARS.add('GH_TOKEN'); From 3f51a228dc8b75a39f3b65ea924ed8c9f6450f93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:33:32 +0000 Subject: [PATCH 6/9] feat: add --cli-proxy-mcpg-image for compiler-controlled mcpg version Make the gh-aw-mcpg source image configurable via CLI so the AWF compiler can control which mcpg version is pulled and run in the cli-proxy sidecar: - Dockerfile: use ARG MCPG_IMAGE (default ghcr.io/github/gh-aw-mcpg:v0.2.2) - src/types.ts: add cliProxyMcpgImage to WrapperConfig - src/cli.ts: add --cli-proxy-mcpg-image flag under CLI Proxy section - src/docker-manager.ts: pass MCPG_IMAGE build arg when building locally - src/docker-manager.test.ts: 3 new tests (320 total) Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/15ba6c64-db6c-4c47-b813-21878fb57168 --- containers/cli-proxy/Dockerfile | 8 ++++++-- src/cli.ts | 8 ++++++++ src/docker-manager.test.ts | 35 +++++++++++++++++++++++++++++++++ src/docker-manager.ts | 7 +++++++ src/types.ts | 15 ++++++++++++++ 5 files changed, 71 insertions(+), 2 deletions(-) diff --git a/containers/cli-proxy/Dockerfile b/containers/cli-proxy/Dockerfile index 318641034..bc9d8c4b1 100644 --- a/containers/cli-proxy/Dockerfile +++ b/containers/cli-proxy/Dockerfile @@ -8,8 +8,12 @@ # 1. mcpg proxy (TLS, port 18443) - holds GH_TOKEN, enforces guard policies # 2. HTTP server (port 11000) - receives gh invocations from the agent container -# Stage 1: Extract the mcpg binary from the official gh-aw-mcpg image -FROM ghcr.io/github/gh-aw-mcpg:v0.2.2 AS mcpg-source +# Stage 1: Extract the mcpg binary from the official gh-aw-mcpg image. +# MCPG_IMAGE is configurable via --cli-proxy-mcpg-image so the AWF compiler +# can control which mcpg version is pulled and run (e.g. for version pinning +# or testing a new mcpg release before it is bundled in the GHCR cli-proxy image). +ARG MCPG_IMAGE=ghcr.io/github/gh-aw-mcpg:v0.2.2 +FROM ${MCPG_IMAGE} AS mcpg-source # Stage 2: Build the CLI proxy image FROM node:22-alpine diff --git a/src/cli.ts b/src/cli.ts index b706397f0..119b31875 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1433,6 +1433,13 @@ program 'Guard policy JSON for the mcpg DIFC proxy inside the CLI proxy sidecar\n' + ' (e.g. \'{"repos":["owner/repo"],"min-integrity":"public"}\')', ) + .option( + '--cli-proxy-mcpg-image ', + 'Docker image for the mcpg DIFC proxy used inside the CLI proxy sidecar\n' + + ' (only used with --build-local; ignored when pulling pre-built GHCR images)\n' + + ' Set by the AWF compiler to control which mcpg version is pulled and run', + 'ghcr.io/github/gh-aw-mcpg:v0.2.2' + ) // -- Logging & Debug -- .option( '--log-level ', @@ -1807,6 +1814,7 @@ program enableCliProxy: options.enableCliProxy, cliProxyWritable: options.cliProxyWritable, cliProxyPolicy: options.cliProxyPolicy, + cliProxyMcpgImage: options.cliProxyMcpgImage, githubToken: process.env.GITHUB_TOKEN || process.env.GH_TOKEN, }; diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index 959eee0c8..0135e8286 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -2772,6 +2772,41 @@ describe('docker-manager', () => { expect(proxy.image).toBeUndefined(); }); + it('should not pass MCPG_IMAGE build arg when cliProxyMcpgImage is not set', () => { + const configWithCliProxy = { ...mockConfig, enableCliProxy: true, githubToken: 'ghp_test_token', buildLocal: true }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfigWithCliProxy); + const proxy = result.services['cli-proxy']; + expect((proxy.build as any).args).toBeUndefined(); + }); + + it('should pass MCPG_IMAGE build arg when cliProxyMcpgImage is set with --build-local', () => { + const configWithCliProxy = { + ...mockConfig, + enableCliProxy: true, + githubToken: 'ghp_test_token', + buildLocal: true, + cliProxyMcpgImage: 'ghcr.io/github/gh-aw-mcpg:v0.3.0', + }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfigWithCliProxy); + const proxy = result.services['cli-proxy']; + expect((proxy.build as any).args).toEqual({ MCPG_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.0' }); + }); + + it('should ignore cliProxyMcpgImage when not using --build-local (pre-built GHCR image)', () => { + const configWithCliProxy = { + ...mockConfig, + enableCliProxy: true, + githubToken: 'ghp_test_token', + buildLocal: false, + cliProxyMcpgImage: 'ghcr.io/github/gh-aw-mcpg:v0.3.0', + }; + const result = generateDockerCompose(configWithCliProxy, mockNetworkConfigWithCliProxy); + const proxy = result.services['cli-proxy']; + // Pre-built GHCR image already contains mcpg; build arg has no effect + expect(proxy.image).toContain('cli-proxy'); + expect(proxy.build).toBeUndefined(); + }); + it('should not include cli-proxy when cliProxyIp is missing from networkConfig', () => { const configWithCliProxy = { ...mockConfig, enableCliProxy: true, githubToken: 'ghp_test_token' }; const result = generateDockerCompose(configWithCliProxy, mockNetworkConfig); diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 31987c521..f62f33018 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -1683,9 +1683,16 @@ export function generateDockerCompose( if (useGHCR) { cliProxyService.image = `${registry}/cli-proxy:${tag}`; } else { + // When building locally, pass MCPG_IMAGE as a build arg so the compiler + // can control which mcpg version is pulled (mirrors the Dockerfile's ARG default). + const buildArgs: Record = {}; + if (config.cliProxyMcpgImage) { + buildArgs.MCPG_IMAGE = config.cliProxyMcpgImage; + } cliProxyService.build = { context: path.join(projectRoot, 'containers/cli-proxy'), dockerfile: 'Dockerfile', + ...(Object.keys(buildArgs).length > 0 && { args: buildArgs }), }; } diff --git a/src/types.ts b/src/types.ts index 4d289fa1b..2d9068c07 100644 --- a/src/types.ts +++ b/src/types.ts @@ -839,6 +839,21 @@ export interface WrapperConfig { */ githubToken?: string; + /** + * Docker image reference for the mcpg DIFC proxy used inside the CLI proxy sidecar + * + * Passed as the `MCPG_IMAGE` build argument when building the cli-proxy image + * locally with `--build-local`. Has no effect when using the pre-built GHCR + * cli-proxy image (mcpg is already bundled in that image). + * + * The AWF compiler (gh-aw) sets this to control which mcpg version is pulled + * and run, enabling version pinning and testing of new mcpg releases. + * + * @default 'ghcr.io/github/gh-aw-mcpg:v0.2.2' + * @example 'ghcr.io/github/gh-aw-mcpg:v0.3.0' + */ + cliProxyMcpgImage?: string; + /** * Enable Data Loss Prevention (DLP) scanning * From 9fe138f443b3e5b271632a359af1ac3be57f1309 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:39:05 +0000 Subject: [PATCH 7/9] feat: bump default mcpg image to v0.2.15 Update the default MCPG_IMAGE from v0.2.2 to v0.2.15 in: - containers/cli-proxy/Dockerfile (ARG default) - src/cli.ts (--cli-proxy-mcpg-image flag default) - src/types.ts (JSDoc @default) Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/dba17693-9f82-440d-9284-d2e36ccac016 Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- containers/cli-proxy/Dockerfile | 2 +- src/cli.ts | 2 +- src/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/containers/cli-proxy/Dockerfile b/containers/cli-proxy/Dockerfile index bc9d8c4b1..b87e78212 100644 --- a/containers/cli-proxy/Dockerfile +++ b/containers/cli-proxy/Dockerfile @@ -12,7 +12,7 @@ # MCPG_IMAGE is configurable via --cli-proxy-mcpg-image so the AWF compiler # can control which mcpg version is pulled and run (e.g. for version pinning # or testing a new mcpg release before it is bundled in the GHCR cli-proxy image). -ARG MCPG_IMAGE=ghcr.io/github/gh-aw-mcpg:v0.2.2 +ARG MCPG_IMAGE=ghcr.io/github/gh-aw-mcpg:v0.2.15 FROM ${MCPG_IMAGE} AS mcpg-source # Stage 2: Build the CLI proxy image diff --git a/src/cli.ts b/src/cli.ts index 119b31875..4f916f2ac 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1438,7 +1438,7 @@ program 'Docker image for the mcpg DIFC proxy used inside the CLI proxy sidecar\n' + ' (only used with --build-local; ignored when pulling pre-built GHCR images)\n' + ' Set by the AWF compiler to control which mcpg version is pulled and run', - 'ghcr.io/github/gh-aw-mcpg:v0.2.2' + 'ghcr.io/github/gh-aw-mcpg:v0.2.15' ) // -- Logging & Debug -- .option( diff --git a/src/types.ts b/src/types.ts index 2d9068c07..a07f64fe0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -849,7 +849,7 @@ export interface WrapperConfig { * The AWF compiler (gh-aw) sets this to control which mcpg version is pulled * and run, enabling version pinning and testing of new mcpg releases. * - * @default 'ghcr.io/github/gh-aw-mcpg:v0.2.2' + * @default 'ghcr.io/github/gh-aw-mcpg:v0.2.15' * @example 'ghcr.io/github/gh-aw-mcpg:v0.3.0' */ cliProxyMcpgImage?: string; From d1f10eb4828b198cf353b29fb59d5930fcc384aa Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Mon, 6 Apr 2026 17:04:43 -0700 Subject: [PATCH 8/9] test: add coverage for enableCliProxy in predownload and CLI Add tests for: - resolveImages with enableCliProxy - resolveImages with both enableApiProxy and enableCliProxy - predownloadCommand with enableCliProxy - handlePredownloadAction forwarding enableCliProxy Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/cli.test.ts | 2 ++ src/commands/predownload.test.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/cli.test.ts b/src/cli.test.ts index c5a526699..0b8096362 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -2389,6 +2389,7 @@ describe('cli', () => { imageTag: 'v1.0', agentImage: 'default', enableApiProxy: false, + enableCliProxy: true, }); expect(mockPredownloadCommand).toHaveBeenCalledWith({ @@ -2396,6 +2397,7 @@ describe('cli', () => { imageTag: 'v1.0', agentImage: 'default', enableApiProxy: false, + enableCliProxy: true, }); }); diff --git a/src/commands/predownload.test.ts b/src/commands/predownload.test.ts index 679092a92..b0a946a7e 100644 --- a/src/commands/predownload.test.ts +++ b/src/commands/predownload.test.ts @@ -43,6 +43,25 @@ describe('predownload', () => { ]); }); + it('should include cli-proxy when enabled', () => { + const images = resolveImages({ ...defaults, enableCliProxy: true }); + expect(images).toEqual([ + 'ghcr.io/github/gh-aw-firewall/squid:latest', + 'ghcr.io/github/gh-aw-firewall/agent:latest', + 'ghcr.io/github/gh-aw-firewall/cli-proxy:latest', + ]); + }); + + it('should include both api-proxy and cli-proxy when both enabled', () => { + const images = resolveImages({ ...defaults, enableApiProxy: true, enableCliProxy: true }); + expect(images).toEqual([ + 'ghcr.io/github/gh-aw-firewall/squid:latest', + 'ghcr.io/github/gh-aw-firewall/agent:latest', + 'ghcr.io/github/gh-aw-firewall/api-proxy:latest', + 'ghcr.io/github/gh-aw-firewall/cli-proxy:latest', + ]); + }); + it('should use custom registry and tag', () => { const images = resolveImages({ ...defaults, @@ -116,6 +135,17 @@ describe('predownload', () => { ); }); + it('should pull cli-proxy when enabled', async () => { + await predownloadCommand({ ...defaults, enableCliProxy: true }); + + expect(execa).toHaveBeenCalledTimes(3); + expect(execa).toHaveBeenCalledWith( + 'docker', + ['pull', 'ghcr.io/github/gh-aw-firewall/cli-proxy:latest'], + { stdio: 'inherit' }, + ); + }); + it('should throw with exitCode 1 when a pull fails', async () => { execa .mockResolvedValueOnce({ stdout: '', stderr: '' }) From 81de86ca36461f25c8c77f3cf769bf937e7b1cb3 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Mon, 6 Apr 2026 17:10:16 -0700 Subject: [PATCH 9/9] test: add coverage for emitCliProxyStatusLogs in cli.ts Extract CLI proxy logging into testable emitCliProxyStatusLogs() function (same pattern as emitApiProxyTargetWarnings). Add tests for: - disabled/undefined cli proxy (no-op) - enabled with token present (read-only and writable modes) - enabled with missing token (warning messages) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/cli.test.ts | 69 ++++++++++++++++++++++++++++++++++++++++++++++++- src/cli.ts | 28 ++++++++++++++------ 2 files changed, 88 insertions(+), 9 deletions(-) diff --git a/src/cli.test.ts b/src/cli.test.ts index 0b8096362..3e28dde37 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -1,5 +1,5 @@ import { Command } from 'commander'; -import { parseEnvironmentVariables, parseDomains, parseDomainsFile, escapeShellArg, joinShellArgs, parseVolumeMounts, isValidIPv4, isValidIPv6, parseDnsServers, parseDnsOverHttps, validateAgentImage, isAgentImagePreset, AGENT_IMAGE_PRESETS, processAgentImageOption, processLocalhostKeyword, validateSkipPullWithBuildLocal, validateAllowHostPorts, validateAllowHostServicePorts, applyHostServicePortsConfig, parseMemoryLimit, validateFormat, validateApiProxyConfig, buildRateLimitConfig, validateRateLimitFlags, hasRateLimitOptions, collectRulesetFile, validateApiTargetInAllowedDomains, DEFAULT_OPENAI_API_TARGET, DEFAULT_ANTHROPIC_API_TARGET, DEFAULT_COPILOT_API_TARGET, DEFAULT_GEMINI_API_TARGET, emitApiProxyTargetWarnings, formatItem, program, parseAgentTimeout, applyAgentTimeout, handlePredownloadAction, resolveApiTargetsToAllowedDomains, extractGhesDomainsFromEngineApiTarget, extractGhecDomainsFromServerUrl } from './cli'; +import { parseEnvironmentVariables, parseDomains, parseDomainsFile, escapeShellArg, joinShellArgs, parseVolumeMounts, isValidIPv4, isValidIPv6, parseDnsServers, parseDnsOverHttps, validateAgentImage, isAgentImagePreset, AGENT_IMAGE_PRESETS, processAgentImageOption, processLocalhostKeyword, validateSkipPullWithBuildLocal, validateAllowHostPorts, validateAllowHostServicePorts, applyHostServicePortsConfig, parseMemoryLimit, validateFormat, validateApiProxyConfig, buildRateLimitConfig, validateRateLimitFlags, hasRateLimitOptions, collectRulesetFile, validateApiTargetInAllowedDomains, DEFAULT_OPENAI_API_TARGET, DEFAULT_ANTHROPIC_API_TARGET, DEFAULT_COPILOT_API_TARGET, DEFAULT_GEMINI_API_TARGET, emitApiProxyTargetWarnings, emitCliProxyStatusLogs, formatItem, program, parseAgentTimeout, applyAgentTimeout, handlePredownloadAction, resolveApiTargetsToAllowedDomains, extractGhesDomainsFromEngineApiTarget, extractGhecDomainsFromServerUrl } from './cli'; import { redactSecrets } from './redact-secrets'; import * as fs from 'fs'; import * as path from 'path'; @@ -2040,6 +2040,73 @@ describe('cli', () => { }); }); + describe('emitCliProxyStatusLogs', () => { + it('should emit nothing when cli proxy is disabled', () => { + const infos: string[] = []; + const warns: string[] = []; + emitCliProxyStatusLogs( + { enableCliProxy: false, githubToken: 'tok' }, + (msg) => infos.push(msg), + (msg) => warns.push(msg), + ); + expect(infos).toHaveLength(0); + expect(warns).toHaveLength(0); + }); + + it('should emit nothing when enableCliProxy is undefined', () => { + const infos: string[] = []; + const warns: string[] = []; + emitCliProxyStatusLogs( + {}, + (msg) => infos.push(msg), + (msg) => warns.push(msg), + ); + expect(infos).toHaveLength(0); + expect(warns).toHaveLength(0); + }); + + it('should emit info when token is present (read-only)', () => { + const infos: string[] = []; + const warns: string[] = []; + emitCliProxyStatusLogs( + { enableCliProxy: true, githubToken: 'ghp_test123', cliProxyWritable: false }, + (msg) => infos.push(msg), + (msg) => warns.push(msg), + ); + expect(infos).toHaveLength(1); + expect(infos[0]).toContain('CLI proxy enabled'); + expect(infos[0]).toContain('writable=false'); + expect(warns).toHaveLength(0); + }); + + it('should emit info when token is present (writable)', () => { + const infos: string[] = []; + const warns: string[] = []; + emitCliProxyStatusLogs( + { enableCliProxy: true, githubToken: 'ghp_test123', cliProxyWritable: true }, + (msg) => infos.push(msg), + (msg) => warns.push(msg), + ); + expect(infos).toHaveLength(1); + expect(infos[0]).toContain('writable=true'); + expect(warns).toHaveLength(0); + }); + + it('should emit warnings when token is missing', () => { + const infos: string[] = []; + const warns: string[] = []; + emitCliProxyStatusLogs( + { enableCliProxy: true }, + (msg) => infos.push(msg), + (msg) => warns.push(msg), + ); + expect(infos).toHaveLength(0); + expect(warns).toHaveLength(2); + expect(warns[0]).toContain('no GitHub token found'); + expect(warns[1]).toContain('GITHUB_TOKEN or GH_TOKEN'); + }); + }); + describe('resolveApiTargetsToAllowedDomains', () => { it('should add copilot-api-target option to allowed domains', () => { const domains: string[] = ['github.com']; diff --git a/src/cli.ts b/src/cli.ts index 4f916f2ac..04feca9c9 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -394,6 +394,25 @@ export function emitApiProxyTargetWarnings( } } +/** + * Logs CLI proxy status and emits warnings when misconfigured. + * Extracted for testability (same pattern as emitApiProxyTargetWarnings). + */ +export function emitCliProxyStatusLogs( + config: { enableCliProxy?: boolean; cliProxyWritable?: boolean; githubToken?: string }, + info: (msg: string) => void, + warn: (msg: string) => void, +): void { + if (!config.enableCliProxy) return; + + if (config.githubToken) { + info(`CLI proxy enabled: token present (GITHUB_TOKEN/GH_TOKEN), writable=${!!config.cliProxyWritable}`); + } else { + warn('⚠️ CLI proxy enabled but no GitHub token found in environment'); + warn(' Set GITHUB_TOKEN or GH_TOKEN to enable authenticated gh CLI access through the proxy'); + } +} + /** * Extracts GHEC domains from GITHUB_SERVER_URL and GITHUB_API_URL environment variables. * When GITHUB_SERVER_URL points to a GHEC tenant (*.ghe.com), returns the tenant hostname, @@ -1913,14 +1932,7 @@ program emitApiProxyTargetWarnings(config, allowedDomains, logger.warn.bind(logger)); // Log CLI proxy status - if (config.enableCliProxy) { - if (config.githubToken) { - logger.info(`CLI proxy enabled: token present (GITHUB_TOKEN/GH_TOKEN), writable=${!!config.cliProxyWritable}`); - } else { - logger.warn('⚠️ CLI proxy enabled but no GitHub token found in environment'); - logger.warn(' Set GITHUB_TOKEN or GH_TOKEN to enable authenticated gh CLI access through the proxy'); - } - } + emitCliProxyStatusLogs(config, logger.info.bind(logger), logger.warn.bind(logger)); // Log config with redacted secrets - remove API keys entirely // to prevent sensitive data from flowing to logger (CodeQL sensitive data logging)