From 7f95f18c5096a9b0026a7c5a327716be0f33360b Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Wed, 22 Jul 2026 09:33:06 +0200 Subject: [PATCH 1/2] feat(mcp-server): publish a Docker image + add /health endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lets clients deploy the MCP server without a Node toolchain (e.g. a Ruby or Python agent stack) — pull the image and run it, config via env vars only. Mirrors the workflow-executor image setup. - add an unauthenticated GET /health liveness probe (Docker HEALTHCHECK, k8s) - multi-stage Dockerfile with an isolated prod-deps install (5-package @forestadmin closure: mcp-server + agent-client + forestadmin-client + datasource-toolkit + agent-toolkit), digest-pinned base, non-root user - docker/ assets: deps-manifest generator, closure drift check, dedicated yarn.lock, boot smoke-test, README - docker-publish-mcp-server.yml: PR validate (build + smoke + trivy OS gate, npm libs report-only) and multi-arch push-by-digest to ghcr.io/forestadmin/mcp-server on release - build.yml release job dispatches the image publish on a new mcp-server tag - README/CLAUDE docs No OpenTelemetry (unlike the executor) and no prod auto-deploy: the image is deployed by the client on their own infra. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build.yml | 24 + .../workflows/docker-publish-mcp-server.yml | 268 +++++ packages/mcp-server/CLAUDE.md | 2 +- packages/mcp-server/Dockerfile | 98 ++ packages/mcp-server/README.md | 22 + packages/mcp-server/docker-compose.yml | 19 + packages/mcp-server/docker-entrypoint.sh | 11 + packages/mcp-server/docker/README.md | 44 + .../mcp-server/docker/build-deps-manifest.js | 109 ++ .../mcp-server/docker/check-deps-closure.js | 73 ++ packages/mcp-server/docker/deps/yarn.lock | 963 ++++++++++++++++++ packages/mcp-server/docker/smoke-test.sh | 52 + packages/mcp-server/src/server.ts | 6 + packages/mcp-server/test/server.test.ts | 7 + 14 files changed, 1697 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docker-publish-mcp-server.yml create mode 100644 packages/mcp-server/Dockerfile create mode 100644 packages/mcp-server/docker-compose.yml create mode 100755 packages/mcp-server/docker-entrypoint.sh create mode 100644 packages/mcp-server/docker/README.md create mode 100644 packages/mcp-server/docker/build-deps-manifest.js create mode 100644 packages/mcp-server/docker/check-deps-closure.js create mode 100644 packages/mcp-server/docker/deps/yarn.lock create mode 100755 packages/mcp-server/docker/smoke-test.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cfa2173d95..3ac11cecea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -236,6 +236,8 @@ jobs: run: npm config set workspaces-update false - name: Snapshot workflow-executor tags (pre-release) run: git tag --list '@forestadmin/workflow-executor@*' | sort > /tmp/we-tags-before.txt + - name: Snapshot mcp-server tags (pre-release) + run: git tag --list '@forestadmin/mcp-server@*' | sort > /tmp/mcp-tags-before.txt - name: "Run multi-semantic-release" run: "$(yarn bin)/multi-semantic-release --deps.bump=override" env: @@ -274,6 +276,28 @@ jobs: echo "workflow-executor@${VERSION} released — dispatching docker-publish.yml." gh workflow run docker-publish.yml --ref "$GITHUB_REF_NAME" -f version="$VERSION" + - name: Trigger mcp-server Docker image publish (if released) + if: ${{ !cancelled() }} + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + run: | + if [ ! -f /tmp/mcp-tags-before.txt ]; then + echo "Pre-release snapshot missing — release step never ran; nothing to publish." + exit 0 + fi + git tag --list '@forestadmin/mcp-server@*' | sort > /tmp/mcp-tags-after.txt + NEW_TAGS=$(comm -13 /tmp/mcp-tags-before.txt /tmp/mcp-tags-after.txt) + if [ -z "$NEW_TAGS" ]; then + echo "No new @forestadmin/mcp-server tag in this run — nothing to publish." + exit 0 + fi + # If more than one appears, pick the highest so :latest never regresses. + VERSION=$(echo "$NEW_TAGS" \ + | sed 's|^@forestadmin/mcp-server@||' \ + | sort -V | tail -n1) + echo "mcp-server@${VERSION} released — dispatching docker-publish-mcp-server.yml." + gh workflow run docker-publish-mcp-server.yml --ref "$GITHUB_REF_NAME" -f version="$VERSION" + publish-api-reference: name: Publish API Reference runs-on: ubuntu-latest diff --git a/.github/workflows/docker-publish-mcp-server.yml b/.github/workflows/docker-publish-mcp-server.yml new file mode 100644 index 0000000000..244b59ac59 --- /dev/null +++ b/.github/workflows/docker-publish-mcp-server.yml @@ -0,0 +1,268 @@ +name: Publish mcp-server Docker image + +on: + # Releases publish via workflow_dispatch from build.yml's `release` job, not a + # tag-push trigger — the release commit's `[skip ci]` would suppress it. + pull_request: + # Validate on any change to the MCP server or to a dependency package's + # manifest — a dep bump in one of the 4 source packages can drift the + # image's dedicated lockfile (see packages/mcp-server/docker/). + paths: + - 'packages/mcp-server/**' + - 'packages/agent-client/package.json' + - 'packages/forestadmin-client/package.json' + - 'packages/datasource-toolkit/package.json' + - 'packages/agent-toolkit/package.json' + # The builder runs `yarn install --frozen-lockfile` from the repo root, so a + # lockfile-only change can alter the build (build.yml installs without + # --frozen-lockfile and wouldn't catch an inconsistency). + - 'yarn.lock' + - '.github/workflows/docker-publish-mcp-server.yml' + workflow_dispatch: + inputs: + version: + description: 'Version tag to publish (e.g. 1.18.0)' + required: true + +permissions: + contents: read + packages: write + +concurrency: + # Include the dispatch version so two manual runs for different versions from the + # same branch don't cancel each other mid-release. + group: docker-publish-mcp-server-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version || '' }} + cancel-in-progress: true + +jobs: + # On PRs: build the image (no push) to catch Dockerfile breakage before release. + validate: + name: Validate Dockerfile build + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + + - name: Check @forestadmin dependency closure + run: node packages/mcp-server/docker/check-deps-closure.js + + - uses: docker/setup-buildx-action@v3 + - name: Build (no push) + uses: docker/build-push-action@v6 + with: + context: . + file: packages/mcp-server/Dockerfile + platforms: linux/amd64 + push: false + load: true + tags: mcp-server:pr + cache-from: type=gha,scope=mcp-amd64 + + # The build only proves the image compiles. Run it to catch entrypoint + # breakage, a missing module (e.g. an un-copied @forestadmin package), or a + # startup crash — none of which a build-only step would surface. + - name: Smoke test (entrypoint + module graph + boot) + run: sh packages/mcp-server/docker/smoke-test.sh mcp-server:pr + + # Gate by ORIGIN, not severity. OS packages can only be fixed here, so they + # BLOCK; the MCP server's npm deps are already shipped via the package, so + # blocking the image would just desync GHCR from npm — report-only. + - name: Scan OS packages (blocking) + uses: aquasecurity/trivy-action@v0.36.0 + with: + image-ref: mcp-server:pr + vuln-type: os + severity: CRITICAL,HIGH + ignore-unfixed: true + exit-code: '1' + - name: Scan libraries (report only) + uses: aquasecurity/trivy-action@v0.36.0 + with: + image-ref: mcp-server:pr + vuln-type: library + severity: CRITICAL,HIGH + ignore-unfixed: true + exit-code: '0' + + extract-version: + name: Extract version + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + outputs: + full: ${{ steps.version.outputs.full }} + minor: ${{ steps.version.outputs.minor }} + major: ${{ steps.version.outputs.major }} + is_latest: ${{ steps.version.outputs.is_latest }} + steps: + - uses: actions/checkout@v4 + with: + # Need all tags to determine whether this is the latest stable version. + fetch-depth: 0 + - name: Parse version from input + id: version + run: | + VERSION="${{ github.event.inputs.version }}" + echo "full=$VERSION" >> $GITHUB_OUTPUT + echo "minor=${VERSION%.*}" >> $GITHUB_OUTPUT + echo "major=${VERSION%%.*}" >> $GITHUB_OUTPUT + + # Move the mutable tags (:latest, :major, :minor) only when this is the + # highest STABLE version of this package — never for a prerelease, and + # never when rebuilding an older version via workflow_dispatch (which + # would regress :latest onto a stale image). We compare against the git + # tags of this package specifically (the repo-wide GitHub "latest + # release" belongs to whichever monorepo package shipped last). + PREFIX="@forestadmin/mcp-server@" + LATEST_STABLE=$(git tag --list "${PREFIX}*" \ + | sed "s|^${PREFIX}||" \ + | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' \ + | sort -V | tail -n1) + case "$VERSION" in + *-*) IS_LATEST=false ;; # prerelease + "$LATEST_STABLE") IS_LATEST=true ;; + *) IS_LATEST=false ;; + esac + echo "is_latest=$IS_LATEST" >> $GITHUB_OUTPUT + + build: + name: Build (${{ matrix.arch }}) + if: github.event_name != 'pull_request' + needs: extract-version + runs-on: ${{ matrix.runner }} + strategy: + matrix: + include: + - arch: amd64 + platform: linux/amd64 + runner: ubuntu-latest + - arch: arm64 + platform: linux/arm64 + runner: ubuntu-24.04-arm + steps: + - uses: actions/checkout@v4 + with: + # Build from the requested version's tag, not the default branch. + ref: ${{ format('@forestadmin/mcp-server@{0}', github.event.inputs.version) }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Build and LOAD locally first (no push) so we can smoke-test and scan the + # exact image before it reaches the registry. Nothing is published until + # both gates pass — otherwise a vulnerable/broken image would be pullable + # by digest even when the gate "fails". + - name: Build (load for gating) + uses: docker/build-push-action@v6 + with: + context: . + file: packages/mcp-server/Dockerfile + platforms: ${{ matrix.platform }} + load: true + tags: mcp-server:${{ matrix.arch }} + cache-from: type=gha,scope=mcp-${{ matrix.arch }} + cache-to: type=gha,mode=max,scope=mcp-${{ matrix.arch }} + + - name: Smoke test (entrypoint + module graph + boot) + run: sh packages/mcp-server/docker/smoke-test.sh mcp-server:${{ matrix.arch }} + + # Gate by origin (see the validate job): OS blocks; the MCP server's npm + # deps are report-only (already shipped via the package). + - name: Scan OS packages (blocking) + uses: aquasecurity/trivy-action@v0.36.0 + with: + image-ref: mcp-server:${{ matrix.arch }} + vuln-type: os + severity: CRITICAL,HIGH + ignore-unfixed: true + exit-code: '1' + - name: Scan libraries (report only) + uses: aquasecurity/trivy-action@v0.36.0 + with: + image-ref: mcp-server:${{ matrix.arch }} + vuln-type: library + severity: CRITICAL,HIGH + ignore-unfixed: true + exit-code: '0' + + # All gates passed (smoke + OS scan). Publish by digest (cache hit). + - name: Push by digest + id: build + uses: docker/build-push-action@v6 + with: + context: . + file: packages/mcp-server/Dockerfile + platforms: ${{ matrix.platform }} + outputs: type=image,name=ghcr.io/forestadmin/mcp-server,push-by-digest=true,name-canonical=true,push=true + sbom: true + cache-from: type=gha,scope=mcp-${{ matrix.arch }} + + - name: Export digest + run: | + mkdir -p /tmp/digests + touch "/tmp/digests/${DIGEST#sha256:}" + env: + DIGEST: ${{ steps.build.outputs.digest }} + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digest-${{ matrix.arch }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + name: Publish multi-arch manifest + if: github.event_name != 'pull_request' + needs: [extract-version, build] + runs-on: ubuntu-latest + steps: + - name: Download digests + uses: actions/download-artifact@v4 + with: + pattern: digest-* + merge-multiple: true + path: /tmp/digests + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create and push multi-arch manifest + working-directory: /tmp/digests + env: + IMAGE: ghcr.io/forestadmin/mcp-server + FULL: ${{ needs.extract-version.outputs.full }} + MINOR: ${{ needs.extract-version.outputs.minor }} + MAJOR: ${{ needs.extract-version.outputs.major }} + IS_LATEST: ${{ needs.extract-version.outputs.is_latest }} + run: | + # Always publish the immutable full-version tag. The mutable tags + # (minor, major, latest) only move when this is the latest stable + # version, so neither a prerelease nor a rebuild of an older version + # ever overwrites the current stable :latest / :1 images. + TAGS="-t $IMAGE:$FULL" + if [ "$IS_LATEST" = "true" ]; then + TAGS="$TAGS -t $IMAGE:$MINOR -t $IMAGE:$MAJOR -t $IMAGE:latest" + fi + docker buildx imagetools create $TAGS \ + $(printf "$IMAGE@sha256:%s " *) + + - name: Inspect manifest + run: docker buildx imagetools inspect ghcr.io/forestadmin/mcp-server:${{ needs.extract-version.outputs.full }} diff --git a/packages/mcp-server/CLAUDE.md b/packages/mcp-server/CLAUDE.md index 552db3aa83..d0d9106549 100644 --- a/packages/mcp-server/CLAUDE.md +++ b/packages/mcp-server/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Overview -`@forestadmin/mcp-server` is a Model Context Protocol (MCP) server that exposes Forest Admin data operations (list, CRUD, relations, custom actions) to AI assistants over an OAuth-authenticated, streamable HTTP transport. It runs either standalone (`forest-mcp-server` CLI) or embedded in a Forest Admin agent via `agent.mountAiMcpServer()`. +`@forestadmin/mcp-server` is a Model Context Protocol (MCP) server that exposes Forest Admin data operations (list, CRUD, relations, custom actions) to AI assistants over an OAuth-authenticated, streamable HTTP transport. It runs either standalone (`forest-mcp-server` CLI) or embedded in a Forest Admin agent via `agent.mountAiMcpServer()`. It is also distributed as a Docker image (`ghcr.io/forestadmin/mcp-server`) for Node-less deployments — build assets live in `docker/` + `Dockerfile`, published by `.github/workflows/docker-publish-mcp-server.yml`. ## Architecture diff --git a/packages/mcp-server/Dockerfile b/packages/mcp-server/Dockerfile new file mode 100644 index 0000000000..c067e5c4fc --- /dev/null +++ b/packages/mcp-server/Dockerfile @@ -0,0 +1,98 @@ +# syntax=docker/dockerfile:1 +# +# Production image for @forestadmin/mcp-server. +# The build context MUST be the monorepo root (yarn workspaces + yarn.lock): +# +# docker build -f packages/mcp-server/Dockerfile -t ghcr.io/forestadmin/mcp-server:latest . +# +# Configuration is passed entirely via environment variables — see README for the full list. + +# Base image pinned by digest for reproducible builds (node:22-bookworm-slim). +# Bump via Renovate/Dependabot. +FROM node:22-bookworm-slim@sha256:813a7480f28fdadac1f7f5c824bcdad435b5bc1322a5968bbbdef8d058f9dff4 AS base + +# ── Stage 1: build ──────────────────────────────────────────────────────────── +FROM base AS builder +WORKDIR /app + +# Install from manifests + lockfile only (before the source COPY) so editing +# source doesn't bust the install layer — keeps the install cached on +# source-only changes (CI cache-warm + local rebuilds). --parents preserves the +# packages/*/ layout that yarn workspaces needs. +# --frozen-lockfile guarantees the resolved tree matches yarn.lock exactly. +# --ignore-scripts skips husky, native (node-gyp) builds, and binary downloads +# irrelevant here. +COPY --parents package.json yarn.lock packages/*/package.json ./ +RUN yarn install --frozen-lockfile --ignore-scripts + +COPY . . + +# Build the MCP server and only its 4 workspace dependencies, in topological order. +RUN node_modules/.bin/lerna run build \ + --scope @forestadmin/mcp-server \ + --include-dependencies + +# ── Stage 2: isolated prod deps ─────────────────────────────────────────────── +# Install only the image's runtime deps (external deps of the 5 workspace packages) +# into a clean, standalone node_modules. Avoids hoisting deps of the other +# workspace packages and cuts the runtime image significantly. +# +# The manifest is regenerated from the live package.json files; the committed +# yarn.lock pins every (transitive) version. --frozen-lockfile makes a workspace +# dependency change that the lock does not cover fail the build instead of +# silently shipping an unpinned version. +FROM base AS prod-deps +WORKDIR /deps + +COPY packages/mcp-server/docker/build-deps-manifest.js ./build-deps-manifest.js +COPY packages/mcp-server/docker/deps/yarn.lock ./yarn.lock +COPY packages/mcp-server/package.json ./packages/mcp-server/package.json +COPY packages/agent-client/package.json ./packages/agent-client/package.json +COPY packages/forestadmin-client/package.json ./packages/forestadmin-client/package.json +COPY packages/datasource-toolkit/package.json ./packages/datasource-toolkit/package.json +COPY packages/agent-toolkit/package.json ./packages/agent-toolkit/package.json + +RUN node build-deps-manifest.js packages package.json +RUN yarn install --frozen-lockfile --ignore-scripts + +# ── Stage 3: runtime ────────────────────────────────────────────────────────── +FROM base AS runtime +WORKDIR /app +ENV NODE_ENV=production + +# External runtime deps (clean, reproducible install — no monorepo workspace noise). +COPY --from=prod-deps /deps/node_modules ./node_modules + +# @forestadmin/* workspace packages placed directly in node_modules. +# Node resolves them by walking up to /app/node_modules/@forestadmin//, +# where package.json's "main" field points into dist/. +COPY --from=builder /app/packages/agent-client/dist ./node_modules/@forestadmin/agent-client/dist +COPY --from=builder /app/packages/agent-client/package.json ./node_modules/@forestadmin/agent-client/package.json +COPY --from=builder /app/packages/forestadmin-client/dist ./node_modules/@forestadmin/forestadmin-client/dist +COPY --from=builder /app/packages/forestadmin-client/package.json ./node_modules/@forestadmin/forestadmin-client/package.json +COPY --from=builder /app/packages/datasource-toolkit/dist ./node_modules/@forestadmin/datasource-toolkit/dist +COPY --from=builder /app/packages/datasource-toolkit/package.json ./node_modules/@forestadmin/datasource-toolkit/package.json +COPY --from=builder /app/packages/agent-toolkit/dist ./node_modules/@forestadmin/agent-toolkit/dist +COPY --from=builder /app/packages/agent-toolkit/package.json ./node_modules/@forestadmin/agent-toolkit/package.json + +# MCP server entry point. +COPY --from=builder /app/packages/mcp-server/dist ./packages/mcp-server/dist +COPY --from=builder /app/packages/mcp-server/package.json ./packages/mcp-server/package.json +COPY packages/mcp-server/docker-entrypoint.sh ./packages/mcp-server/docker-entrypoint.sh + +USER node + +# Streamable-HTTP MCP server (GET /health, OAuth discovery, POST /mcp). +# Override with MCP_SERVER_PORT env var. +EXPOSE 3931 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD node -e "require('http').get('http://localhost:'+(process.env.MCP_SERVER_PORT||3931)+'/health',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))" + +# OCI image metadata (https://github.com/opencontainers/image-spec). +LABEL org.opencontainers.image.title="forestadmin-mcp-server" \ + org.opencontainers.image.description="Model Context Protocol server for Forest Admin." \ + org.opencontainers.image.source="https://github.com/ForestAdmin/agent-nodejs" \ + org.opencontainers.image.licenses="GPL-3.0" + +ENTRYPOINT ["/bin/sh", "packages/mcp-server/docker-entrypoint.sh"] diff --git a/packages/mcp-server/README.md b/packages/mcp-server/README.md index d092e9d0a0..2fbaee3414 100644 --- a/packages/mcp-server/README.md +++ b/packages/mcp-server/README.md @@ -85,6 +85,27 @@ Or set the variables inline: FOREST_ENV_SECRET="your-env-secret" FOREST_AUTH_SECRET="your-auth-secret" npx forest-mcp-server ``` +### With Docker + +A prebuilt image is published to GitHub Container Registry, so you can run the +server without installing Node — handy when your stack is not Node-based (e.g. a +Ruby or Python agent). Configuration is entirely via environment variables (same +as the standalone CLI). + +```bash +docker run --rm -p 3931:3931 \ + -e FOREST_ENV_SECRET="your-env-secret" \ + -e FOREST_AUTH_SECRET="your-auth-secret" \ + ghcr.io/forestadmin/mcp-server:latest +``` + +The MCP endpoint is then at `http://localhost:3931/mcp`, and `GET /health` +answers `200` for liveness/readiness probes. Tags: `:latest`, `:`, +`:`, and the immutable `:`. + +A [`docker-compose.yml`](./docker-compose.yml) is provided for a minimal +single-instance setup (`env_file: .env`). + ## Restrict Tools You can restrict which tools the MCP server exposes using `enabledTools`. Only the listed tools will be available. **New tools added in future releases will NOT be automatically enabled** — you must explicitly add them. @@ -114,6 +135,7 @@ Once running, the MCP server exposes the following endpoints: | Method | Path | Description | |--------|------|-------------| +| GET | `/health` | Liveness probe (no auth), returns `200 {"status":"ok"}` | | POST | `/mcp` | Main MCP protocol endpoint (requires Bearer token) | | POST | `/oauth/authorize` | OAuth 2.0 authorization | | POST | `/oauth/token` | OAuth 2.0 token exchange | diff --git a/packages/mcp-server/docker-compose.yml b/packages/mcp-server/docker-compose.yml new file mode 100644 index 0000000000..aefb9242e6 --- /dev/null +++ b/packages/mcp-server/docker-compose.yml @@ -0,0 +1,19 @@ +# Minimal single-instance setup for the Forest Admin MCP server. +# +# 1. Create a .env file with your secrets (FOREST_ENV_SECRET, FOREST_AUTH_SECRET +# at minimum — see the README for the full list). +# 2. docker compose up +# +# The MCP endpoint is then reachable at: http://localhost:3931/mcp + +services: + mcp-server: + image: ghcr.io/forestadmin/mcp-server:latest + restart: unless-stopped + # Forward the whole .env into the container. Unset optional vars fall back to + # the image's built-in defaults — no whitelist to keep in sync. + env_file: + - .env + ports: + - "${MCP_SERVER_PORT:-3931}:${MCP_SERVER_PORT:-3931}" + # Healthcheck is inherited from the image's HEALTHCHECK (see Dockerfile). diff --git a/packages/mcp-server/docker-entrypoint.sh b/packages/mcp-server/docker-entrypoint.sh new file mode 100755 index 0000000000..81dd0ba047 --- /dev/null +++ b/packages/mcp-server/docker-entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +# Run the MCP server when invoked with no args. Any first token is treated as a +# command override (e.g. `docker run node …` or `sh`) and exec'd as-is, +# so the image stays debuggable. +if [ "$#" -eq 0 ]; then + exec node /app/packages/mcp-server/dist/cli.js +fi + +exec "$@" diff --git a/packages/mcp-server/docker/README.md b/packages/mcp-server/docker/README.md new file mode 100644 index 0000000000..abee5d8fad --- /dev/null +++ b/packages/mcp-server/docker/README.md @@ -0,0 +1,44 @@ +# Docker image build assets + +The production image installs its runtime dependencies into an **isolated** +`node_modules` (rather than shipping the whole monorepo's hoisted tree). This +keeps the image small while staying reproducible. + +## How it works + +- [`build-deps-manifest.js`](./build-deps-manifest.js) merges the external + (non-`@forestadmin`) runtime dependencies of the MCP server and its 4 workspace + dependencies into a single `package.json`. +- [`deps/yarn.lock`](./deps/) pins every transitive version. +- The Docker build regenerates the manifest from the live `package.json` files + and runs `yarn install --frozen-lockfile`. If a workspace dependency changes + in a way the committed lock does not cover, **the build fails** instead of + silently shipping an unpinned version. +- [`check-deps-closure.js`](./check-deps-closure.js) guards against drift between + the real `@forestadmin/*` closure of `mcp-server`, `WORKSPACE_PACKAGES`, and the + Dockerfile's `COPY` lines. +- [`smoke-test.sh`](./smoke-test.sh) boots a built image and asserts it answers + `/health` (proving the entrypoint and full module graph work). + +## Updating the lockfile + +Only `deps/yarn.lock` is committed — the manifest is generated on demand (the +Docker build regenerates it too), so there is no stale `package.json` to drift. +Run this whenever a runtime dependency of one of the 5 workspace packages +changes (the build will fail with `--frozen-lockfile` until you do). + +The generated manifest carries the repo's pinned `packageManager` +(`yarn@1.22.19`), so with Corepack enabled (`corepack enable`) the refresh uses +the same Yarn as the Docker build — a global Yarn 4 would otherwise emit an +incompatible lockfile format. + +```bash +# from the monorepo root (Corepack enabled) +TMP=$(mktemp -d) +node packages/mcp-server/docker/build-deps-manifest.js packages "$TMP/package.json" +( cd "$TMP" && yarn install --ignore-scripts ) +cp "$TMP/yarn.lock" packages/mcp-server/docker/deps/yarn.lock +rm -rf "$TMP" +``` + +Then commit the updated `deps/yarn.lock`. diff --git a/packages/mcp-server/docker/build-deps-manifest.js b/packages/mcp-server/docker/build-deps-manifest.js new file mode 100644 index 0000000000..c7f437ae56 --- /dev/null +++ b/packages/mcp-server/docker/build-deps-manifest.js @@ -0,0 +1,109 @@ +// Generates the package.json for the Docker image's isolated runtime deps. +// +// It merges the external (non-@forestadmin) runtime dependencies of the MCP +// server and its 4 workspace dependencies into a single manifest. +// +// The output is deterministic (sorted keys). A committed yarn.lock sits next to +// the generated manifest; the Docker build regenerates the manifest and runs +// `yarn install --frozen-lockfile`, so any workspace dependency change that the +// lock does not cover fails the build instead of silently drifting. +// +// Usage: node build-deps-manifest.js +// directory containing the workspace packages (e.g. "packages") +// path to write the merged package.json + +const fs = require('fs'); +const path = require('path'); + +const WORKSPACE_PACKAGES = [ + 'mcp-server', + 'agent-client', + 'forestadmin-client', + 'datasource-toolkit', + 'agent-toolkit', +]; + +function generate(packagesDir, outFile) { + // Collect every declared range per external dependency, tracking which packages + // ask for it — a flat install can hold only one version, so we must resolve + // disagreements deliberately rather than let iteration order decide. + const declared = {}; + for (const pkg of WORKSPACE_PACKAGES) { + const manifestPath = path.join(packagesDir, pkg, 'package.json'); + const { dependencies = {} } = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + + for (const [name, range] of Object.entries(dependencies)) { + if (name.startsWith('@forestadmin/')) continue; + declared[name] = declared[name] || {}; + (declared[name][range] = declared[name][range] || []).push(pkg); + } + } + + const deps = {}; + for (const [name, byRange] of Object.entries(declared)) { + const ranges = Object.keys(byRange); + if (ranges.length === 1) { + [deps[name]] = ranges; + continue; + } + + // Conflicting ranges. The image IS the MCP server, so its own pin is + // authoritative (its tested version); any other conflict must be aligned by + // a human rather than silently collapsed. + const serverRange = ranges.find(r => byRange[r].includes('mcp-server')); + if (!serverRange) { + const detail = ranges.map(r => `${r} (${byRange[r].join(', ')})`).join('; '); + throw new Error( + `Conflicting ranges for "${name}" not arbitrated by mcp-server: ${detail}. ` + + 'Align the version in the source packages.', + ); + } + console.warn( + `[build-deps] "${name}": conflicting ranges (${ranges.join(', ')}); ` + + `using the mcp-server's pin "${serverRange}".`, + ); + deps[name] = serverRange; + } + + const sorted = Object.fromEntries(Object.keys(deps).sort().map(key => [key, deps[key]])); + + const manifest = { name: 'mcp-server-docker-deps', private: true }; + + // Carry the monorepo's pinned package manager so a manual lockfile refresh + // (yarn install on this generated manifest) uses the same Yarn via Corepack, + // not a contributor's global Yarn 4 which would emit an incompatible lockfile. + // Resolved from the repo root relative to this script; absent in the Docker + // build (root package.json isn't copied there) — harmless, the image uses its + // own bundled Yarn 1.x. + const packageManager = rootPackageManager(); + if (packageManager) manifest.packageManager = packageManager; + + manifest.dependencies = sorted; + + fs.writeFileSync(outFile, `${JSON.stringify(manifest, null, 2)}\n`); +} + +function rootPackageManager() { + try { + const root = JSON.parse( + fs.readFileSync(path.join(__dirname, '..', '..', '..', 'package.json'), 'utf8'), + ); + + return root.packageManager; + } catch { + return undefined; + } +} + +if (require.main === module) { + const [, , packagesDir, outFile] = process.argv; + + if (!packagesDir || !outFile) { + console.error('Usage: node build-deps-manifest.js '); + process.exit(1); + } + + generate(packagesDir, outFile); +} + +module.exports = { WORKSPACE_PACKAGES, generate }; diff --git a/packages/mcp-server/docker/check-deps-closure.js b/packages/mcp-server/docker/check-deps-closure.js new file mode 100644 index 0000000000..7a7d4d39cd --- /dev/null +++ b/packages/mcp-server/docker/check-deps-closure.js @@ -0,0 +1,73 @@ +// Guard-rail against silent drift in the Docker image's @forestadmin closure. +// +// The image's runtime deps are assembled from a HARDCODED set in two places: +// - WORKSPACE_PACKAGES in build-deps-manifest.js (gathers their external deps) +// - the `COPY --from=builder .../dist` lines in the Dockerfile (ships their build) +// +// If the MCP server gains/loses an @forestadmin/* dependency, both must change or the +// runtime image breaks at startup (Cannot find module) — invisible to a build-only CI. +// This check recomputes the real transitive @forestadmin closure of mcp-server +// and fails if the hardcoded list or the Dockerfile COPYs don't match it. +// +// Usage: node check-deps-closure.js (exits non-zero on drift) + +const fs = require('fs'); +const path = require('path'); +const { WORKSPACE_PACKAGES } = require('./build-deps-manifest'); + +const PACKAGES_DIR = path.resolve(__dirname, '../..'); +const DOCKERFILE = path.resolve(__dirname, '../Dockerfile'); +const ROOT_PACKAGE = '@forestadmin/mcp-server'; + +// Map every @forestadmin/* package name to its directory under packages/. +const nameToDir = {}; +for (const dir of fs.readdirSync(PACKAGES_DIR)) { + const manifestPath = path.join(PACKAGES_DIR, dir, 'package.json'); + if (!fs.existsSync(manifestPath)) continue; + const { name } = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + if (name) nameToDir[name] = dir; +} + +// BFS the transitive @forestadmin closure (including the root itself). +const closure = new Set(); +const queue = [ROOT_PACKAGE]; +while (queue.length) { + const name = queue.shift(); + const dir = nameToDir[name]; + if (!dir || closure.has(dir)) continue; + closure.add(dir); + const { dependencies = {} } = JSON.parse( + fs.readFileSync(path.join(PACKAGES_DIR, dir, 'package.json'), 'utf8'), + ); + for (const dep of Object.keys(dependencies)) { + if (dep.startsWith('@forestadmin/')) queue.push(dep); + } +} + +const actual = [...closure].sort(); +const declared = [...WORKSPACE_PACKAGES].sort(); +const dockerfile = fs.readFileSync(DOCKERFILE, 'utf8'); + +const errors = []; + +const missingFromList = actual.filter(p => !declared.includes(p)); +const extraInList = declared.filter(p => !actual.includes(p)); +if (missingFromList.length) errors.push(`WORKSPACE_PACKAGES is missing: ${missingFromList.join(', ')}`); +if (extraInList.length) errors.push(`WORKSPACE_PACKAGES has stale entries: ${extraInList.join(', ')}`); + +// Every closure package except the MCP server itself must be copied into node_modules. +for (const pkg of actual) { + if (pkg === 'mcp-server') continue; + if (!dockerfile.includes(`/app/packages/${pkg}/dist`)) { + errors.push(`Dockerfile is missing a COPY for packages/${pkg}/dist`); + } +} + +if (errors.length) { + console.error('@forestadmin dependency closure drift detected:\n - ' + errors.join('\n - ')); + console.error(`\nActual closure: ${actual.join(', ')}`); + console.error('Update WORKSPACE_PACKAGES (build-deps-manifest.js) and the Dockerfile COPY lines to match.'); + process.exit(1); +} + +console.log(`@forestadmin closure OK (${actual.length} packages): ${actual.join(', ')}`); diff --git a/packages/mcp-server/docker/deps/yarn.lock b/packages/mcp-server/docker/deps/yarn.lock new file mode 100644 index 0000000000..65a8bc553d --- /dev/null +++ b/packages/mcp-server/docker/deps/yarn.lock @@ -0,0 +1,963 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@hono/node-server@^1.19.9": + version "1.19.14" + resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-1.19.14.tgz#e30f844bc77e3ce7be442aac3b1f73ad8b58d181" + integrity sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw== + +"@modelcontextprotocol/sdk@^1.28.0": + version "1.29.0" + resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz#79786d8b525e269de850ac82b1f1f757f3915f44" + integrity sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ== + dependencies: + "@hono/node-server" "^1.19.9" + ajv "^8.17.1" + ajv-formats "^3.0.1" + content-type "^1.0.5" + cors "^2.8.5" + cross-spawn "^7.0.5" + eventsource "^3.0.2" + eventsource-parser "^3.0.0" + express "^5.2.1" + express-rate-limit "^8.2.1" + hono "^4.11.4" + jose "^6.1.3" + json-schema-typed "^8.0.2" + pkce-challenge "^5.0.0" + raw-body "^3.0.0" + zod "^3.25 || ^4.0" + zod-to-json-schema "^3.25.1" + +"@noble/hashes@^1.1.5": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" + integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== + +"@paralleldrive/cuid2@^2.2.2": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz#3d62ea9e7be867d3fa94b9897fab5b0ae187d784" + integrity sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw== + dependencies: + "@noble/hashes" "^1.1.5" + +accepts@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" + integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== + dependencies: + mime-types "^3.0.0" + negotiator "^1.0.0" + +ajv-formats@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-3.0.1.tgz#3d5dc762bca17679c3c2ea7e90ad6b7532309578" + integrity sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ== + dependencies: + ajv "^8.0.0" + +ajv@^8.0.0, ajv@^8.17.1: + version "8.20.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.20.0.tgz#304b3636add88ba7d936760dd50ece006dea95f9" + integrity sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +body-parser@^2.2.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.3.0.tgz#6d8662f4d8c336028b8ac9aa24251b0ca64ba437" + integrity sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw== + dependencies: + bytes "^3.1.2" + content-type "^2.0.0" + debug "^4.4.3" + http-errors "^2.0.1" + iconv-lite "^0.7.2" + on-finished "^2.4.1" + qs "^6.15.2" + raw-body "^3.0.2" + type-is "^2.1.0" + +buffer-equal-constant-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + +bytes@^3.1.2, bytes@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +component-emitter@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17" + integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ== + +content-disposition@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.1.0.tgz#f3db789c752d45564cc7e9e1e0b31790d4a38e17" + integrity sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g== + +content-type@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +content-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-2.0.0.tgz#2fb3ede69dffa0af78ca7c4ce7589680638b56df" + integrity sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ== + +cookie-signature@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" + integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== + +cookie@^0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + +cookiejar@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" + integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== + +cors@^2.8.5: + version "2.8.6" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.6.tgz#ff5dd69bd95e547503820d29aba4f8faf8dfec96" + integrity sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw== + dependencies: + object-assign "^4" + vary "^1" + +cross-spawn@^7.0.5: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@^4.3.7, debug@^4.4.0, debug@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@^2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +encodeurl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz#a2d0b373205724dfa525d23b0c3e1b1ca582c99b" + integrity sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +escape-html@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +etag@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eventsource-parser@^3.0.0, eventsource-parser@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.1.0.tgz#4e198eb91cd333d0a8ddcc036502b3618a25f449" + integrity sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg== + +eventsource@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-2.0.2.tgz#76dfcc02930fb2ff339520b6d290da573a9e8508" + integrity sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA== + +eventsource@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-3.0.7.tgz#1157622e2f5377bb6aef2114372728ba0c156989" + integrity sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA== + dependencies: + eventsource-parser "^3.0.1" + +express-rate-limit@^8.2.1: + version "8.6.0" + resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-8.6.0.tgz#2e64ab10361da35881519ea92b426a43f8aa8c79" + integrity sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA== + dependencies: + debug "^4.4.3" + ip-address "^10.2.0" + +express@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/express/-/express-5.2.1.tgz#8f21d15b6d327f92b4794ecf8cb08a72f956ac04" + integrity sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw== + dependencies: + accepts "^2.0.0" + body-parser "^2.2.1" + content-disposition "^1.0.0" + content-type "^1.0.5" + cookie "^0.7.1" + cookie-signature "^1.2.1" + debug "^4.4.0" + depd "^2.0.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + finalhandler "^2.1.0" + fresh "^2.0.0" + http-errors "^2.0.0" + merge-descriptors "^2.0.0" + mime-types "^3.0.0" + on-finished "^2.4.1" + once "^1.4.0" + parseurl "^1.3.3" + proxy-addr "^2.0.7" + qs "^6.14.0" + range-parser "^1.2.1" + router "^2.2.0" + send "^1.1.0" + serve-static "^2.2.0" + statuses "^2.0.1" + type-is "^2.0.1" + vary "^1.1.2" + +fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + +fast-uri@^3.0.1: + version "3.1.4" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.4.tgz#3b3daf9ce68f41f956df0b505132c0cfce9ec7af" + integrity sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw== + +finalhandler@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.1.1.tgz#a2c517a6559852bcdb06d1f8bd7f51b68fad8099" + integrity sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA== + dependencies: + debug "^4.4.0" + encodeurl "^2.0.0" + escape-html "^1.0.3" + on-finished "^2.4.1" + parseurl "^1.3.3" + statuses "^2.0.1" + +form-data@^4.0.5: + version "4.0.6" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.6.tgz#28e864e1b786dbebb68db1f452f9635278665827" + integrity sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + hasown "^2.0.4" + mime-types "^2.1.35" + +formidable@^3.5.4: + version "3.5.4" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-3.5.4.tgz#ac9a593b951e829b3298f21aa9a2243932f32ed9" + integrity sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug== + dependencies: + "@paralleldrive/cuid2" "^2.2.2" + dezalgo "^1.0.4" + once "^1.4.0" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4" + integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.2, hasown@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.4.tgz#8c62d8cb90beb2aad5d0a5b67581ad9854c3f003" + integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A== + dependencies: + function-bind "^1.1.2" + +hono@^4.11.4: + version "4.12.31" + resolved "https://registry.yarnpkg.com/hono/-/hono-4.12.31.tgz#2ccaaf3ccd82db372f169c5b39c065c31ff36294" + integrity sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg== + +http-errors@^2.0.0, http-errors@^2.0.1, http-errors@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b" + integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== + dependencies: + depd "~2.0.0" + inherits "~2.0.4" + setprototypeof "~1.2.0" + statuses "~2.0.2" + toidentifier "~1.0.1" + +iconv-lite@^0.7.2, iconv-lite@~0.7.0: + version "0.7.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.7.3.tgz#84ee12f963e7de50bc01a13e160a078b3b0f415f" + integrity sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +inflected@^1.1.6: + version "1.1.7" + resolved "https://registry.yarnpkg.com/inflected/-/inflected-1.1.7.tgz#c393df6e28472d0d77b3082ec3aa2091f4bc96f9" + integrity sha512-3lz7idKIPmKvz0wqlu1PUPSg5strJnCh2v2NldPQy13Fmd6WsWQ5yExDoiIX48lQ9mo8N7ztdDlkZxOauZ/E5g== + +inherits@~2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ip-address@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.2.0.tgz#805fc178b20c518bd4c8548b24fe30892d7f3206" + integrity sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-promise@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" + integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +jose@^4.15.9: + version "4.15.9" + resolved "https://registry.yarnpkg.com/jose/-/jose-4.15.9.tgz#9b68eda29e9a0614c042fa29387196c7dd800100" + integrity sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA== + +jose@^6.1.3: + version "6.2.4" + resolved "https://registry.yarnpkg.com/jose/-/jose-6.2.4.tgz#69de7346761cd04942c659e524d988feb16a4a6e" + integrity sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA== + +json-api-serializer@^2.6.6: + version "2.6.6" + resolved "https://registry.yarnpkg.com/json-api-serializer/-/json-api-serializer-2.6.6.tgz#fa5d283b913e0ea25653f4d04f981dc44d66dd4f" + integrity sha512-l7/e2O5/0+GU38RTMowRUKxRlT8zFc1oryAYBxqK54sFZnvJOJI2bo4XbbxnkTtvSzwkyv5DoBUuFm0hvG3avg== + dependencies: + setimmediate "^1.0.5" + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-schema-typed@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/json-schema-typed/-/json-schema-typed-8.0.2.tgz#e98ee7b1899ff4a184534d1f167c288c66bbeff4" + integrity sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA== + +jsonapi-serializer@^3.6.9: + version "3.6.9" + resolved "https://registry.yarnpkg.com/jsonapi-serializer/-/jsonapi-serializer-3.6.9.tgz#a2ea0b53a24cf4bb7659232406ed8caa2423e9b2" + integrity sha512-LeRPlP93Mz6+Klu13OKcnXNLvtH1gbeo/yfThqihAMw7vUBCWWs6jHImpR/tQwzAxJi7F1+bfVJxeHoNCrbZiQ== + dependencies: + inflected "^1.1.6" + lodash "^4.16.3" + +jsonwebtoken@^9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz#6cd57ab01e9b0ac07cb847d53d3c9b6ee31f7ae2" + integrity sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g== + dependencies: + jws "^4.0.1" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + +jwa@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804" + integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg== + dependencies: + buffer-equal-constant-time "^1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.1.tgz#07edc1be8fac20e677b283ece261498bd38f0690" + integrity sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA== + dependencies: + jwa "^2.0.1" + safe-buffer "^5.0.1" + +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw== + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +lodash@^4.16.3: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +luxon@^3.2.1: + version "3.7.2" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.7.2.tgz#d697e48f478553cca187a0f8436aff468e3ba0ba" + integrity sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew== + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +media-typer@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561" + integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== + +merge-descriptors@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808" + integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== + +methods@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + +mime-types@^2.1.35: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime-types@^3.0.0, mime-types@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab" + integrity sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A== + dependencies: + mime-db "^1.54.0" + +mime@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +ms@^2.1.1, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +negotiator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" + integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== + +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-hash@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" + integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + +object-inspect@^1.13.3, object-inspect@^1.13.4: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + +oidc-token-hash@^5.0.3: + version "5.2.0" + resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz#be8a8885c7e2478d21a674e15afa31f1bcc4a61f" + integrity sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw== + +on-finished@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +openid-client@^5.7.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-5.7.1.tgz#34cace862a3e6472ed7d0a8616ef73b7fb85a9c3" + integrity sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew== + dependencies: + jose "^4.15.9" + lru-cache "^6.0.0" + object-hash "^2.2.0" + oidc-token-hash "^5.0.3" + +parseurl@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-to-regexp@^8.0.0: + version "8.4.2" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.4.2.tgz#795c420c4f7ca45c5b887366f622ee0c9852cccd" + integrity sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA== + +pkce-challenge@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/pkce-challenge/-/pkce-challenge-5.0.1.tgz#3b4446865b17b1745e9ace2016a31f48ddf6230d" + integrity sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ== + +proxy-addr@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +qs@^6.14.0, qs@^6.14.1, qs@^6.15.2: + version "6.15.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.3.tgz#76852132a58ed5c7c0ef67e4441b9bb5d6061b3b" + integrity sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A== + dependencies: + es-define-property "^1.0.1" + side-channel "^1.1.1" + +range-parser@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.3.0.tgz#d7f19be812bb62721472b45d3be219ef09572b47" + integrity sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw== + +raw-body@^3.0.0, raw-body@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.2.tgz#3e3ada5ae5568f9095d84376fd3a49b8fb000a51" + integrity sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA== + dependencies: + bytes "~3.1.2" + http-errors "~2.0.1" + iconv-lite "~0.7.0" + unpipe "~1.0.0" + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +router@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef" + integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ== + dependencies: + debug "^4.4.0" + depd "^2.0.0" + is-promise "^4.0.0" + parseurl "^1.3.3" + path-to-regexp "^8.0.0" + +safe-buffer@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^7.5.4: + version "7.8.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.5.tgz#39b646037dd50c14fb451e7e4cac58ed8b863f69" + integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA== + +send@^1.1.0, send@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/send/-/send-1.2.1.tgz#9eab743b874f3550f40a26867bf286ad60d3f3ed" + integrity sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ== + dependencies: + debug "^4.4.3" + encodeurl "^2.0.0" + escape-html "^1.0.3" + etag "^1.8.1" + fresh "^2.0.0" + http-errors "^2.0.1" + mime-types "^3.0.2" + ms "^2.1.3" + on-finished "^2.4.1" + range-parser "^1.2.1" + statuses "^2.0.2" + +serve-static@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.1.tgz#7f186a4a4e5f5b663ad7a4294ff1bf37cf0e98a9" + integrity sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw== + dependencies: + encodeurl "^2.0.0" + escape-html "^1.0.3" + parseurl "^1.3.3" + send "^1.2.0" + +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + +setprototypeof@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel-list@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.1.tgz#c2e0b5a14a540aebee3bbc6c3f8666cc9b509127" + integrity sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.4" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.1.tgz#ea02c62e05dc4bea67d4442f0fb71ee192f8e0ab" + integrity sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.4" + side-channel-list "^1.0.1" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +statuses@^2.0.1, statuses@^2.0.2, statuses@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + +superagent@^10.3.0: + version "10.3.0" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-10.3.0.tgz#ff1e39e7976b63f8084291d65f5bfbbbbd156989" + integrity sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ== + dependencies: + component-emitter "^1.3.1" + cookiejar "^2.1.4" + debug "^4.3.7" + fast-safe-stringify "^2.1.1" + form-data "^4.0.5" + formidable "^3.5.4" + methods "^1.1.2" + mime "2.6.0" + qs "^6.14.1" + +toidentifier@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +type-is@^2.0.1, type-is@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.1.0.tgz#71d1a7053293582e16ac9f3ebaf1ab9aa49e5570" + integrity sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA== + dependencies: + content-type "^2.0.0" + media-typer "^1.1.0" + mime-types "^3.0.0" + +unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +uuid@11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.1.tgz#f6d81d2e1c65d00762e5e29b16c5d2d995e208ad" + integrity sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ== + +vary@^1, vary@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +zod-to-json-schema@^3.25.1: + version "3.25.2" + resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz#3fa799a7badd554541472fb65843fdc460b2e5aa" + integrity sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA== + +"zod@^3.25 || ^4.0", zod@^4.3.5: + version "4.4.3" + resolved "https://registry.yarnpkg.com/zod/-/zod-4.4.3.tgz#b680f172885d18bbebf21a834ea25e55a1bbf356" + integrity sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ== diff --git a/packages/mcp-server/docker/smoke-test.sh b/packages/mcp-server/docker/smoke-test.sh new file mode 100755 index 0000000000..e7f7fee8d2 --- /dev/null +++ b/packages/mcp-server/docker/smoke-test.sh @@ -0,0 +1,52 @@ +#!/bin/sh +# Smoke-test a built mcp-server image: prove the entrypoint works, the full module +# graph loads (cli.js → server.ts eagerly imports every @forestadmin + external +# dep), and the server boots far enough to answer its health probe. +# Run against a locally-loaded image before it is published. +# +# Boot needs no reachable Forest server — env discovery and schema fetch both +# degrade gracefully (warn, don't throw), so the server still listens. +# +# Usage: smoke-test.sh +set -eu + +IMAGE="${1:?usage: smoke-test.sh }" +PORT=3931 +NAME="mcp-server-smoke-$$" + +cleanup() { + logs="$(docker logs "$NAME" 2>&1 || true)" + echo "$logs" + docker rm -f "$NAME" >/dev/null 2>&1 || true + + if echo "$logs" | grep -qiE "Cannot find module|MODULE_NOT_FOUND"; then + echo "::error::module resolution failure in the image" + exit 1 + fi +} +trap cleanup EXIT + +docker run -d --name "$NAME" \ + -e FOREST_ENV_SECRET="$(openssl rand -hex 32)" \ + -e FOREST_AUTH_SECRET=smoke-test \ + -e MCP_SERVER_PORT="$PORT" \ + "$IMAGE" >/dev/null + +# Health probe from inside the container (node is always present — no host curl +# or published port needed). A missing module or boot crash never reaches 200. +i=0 +until docker exec "$NAME" node -e "require('http').get('http://localhost:$PORT/health',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))" 2>/dev/null; do + i=$((i + 1)) + if [ "$i" -ge 30 ]; then + echo "::error::mcp-server did not answer /health within 30s — boot/module failure" + exit 1 + fi + sleep 1 +done + +if ! docker logs "$NAME" 2>&1 | grep -q "Forest Admin MCP Server running"; then + echo "::error::server did not log its startup marker" + exit 1 +fi + +echo "smoke test passed for $IMAGE" diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index 9ed632b95e..05f42e4fdc 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -462,6 +462,12 @@ export default class ForestMCPServer { // This is required for express-rate-limit to correctly identify clients app.set('trust proxy', 1); + // Unauthenticated liveness probe (Docker HEALTHCHECK, k8s). Registered before + // allowedMethods(['POST']) so the GET isn't rejected. + app.get('/health', (_req, res) => { + res.status(200).json({ status: 'ok' }); + }); + app.use( cors({ origin: '*', diff --git a/packages/mcp-server/test/server.test.ts b/packages/mcp-server/test/server.test.ts index 254e0de2cd..2c6ea7d41b 100644 --- a/packages/mcp-server/test/server.test.ts +++ b/packages/mcp-server/test/server.test.ts @@ -200,6 +200,13 @@ describe('ForestMCPServer Instance', () => { await shutDownHttpServer(server?.httpServer as http.Server); }); + it('should answer the unauthenticated health probe', async () => { + const response = await request(httpServer).get('/health'); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + it('should handle POST requests to /mcp', async () => { const response = await request(httpServer) .post('/mcp') From 680ecf07273df5faff34672a85d347d03b933468 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Wed, 22 Jul 2026 09:52:11 +0200 Subject: [PATCH 2/2] fix(mcp-server): smoke-test reports a boot crash immediately, not as a timeout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The health-poll swallowed `docker exec` stderr, so a container that crashed on boot (the most likely failure — an un-copied @forestadmin package) spun the full 30s and was then mislabelled "did not answer within 30s", pointing at a slow boot instead of an instant crash. Probe container liveness each iteration and fail fast with the real reason. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/mcp-server/docker/smoke-test.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/mcp-server/docker/smoke-test.sh b/packages/mcp-server/docker/smoke-test.sh index e7f7fee8d2..ef4095b71c 100755 --- a/packages/mcp-server/docker/smoke-test.sh +++ b/packages/mcp-server/docker/smoke-test.sh @@ -36,6 +36,12 @@ docker run -d --name "$NAME" \ # or published port needed). A missing module or boot crash never reaches 200. i=0 until docker exec "$NAME" node -e "require('http').get('http://localhost:$PORT/health',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))" 2>/dev/null; do + # A boot crash exits the container: report that immediately instead of waiting + # the full timeout and mislabelling an instant crash as a slow boot. + if [ "$(docker inspect -f '{{.State.Running}}' "$NAME" 2>/dev/null)" != "true" ]; then + echo "::error::container exited during boot — see logs below" + exit 1 + fi i=$((i + 1)) if [ "$i" -ge 30 ]; then echo "::error::mcp-server did not answer /health within 30s — boot/module failure"