From 1bbdd7acba099590727712a4186bdba16c6e3c5a Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Sun, 19 Apr 2026 13:08:51 -0400 Subject: [PATCH] feat: add K8s API server, orchestrator abstraction, and CI pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add apps/api/ — Hono REST API server for managing pentest scans via K8s Jobs - POST/GET /api/scans, GET /api/scans/:id, cancel, report endpoints - Bearer token auth, Temporal client integration, K8s Job builder - Dockerfile, Kustomize manifests (Deployment, Service, RBAC) - Add CLI orchestrator abstraction (docker.ts → Orchestrator interface) - DockerOrchestrator and K8sOrchestrator implementations - Backend detection via SHANNON_BACKEND env var or --backend flag - Add CI workflow: type-check + lint on PR, build+push both images on main - Switch all workflows to self-hosted runners (runners-farhoodliquor) - Add shannon-api image build to release and release-beta workflows - Add root infra/kustomization.yaml as Flux entry point - Export PipelineProgress from @shannon/worker/pipeline Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 80 +++ .github/workflows/release-beta.yml | 123 +++-- .github/workflows/release.yml | 131 +++-- .github/workflows/rollback-beta.yml | 2 +- .github/workflows/rollback.yml | 2 +- apps/api/Dockerfile | 48 ++ apps/api/package.json | 20 + apps/api/src/app.ts | 35 ++ apps/api/src/config.ts | 38 ++ apps/api/src/index.ts | 57 ++ apps/api/src/middleware/auth.ts | 34 ++ apps/api/src/middleware/error-handler.ts | 20 + apps/api/src/routes/health.ts | 33 ++ apps/api/src/routes/scans.ts | 65 +++ apps/api/src/services/job-builder.ts | 141 +++++ apps/api/src/services/job-manager.ts | 35 ++ apps/api/src/services/scan-manager.ts | 166 ++++++ apps/api/src/services/temporal-client.ts | 36 ++ apps/api/src/services/workspace-reader.ts | 71 +++ apps/api/src/types/api.ts | 47 ++ apps/api/tsconfig.json | 8 + apps/cli/package.json | 1 + apps/cli/src/backend.ts | 54 ++ apps/cli/src/commands/start.ts | 33 +- apps/cli/src/commands/status.ts | 10 +- apps/cli/src/commands/stop.ts | 7 +- apps/cli/src/commands/uninstall.ts | 7 +- apps/cli/src/commands/workspaces.ts | 27 +- apps/cli/src/docker.ts | 500 ++++++++--------- apps/cli/src/env.ts | 19 +- apps/cli/src/index.ts | 18 +- apps/cli/src/k8s.ts | 494 +++++++++++++++++ apps/cli/src/orchestrator.ts | 46 ++ apps/cli/tsdown.config.ts | 2 +- apps/worker/src/temporal/pipeline.ts | 5 +- pnpm-lock.yaml | 634 ++++++++++++++++++++++ 36 files changed, 2635 insertions(+), 414 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 apps/api/Dockerfile create mode 100644 apps/api/package.json create mode 100644 apps/api/src/app.ts create mode 100644 apps/api/src/config.ts create mode 100644 apps/api/src/index.ts create mode 100644 apps/api/src/middleware/auth.ts create mode 100644 apps/api/src/middleware/error-handler.ts create mode 100644 apps/api/src/routes/health.ts create mode 100644 apps/api/src/routes/scans.ts create mode 100644 apps/api/src/services/job-builder.ts create mode 100644 apps/api/src/services/job-manager.ts create mode 100644 apps/api/src/services/scan-manager.ts create mode 100644 apps/api/src/services/temporal-client.ts create mode 100644 apps/api/src/services/workspace-reader.ts create mode 100644 apps/api/src/types/api.ts create mode 100644 apps/api/tsconfig.json create mode 100644 apps/cli/src/backend.ts create mode 100644 apps/cli/src/k8s.ts create mode 100644 apps/cli/src/orchestrator.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a146483 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,80 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + name: Type-check & lint + runs-on: runners-farhoodliquor + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install pnpm + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 + + - name: Setup Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 24 + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Type-check + run: pnpm run check + + - name: Lint + run: pnpm biome + + build-images: + name: Build & push images + needs: check + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: runners-farhoodliquor + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + + - name: Log in to Docker Hub + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push worker image + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + with: + context: . + push: true + tags: | + keygraph/shannon:latest + keygraph/shannon:sha-${{ github.sha }} + + - name: Build and push API image + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + with: + context: . + file: apps/api/Dockerfile + push: true + tags: | + keygraph/shannon-api:latest + keygraph/shannon-api:sha-${{ github.sha }} diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index 56c3e08..07f8bf8 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -13,7 +13,7 @@ concurrency: jobs: preflight: name: Preflight - runs-on: ubuntu-latest + runs-on: runners-farhoodliquor outputs: version: ${{ steps.version.outputs.version }} @@ -45,19 +45,11 @@ jobs: run: 'echo "Next beta version: ${{ steps.version.outputs.version }}"' build-docker: - name: Build Docker (${{ matrix.platform }}) + name: Build Docker (worker) needs: preflight + runs-on: runners-farhoodliquor permissions: contents: read - strategy: - fail-fast: true - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - runs-on: ${{ matrix.runner }} steps: - name: Checkout @@ -72,47 +64,25 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push by digest - id: build + - name: Build and push worker image uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: . - platforms: ${{ matrix.platform }} + push: true provenance: mode=max sbom: true - outputs: type=image,name=keygraph/shannon,push-by-digest=true,name-canonical=true,push=true + tags: keygraph/shannon:${{ needs.preflight.outputs.version }} - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: digests-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - merge-docker: - name: Push Docker manifests - needs: [preflight, build-docker] - runs-on: ubuntu-latest + build-docker-api: + name: Build Docker (API) + needs: preflight + runs-on: runners-farhoodliquor permissions: contents: read - id-token: write - outputs: - digest: ${{ steps.inspect.outputs.digest }} steps: - - name: Download digests - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - path: /tmp/digests - pattern: digests-* - merge-multiple: true + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 @@ -123,38 +93,79 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create \ - --tag "keygraph/shannon:${{ needs.preflight.outputs.version }}" \ - $(printf 'keygraph/shannon@sha256:%s ' *) + - name: Build and push API image + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + with: + context: . + file: apps/api/Dockerfile + push: true + provenance: mode=max + sbom: true + tags: keygraph/shannon-api:${{ needs.preflight.outputs.version }} - - name: Inspect image - id: inspect + sign-docker: + name: Sign Docker images + needs: [preflight, build-docker, build-docker-api] + runs-on: runners-farhoodliquor + permissions: + contents: read + id-token: write + outputs: + worker_digest: ${{ steps.inspect-worker.outputs.digest }} + api_digest: ${{ steps.inspect-api.outputs.digest }} + + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + + - name: Log in to Docker Hub + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Inspect worker image + id: inspect-worker run: | docker buildx imagetools inspect "keygraph/shannon:${{ needs.preflight.outputs.version }}" DIGEST="sha256:$(docker buildx imagetools inspect --raw "keygraph/shannon:${{ needs.preflight.outputs.version }}" | sha256sum | cut -d' ' -f1)" echo "digest=$DIGEST" >> "$GITHUB_OUTPUT" + - name: Inspect API image + id: inspect-api + run: | + docker buildx imagetools inspect "keygraph/shannon-api:${{ needs.preflight.outputs.version }}" + DIGEST="sha256:$(docker buildx imagetools inspect --raw "keygraph/shannon-api:${{ needs.preflight.outputs.version }}" | sha256sum | cut -d' ' -f1)" + echo "digest=$DIGEST" >> "$GITHUB_OUTPUT" + - name: Install cosign uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0 - - name: Sign Docker image - run: cosign sign --yes "keygraph/shannon@${{ steps.inspect.outputs.digest }}" + - name: Sign worker image + run: cosign sign --yes "keygraph/shannon@${{ steps.inspect-worker.outputs.digest }}" - - name: Verify Docker image signature + - name: Sign API image + run: cosign sign --yes "keygraph/shannon-api@${{ steps.inspect-api.outputs.digest }}" + + - name: Verify worker image signature run: | sleep 10 cosign verify \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-identity https://github.com/${{ github.repository }}/.github/workflows/release-beta.yml@${{ github.ref }} \ - "keygraph/shannon@${{ steps.inspect.outputs.digest }}" + "keygraph/shannon@${{ steps.inspect-worker.outputs.digest }}" + + - name: Verify API image signature + run: | + cosign verify \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-identity https://github.com/${{ github.repository }}/.github/workflows/release-beta.yml@${{ github.ref }} \ + "keygraph/shannon-api@${{ steps.inspect-api.outputs.digest }}" publish-npm: name: Publish npm (beta) - needs: [preflight, merge-docker] - runs-on: ubuntu-latest + needs: [preflight, sign-docker] + runs-on: runners-farhoodliquor permissions: contents: read id-token: write diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8e5c70..d0c5525 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ concurrency: jobs: preflight: name: Preflight - runs-on: ubuntu-latest + runs-on: runners-farhoodliquor permissions: contents: write outputs: @@ -57,20 +57,12 @@ jobs: fi build-docker: - name: Build Docker (${{ matrix.platform }}) + name: Build Docker (worker) needs: preflight if: needs.preflight.outputs.should_release == 'true' + runs-on: runners-farhoodliquor permissions: contents: read - strategy: - fail-fast: true - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - runs-on: ${{ matrix.runner }} steps: - name: Checkout @@ -85,47 +77,28 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push by digest - id: build + - name: Build and push worker image uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: . - platforms: ${{ matrix.platform }} + push: true provenance: mode=max sbom: true - outputs: type=image,name=keygraph/shannon,push-by-digest=true,name-canonical=true,push=true + tags: | + keygraph/shannon:${{ needs.preflight.outputs.version }} + keygraph/shannon:latest - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: digests-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - merge-docker: - name: Push Docker manifests - needs: [preflight, build-docker] - runs-on: ubuntu-latest + build-docker-api: + name: Build Docker (API) + needs: preflight + if: needs.preflight.outputs.should_release == 'true' + runs-on: runners-farhoodliquor permissions: contents: read - id-token: write - outputs: - digest: ${{ steps.inspect.outputs.digest }} steps: - - name: Download digests - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - path: /tmp/digests - pattern: digests-* - merge-multiple: true + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 @@ -136,39 +109,81 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create \ - --tag "keygraph/shannon:${{ needs.preflight.outputs.version }}" \ - --tag "keygraph/shannon:latest" \ - $(printf 'keygraph/shannon@sha256:%s ' *) + - name: Build and push API image + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + with: + context: . + file: apps/api/Dockerfile + push: true + provenance: mode=max + sbom: true + tags: | + keygraph/shannon-api:${{ needs.preflight.outputs.version }} + keygraph/shannon-api:latest - - name: Inspect image - id: inspect + sign-docker: + name: Sign Docker images + needs: [preflight, build-docker, build-docker-api] + runs-on: runners-farhoodliquor + permissions: + contents: read + id-token: write + outputs: + worker_digest: ${{ steps.inspect-worker.outputs.digest }} + api_digest: ${{ steps.inspect-api.outputs.digest }} + + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + + - name: Log in to Docker Hub + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Inspect worker image + id: inspect-worker run: | docker buildx imagetools inspect "keygraph/shannon:${{ needs.preflight.outputs.version }}" DIGEST="sha256:$(docker buildx imagetools inspect --raw "keygraph/shannon:${{ needs.preflight.outputs.version }}" | sha256sum | cut -d' ' -f1)" echo "digest=$DIGEST" >> "$GITHUB_OUTPUT" + - name: Inspect API image + id: inspect-api + run: | + docker buildx imagetools inspect "keygraph/shannon-api:${{ needs.preflight.outputs.version }}" + DIGEST="sha256:$(docker buildx imagetools inspect --raw "keygraph/shannon-api:${{ needs.preflight.outputs.version }}" | sha256sum | cut -d' ' -f1)" + echo "digest=$DIGEST" >> "$GITHUB_OUTPUT" + - name: Install cosign uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0 - - name: Sign Docker image - run: cosign sign --yes "keygraph/shannon@${{ steps.inspect.outputs.digest }}" + - name: Sign worker image + run: cosign sign --yes "keygraph/shannon@${{ steps.inspect-worker.outputs.digest }}" - - name: Verify Docker image signature + - name: Sign API image + run: cosign sign --yes "keygraph/shannon-api@${{ steps.inspect-api.outputs.digest }}" + + - name: Verify worker image signature run: | sleep 10 cosign verify \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-identity https://github.com/${{ github.repository }}/.github/workflows/release.yml@${{ github.ref }} \ - "keygraph/shannon@${{ steps.inspect.outputs.digest }}" + "keygraph/shannon@${{ steps.inspect-worker.outputs.digest }}" + + - name: Verify API image signature + run: | + cosign verify \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --certificate-identity https://github.com/${{ github.repository }}/.github/workflows/release.yml@${{ github.ref }} \ + "keygraph/shannon-api@${{ steps.inspect-api.outputs.digest }}" publish-npm: name: Publish npm - needs: [preflight, merge-docker] - runs-on: ubuntu-latest + needs: [preflight, sign-docker] + runs-on: runners-farhoodliquor permissions: contents: read id-token: write @@ -213,7 +228,7 @@ jobs: release: name: Create GitHub release needs: [preflight, publish-npm] - runs-on: ubuntu-latest + runs-on: runners-farhoodliquor permissions: contents: write diff --git a/.github/workflows/rollback-beta.yml b/.github/workflows/rollback-beta.yml index 1255d49..34bd766 100644 --- a/.github/workflows/rollback-beta.yml +++ b/.github/workflows/rollback-beta.yml @@ -18,7 +18,7 @@ concurrency: jobs: rollback: name: Roll back npm beta dist-tag - runs-on: ubuntu-latest + runs-on: runners-farhoodliquor steps: - name: Validate target version id: target diff --git a/.github/workflows/rollback.yml b/.github/workflows/rollback.yml index 6c7645d..ceb5f8b 100644 --- a/.github/workflows/rollback.yml +++ b/.github/workflows/rollback.yml @@ -18,7 +18,7 @@ concurrency: jobs: rollback: name: Roll back npm, Docker, and GitHub release latest - runs-on: ubuntu-latest + runs-on: runners-farhoodliquor steps: - name: Checkout tags uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile new file mode 100644 index 0000000..b5fed8b --- /dev/null +++ b/apps/api/Dockerfile @@ -0,0 +1,48 @@ +# +# Shannon API Server — minimal Node.js image (no security tools) +# + +FROM node:22-alpine AS builder + +RUN npm install -g pnpm@10.33.0 + +WORKDIR /app + +# Copy workspace manifests for install layer caching +COPY package.json pnpm-workspace.yaml pnpm-lock.yaml .npmrc ./ +COPY apps/api/package.json ./apps/api/ +COPY apps/worker/package.json ./apps/worker/ +COPY apps/cli/package.json ./apps/cli/ + +RUN pnpm install --frozen-lockfile + +COPY tsconfig.base.json ./ +COPY apps/worker/ ./apps/worker/ +COPY apps/api/ ./apps/api/ + +# Build worker first (API depends on it for types), then API +RUN pnpm --filter @shannon/worker run build && pnpm --filter @shannon/api run build + +# Production-only deps +RUN rm -rf node_modules apps/*/node_modules && pnpm install --frozen-lockfile --prod + +# Runtime stage +FROM node:22-alpine + +WORKDIR /app + +COPY --from=builder /app/package.json /app/pnpm-workspace.yaml /app/pnpm-lock.yaml /app/.npmrc /app/ +COPY --from=builder /app/node_modules /app/node_modules +COPY --from=builder /app/apps/api/dist /app/apps/api/dist +COPY --from=builder /app/apps/api/package.json /app/apps/api/package.json +COPY --from=builder /app/apps/api/node_modules /app/apps/api/node_modules +COPY --from=builder /app/apps/worker/dist /app/apps/worker/dist +COPY --from=builder /app/apps/worker/package.json /app/apps/worker/package.json +COPY --from=builder /app/apps/worker/node_modules /app/apps/worker/node_modules + +RUN mkdir -p /app/workspaces + +ENV NODE_ENV=production +EXPOSE 3000 + +CMD ["node", "apps/api/dist/index.js"] diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..4c7b796 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,20 @@ +{ + "name": "@shannon/api", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "check": "tsc --noEmit", + "clean": "rm -rf dist", + "start": "node dist/index.js" + }, + "dependencies": { + "@hono/node-server": "^1.14.0", + "@kubernetes/client-node": "^1.4.0", + "@shannon/worker": "workspace:*", + "@temporalio/client": "^1.11.0", + "hono": "^4.7.0", + "zod": "^4.3.6" + } +} diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts new file mode 100644 index 0000000..d921c4b --- /dev/null +++ b/apps/api/src/app.ts @@ -0,0 +1,35 @@ +/** + * Hono app factory. + * Creates the app with middleware and routes. Deps injected for testability. + */ + +import type * as k8s from '@kubernetes/client-node'; +import type { Client } from '@temporalio/client'; +import { Hono } from 'hono'; +import type { Config } from './config.js'; +import { authMiddleware } from './middleware/auth.js'; +import { errorHandler } from './middleware/error-handler.js'; +import { healthRoutes } from './routes/health.js'; +import { scanRoutes } from './routes/scans.js'; + +export interface AppDeps { + readonly temporalClient: Client; + readonly batchApi: k8s.BatchV1Api; + readonly coreApi: k8s.CoreV1Api; +} + +export function createApp(config: Config, deps: AppDeps): Hono { + const app = new Hono(); + + // Global error handler + app.onError(errorHandler); + + // Auth middleware (skips /healthz and /readyz) + app.use('*', authMiddleware(config.apiKey)); + + // Routes + app.route('/', healthRoutes(deps)); + app.route('/api/scans', scanRoutes(config, deps)); + + return app; +} diff --git a/apps/api/src/config.ts b/apps/api/src/config.ts new file mode 100644 index 0000000..e730354 --- /dev/null +++ b/apps/api/src/config.ts @@ -0,0 +1,38 @@ +/** + * Environment-driven configuration for the API server. + * Parsed once at startup — missing required values cause a hard exit. + */ + +export interface Config { + readonly port: number; + readonly temporalAddress: string; + readonly apiKey: string; + readonly k8sNamespace: string; + readonly workerImage: string; + readonly workspacesDir: string; + readonly credentialsSecretName: string; +} + +export function loadConfig(): Config { + const apiKey = process.env.API_KEY; + if (!apiKey) { + console.error('ERROR: API_KEY environment variable is required'); + process.exit(1); + } + + const workerImage = process.env.WORKER_IMAGE; + if (!workerImage) { + console.error('ERROR: WORKER_IMAGE environment variable is required'); + process.exit(1); + } + + return { + port: Number(process.env.PORT) || 3000, + temporalAddress: process.env.TEMPORAL_ADDRESS || 'shannon-temporal:7233', + apiKey, + k8sNamespace: process.env.K8S_NAMESPACE || 'shannon', + workerImage, + workspacesDir: process.env.WORKSPACES_DIR || '/app/workspaces', + credentialsSecretName: process.env.CREDENTIALS_SECRET_NAME || 'shannon-credentials', + }; +} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts new file mode 100644 index 0000000..e75044c --- /dev/null +++ b/apps/api/src/index.ts @@ -0,0 +1,57 @@ +/** + * Shannon API Server — entry point. + * Connects to Temporal, initializes K8s client, starts the Hono server. + */ + +import { serve } from '@hono/node-server'; +import * as k8s from '@kubernetes/client-node'; +import { createApp } from './app.js'; +import { loadConfig } from './config.js'; +import { connectTemporal, disconnectTemporal } from './services/temporal-client.js'; + +async function main(): Promise { + // 1. Load configuration + const config = loadConfig(); + + // 2. Connect to Temporal + const temporal = await connectTemporal(config.temporalAddress); + + // 3. Initialize K8s client (in-cluster or from kubeconfig) + const kc = new k8s.KubeConfig(); + try { + kc.loadFromCluster(); + } catch { + // Fallback to default kubeconfig (for local development) + kc.loadFromDefault(); + } + const batchApi = kc.makeApiClient(k8s.BatchV1Api); + const coreApi = kc.makeApiClient(k8s.CoreV1Api); + + // 4. Create app + const app = createApp(config, { + temporalClient: temporal.client, + batchApi, + coreApi, + }); + + // 5. Start server + const server = serve({ fetch: app.fetch, port: config.port }, (info) => { + console.log(`Shannon API server listening on port ${info.port}`); + }); + + // 6. Graceful shutdown + const shutdown = async (): Promise => { + console.log('Shutting down...'); + server.close(); + await disconnectTemporal(temporal); + process.exit(0); + }; + + process.on('SIGTERM', shutdown); + process.on('SIGINT', shutdown); +} + +main().catch((err) => { + console.error('Failed to start API server:', err); + process.exit(1); +}); diff --git a/apps/api/src/middleware/auth.ts b/apps/api/src/middleware/auth.ts new file mode 100644 index 0000000..f6957f8 --- /dev/null +++ b/apps/api/src/middleware/auth.ts @@ -0,0 +1,34 @@ +/** + * Bearer token authentication middleware. + * Validates the Authorization header against the configured API key. + * Skips health check endpoints. + */ + +import crypto from 'node:crypto'; +import type { Context, Next } from 'hono'; + +const PUBLIC_PATHS = new Set(['/healthz', '/readyz']); + +export function authMiddleware(apiKey: string) { + const expectedBuffer = Buffer.from(apiKey); + + return async (c: Context, next: Next) => { + if (PUBLIC_PATHS.has(c.req.path)) { + return next(); + } + + const header = c.req.header('Authorization'); + if (!header?.startsWith('Bearer ')) { + return c.json({ error: 'Missing or invalid Authorization header' }, 401); + } + + const token = header.slice(7); + const tokenBuffer = Buffer.from(token); + + if (tokenBuffer.length !== expectedBuffer.length || !crypto.timingSafeEqual(tokenBuffer, expectedBuffer)) { + return c.json({ error: 'Invalid API key' }, 401); + } + + return next(); + }; +} diff --git a/apps/api/src/middleware/error-handler.ts b/apps/api/src/middleware/error-handler.ts new file mode 100644 index 0000000..783298f --- /dev/null +++ b/apps/api/src/middleware/error-handler.ts @@ -0,0 +1,20 @@ +/** + * Global error handler middleware. + * Catches unhandled errors and returns structured JSON responses. + */ + +import type { Context } from 'hono'; + +export function errorHandler(err: Error, c: Context): Response { + console.error('Unhandled error:', err); + + const status = 'statusCode' in err && typeof err.statusCode === 'number' ? err.statusCode : 500; + + return c.json( + { + error: status === 500 ? 'Internal server error' : err.message, + code: err.name || 'UNKNOWN_ERROR', + }, + status as 500, + ); +} diff --git a/apps/api/src/routes/health.ts b/apps/api/src/routes/health.ts new file mode 100644 index 0000000..4df51db --- /dev/null +++ b/apps/api/src/routes/health.ts @@ -0,0 +1,33 @@ +/** + * Health and readiness endpoints. + * /healthz — always 200 (server is running) + * /readyz — checks Temporal connectivity + */ + +import { Hono } from 'hono'; +import type { AppDeps } from '../app.js'; + +export function healthRoutes(deps: AppDeps): Hono { + const app = new Hono(); + + app.get('/healthz', (c) => { + return c.json({ status: 'ok' }); + }); + + app.get('/readyz', async (c) => { + try { + // Lightweight Temporal connectivity check — list with a filter that matches nothing + const iter = deps.temporalClient.workflow.list({ query: 'ExecutionStatus = "Running"' }); + // Consume iterator to trigger the gRPC call, then break immediately + for await (const _ of iter) { + break; + } + return c.json({ status: 'ok' }); + } catch (err) { + const message = err instanceof Error ? err.message : 'Unknown error'; + return c.json({ status: 'error', error: `Temporal unreachable: ${message}` }, 503); + } + }); + + return app; +} diff --git a/apps/api/src/routes/scans.ts b/apps/api/src/routes/scans.ts new file mode 100644 index 0000000..0ce4464 --- /dev/null +++ b/apps/api/src/routes/scans.ts @@ -0,0 +1,65 @@ +/** + * Scan CRUD routes — POST/GET /api/scans, GET/POST /api/scans/:id/* + */ + +import { Hono } from 'hono'; +import type { AppDeps } from '../app.js'; +import type { Config } from '../config.js'; +import { cancelScan, getReport, getScan, listScans, startScan } from '../services/scan-manager.js'; +import { CreateScanSchema } from '../types/api.js'; + +export function scanRoutes(config: Config, deps: AppDeps): Hono { + const app = new Hono(); + + // POST /api/scans — start a new scan + app.post('/', async (c) => { + const body = await c.req.json(); + const parsed = CreateScanSchema.safeParse(body); + + if (!parsed.success) { + return c.json({ error: 'Validation failed', details: parsed.error.issues }, 400); + } + + const result = await startScan(config, deps.batchApi, parsed.data); + return c.json(result, 201); + }); + + // GET /api/scans — list all scans + app.get('/', async (c) => { + const scans = await listScans(config, deps.temporalClient, deps.batchApi); + return c.json({ scans }); + }); + + // GET /api/scans/:id — get scan status/progress + app.get('/:id', async (c) => { + const scanId = c.req.param('id'); + const result = await getScan(config, deps.temporalClient, scanId); + + if (!result) { + return c.json({ error: 'Scan not found' }, 404); + } + + return c.json(result); + }); + + // POST /api/scans/:id/cancel — cancel a running scan + app.post('/:id/cancel', async (c) => { + const scanId = c.req.param('id'); + await cancelScan(config, deps.temporalClient, deps.batchApi, scanId); + return c.json({ status: 'cancelled' }); + }); + + // GET /api/scans/:id/report — get the scan report + app.get('/:id/report', async (c) => { + const scanId = c.req.param('id'); + const report = await getReport(config, scanId); + + if (!report) { + return c.json({ error: 'Report not found' }, 404); + } + + return c.text(report); + }); + + return app; +} diff --git a/apps/api/src/services/job-builder.ts b/apps/api/src/services/job-builder.ts new file mode 100644 index 0000000..38b71d5 --- /dev/null +++ b/apps/api/src/services/job-builder.ts @@ -0,0 +1,141 @@ +/** + * K8s Job spec builder for worker scan Jobs. + * Constructs a Job that runs the Shannon worker image with the correct + * volumes, env, and security context. Optionally includes a git clone init container. + */ + +import type * as k8s from '@kubernetes/client-node'; + +export interface JobParams { + readonly jobName: string; + readonly namespace: string; + readonly workerImage: string; + readonly targetUrl: string; + readonly taskQueue: string; + readonly workspace: string; + readonly credentialsSecretName: string; + readonly gitUrl?: string; + readonly gitRef?: string; + readonly repoPath?: string; + readonly configYaml?: string; + readonly pipelineTesting?: boolean; +} + +const WORKER_LABEL = 'shannon-worker'; +const REPO_MOUNT_PATH = '/repo'; + +export function buildJobSpec(params: JobParams): k8s.V1Job { + const repoPath = params.repoPath ?? REPO_MOUNT_PATH; + + // 1. Build worker command + const command = ['node', 'apps/worker/dist/temporal/worker.js', params.targetUrl, repoPath]; + const args: string[] = ['--task-queue', params.taskQueue, '--workspace', params.workspace]; + if (params.pipelineTesting) { + args.push('--pipeline-testing'); + } + + // 2. Build volumes and mounts + const volumes: k8s.V1Volume[] = [ + { name: 'workspaces', persistentVolumeClaim: { claimName: 'shannon-workspaces' } }, + { name: 'shm', emptyDir: { medium: 'Memory', sizeLimit: '2Gi' } }, + ]; + + const volumeMounts: k8s.V1VolumeMount[] = [ + { name: 'workspaces', mountPath: '/app/workspaces' }, + { name: 'shm', mountPath: '/dev/shm' }, + ]; + + // Overlay dirs (writable areas over the read-only repo) + for (const overlay of ['deliverables', 'scratchpad', 'playwright-cli']) { + const volName = `overlay-${overlay}`; + volumes.push({ name: volName, emptyDir: {} }); + volumeMounts.push({ + name: volName, + mountPath: `${repoPath}/.shannon/${overlay === 'playwright-cli' ? '.playwright-cli' : overlay}`, + }); + } + + // 3. Repo volume — emptyDir for git clone, or PVC sub-path for pre-staged repos + const initContainers: k8s.V1Container[] = []; + + if (params.gitUrl) { + // Git clone into an emptyDir + volumes.push({ name: 'repo', emptyDir: {} }); + volumeMounts.push({ name: 'repo', mountPath: REPO_MOUNT_PATH, readOnly: true }); + + const cloneArgs = ['clone', '--depth', '1']; + if (params.gitRef) { + cloneArgs.push('--branch', params.gitRef); + } + cloneArgs.push(params.gitUrl, REPO_MOUNT_PATH); + + initContainers.push({ + name: 'git-clone', + image: 'bitnami/git:2', + command: ['git'], + args: cloneArgs, + volumeMounts: [{ name: 'repo', mountPath: REPO_MOUNT_PATH }], + }); + } else if (params.repoPath) { + // Repo already on a PVC — mount the workspaces PVC (assumes repo is staged there) + volumeMounts.push({ + name: 'workspaces', + mountPath: repoPath, + readOnly: true, + subPath: `repos/${params.workspace}`, + }); + } + + // 4. Env vars + const env: k8s.V1EnvVar[] = [{ name: 'TEMPORAL_ADDRESS', value: 'shannon-temporal:7233' }]; + + // 5. Construct the Job + return { + apiVersion: 'batch/v1', + kind: 'Job', + metadata: { + name: params.jobName, + namespace: params.namespace, + labels: { + app: WORKER_LABEL, + 'shannon.io/workspace': params.workspace, + 'shannon.io/scan-id': params.jobName, + }, + }, + spec: { + backoffLimit: 0, + ttlSecondsAfterFinished: 3600, + template: { + metadata: { + labels: { + app: WORKER_LABEL, + 'shannon.io/workspace': params.workspace, + }, + }, + spec: { + restartPolicy: 'Never', + serviceAccountName: 'default', + securityContext: { + seccompProfile: { type: 'Unconfined' }, + }, + ...(initContainers.length > 0 && { initContainers }), + containers: [ + { + name: 'worker', + image: params.workerImage, + command, + args, + env, + envFrom: [{ secretRef: { name: params.credentialsSecretName } }], + volumeMounts, + resources: { + requests: { memory: '2Gi' }, + }, + }, + ], + volumes, + }, + }, + }, + }; +} diff --git a/apps/api/src/services/job-manager.ts b/apps/api/src/services/job-manager.ts new file mode 100644 index 0000000..470841f --- /dev/null +++ b/apps/api/src/services/job-manager.ts @@ -0,0 +1,35 @@ +/** + * K8s Job lifecycle management — create, delete, list worker Jobs. + */ + +import type * as k8s from '@kubernetes/client-node'; + +const WORKER_LABEL = 'shannon-worker'; + +export async function createJob(batchApi: k8s.BatchV1Api, namespace: string, job: k8s.V1Job): Promise { + await batchApi.createNamespacedJob({ namespace, body: job }); +} + +export async function deleteJob(batchApi: k8s.BatchV1Api, namespace: string, name: string): Promise { + await batchApi.deleteNamespacedJob({ + name, + namespace, + propagationPolicy: 'Background', + }); +} + +export async function getJob(batchApi: k8s.BatchV1Api, namespace: string, name: string): Promise { + try { + return await batchApi.readNamespacedJob({ name, namespace }); + } catch { + return null; + } +} + +export async function listWorkerJobs(batchApi: k8s.BatchV1Api, namespace: string): Promise { + const response = await batchApi.listNamespacedJob({ + namespace, + labelSelector: `app=${WORKER_LABEL}`, + }); + return response.items; +} diff --git a/apps/api/src/services/scan-manager.ts b/apps/api/src/services/scan-manager.ts new file mode 100644 index 0000000..504cd76 --- /dev/null +++ b/apps/api/src/services/scan-manager.ts @@ -0,0 +1,166 @@ +/** + * Scan lifecycle orchestration — combines Temporal queries with K8s Job management. + * This is the main service that route handlers delegate to. + */ + +import crypto from 'node:crypto'; +import type * as k8s from '@kubernetes/client-node'; +import type { Client } from '@temporalio/client'; +import type { Config } from '../config.js'; +import type { CreateScanInput, ScanResponse } from '../types/api.js'; +import { buildJobSpec } from './job-builder.js'; +import { createJob, deleteJob, listWorkerJobs } from './job-manager.js'; +import { cancelWorkflow, queryProgress } from './temporal-client.js'; +import { listWorkspaces, readReport, readSessionJson } from './workspace-reader.js'; + +function randomSuffix(): string { + return crypto.randomBytes(4).toString('hex'); +} + +// === Start Scan === + +export async function startScan( + config: Config, + batchApi: k8s.BatchV1Api, + input: CreateScanInput, +): Promise { + const suffix = randomSuffix(); + const taskQueue = `api-${suffix}`; + const jobName = `shannon-worker-${suffix}`; + + const workspace = + input.workspace ?? `${new URL(input.targetUrl).hostname.replace(/[^a-zA-Z0-9-]/g, '-')}_shannon-${Date.now()}`; + + const job = buildJobSpec({ + jobName, + namespace: config.k8sNamespace, + workerImage: config.workerImage, + targetUrl: input.targetUrl, + taskQueue, + workspace, + credentialsSecretName: config.credentialsSecretName, + ...(input.gitUrl && { gitUrl: input.gitUrl }), + ...(input.gitRef && { gitRef: input.gitRef }), + ...(input.repoPath && { repoPath: input.repoPath }), + ...(input.configYaml && { configYaml: input.configYaml }), + ...(input.pipelineTesting && { pipelineTesting: true }), + }); + + await createJob(batchApi, config.k8sNamespace, job); + + return { + id: jobName, + workspace, + targetUrl: input.targetUrl, + status: 'running', + createdAt: new Date().toISOString(), + }; +} + +// === Get Scan === + +export async function getScan(config: Config, temporalClient: Client, scanId: string): Promise { + // 1. Try Temporal query for live progress + try { + const progress = await queryProgress(temporalClient, scanId); + return { + id: scanId, + workspace: scanId, + targetUrl: '', + status: progress.status, + createdAt: new Date(progress.startTime).toISOString(), + completedAgents: progress.completedAgents, + agentMetrics: progress.agentMetrics, + ...(progress.currentPhase && { currentPhase: progress.currentPhase }), + ...(progress.currentAgent && { currentAgent: progress.currentAgent }), + ...(progress.summary && { summary: progress.summary }), + ...(progress.error && { error: progress.error }), + }; + } catch { + // Workflow not found in Temporal — try workspace session.json + } + + // 2. Fall back to workspace session.json (completed/historical scans) + const session = readSessionJson(config.workspacesDir, scanId); + if (!session) return null; + + return { + id: session.originalWorkflowId ?? scanId, + workspace: session.workspace, + targetUrl: session.webUrl ?? '', + status: 'completed', + createdAt: session.startTime ? new Date(session.startTime).toISOString() : '', + }; +} + +// === List Scans === + +export async function listScans( + config: Config, + _temporalClient: Client, + batchApi: k8s.BatchV1Api, +): Promise { + const results: ScanResponse[] = []; + + // 1. Running scans from K8s Jobs + const jobs = await listWorkerJobs(batchApi, config.k8sNamespace); + for (const job of jobs) { + const jobName = job.metadata?.name ?? ''; + const workspace = job.metadata?.labels?.['shannon.io/workspace'] ?? jobName; + const startTime = job.status?.startTime; + + results.push({ + id: jobName, + workspace, + targetUrl: '', + status: job.status?.succeeded ? 'completed' : job.status?.failed ? 'failed' : 'running', + createdAt: startTime ? new Date(startTime).toISOString() : '', + }); + } + + // 2. Historical scans from workspace session.json files + const workspaces = listWorkspaces(config.workspacesDir); + const jobNames = new Set(results.map((r) => r.workspace)); + + for (const ws of workspaces) { + if (jobNames.has(ws.workspace)) continue; + results.push({ + id: ws.originalWorkflowId ?? ws.workspace, + workspace: ws.workspace, + targetUrl: ws.webUrl ?? '', + status: 'completed', + createdAt: ws.startTime ? new Date(ws.startTime).toISOString() : '', + }); + } + + return results; +} + +// === Cancel Scan === + +export async function cancelScan( + config: Config, + temporalClient: Client, + batchApi: k8s.BatchV1Api, + scanId: string, +): Promise { + // Cancel Temporal workflow (best-effort) + try { + await cancelWorkflow(temporalClient, scanId); + } catch { + // Workflow may have already completed + } + + // Delete K8s Job + try { + await deleteJob(batchApi, config.k8sNamespace, scanId); + } catch { + // Job may have already been cleaned up + } +} + +// === Get Report === + +export async function getReport(config: Config, scanId: string): Promise { + return readReport(config.workspacesDir, scanId); +} diff --git a/apps/api/src/services/temporal-client.ts b/apps/api/src/services/temporal-client.ts new file mode 100644 index 0000000..e0494ae --- /dev/null +++ b/apps/api/src/services/temporal-client.ts @@ -0,0 +1,36 @@ +/** + * Temporal client management — connection lifecycle and workflow operations. + * Uses @temporalio/client (not worker) since the API server only submits and queries workflows. + */ + +import type { PipelineProgress } from '@shannon/worker/pipeline'; +import { Client, Connection } from '@temporalio/client'; + +export interface TemporalClients { + readonly client: Client; + readonly connection: Connection; +} + +export async function connectTemporal(address: string): Promise { + console.log(`Connecting to Temporal at ${address}...`); + const connection = await Connection.connect({ address }); + const client = new Client({ connection }); + console.log('Temporal connected.'); + return { client, connection }; +} + +export async function disconnectTemporal(clients: TemporalClients): Promise { + await clients.connection.close(); +} + +/** Query a workflow's progress via the getProgress query. */ +export async function queryProgress(client: Client, workflowId: string): Promise { + const handle = client.workflow.getHandle(workflowId); + return handle.query('getProgress'); +} + +/** Cancel a running workflow. */ +export async function cancelWorkflow(client: Client, workflowId: string): Promise { + const handle = client.workflow.getHandle(workflowId); + await handle.cancel(); +} diff --git a/apps/api/src/services/workspace-reader.ts b/apps/api/src/services/workspace-reader.ts new file mode 100644 index 0000000..1df51eb --- /dev/null +++ b/apps/api/src/services/workspace-reader.ts @@ -0,0 +1,71 @@ +/** + * Workspace reader — reads session.json and deliverables from the shared workspaces PVC. + */ + +import fs from 'node:fs'; +import path from 'node:path'; + +export interface SessionInfo { + readonly workspace: string; + readonly originalWorkflowId?: string; + readonly webUrl?: string; + readonly startTime?: number; + readonly cost?: number; + readonly resumeAttempts?: readonly { workflowId: string; timestamp: number }[]; +} + +export function readSessionJson(workspacesDir: string, workspace: string): SessionInfo | null { + const sessionPath = path.join(workspacesDir, workspace, 'session.json'); + try { + const raw = fs.readFileSync(sessionPath, 'utf-8'); + const data = JSON.parse(raw) as Record; + const session = data.session as Record | undefined; + const originalWorkflowId = session?.originalWorkflowId as string | undefined; + const webUrl = session?.webUrl as string | undefined; + const startTime = session?.startTime as number | undefined; + const cost = session?.totalCostUsd as number | undefined; + const resumeAttempts = session?.resumeAttempts as SessionInfo['resumeAttempts']; + + return { + workspace, + ...(originalWorkflowId && { originalWorkflowId }), + ...(webUrl && { webUrl }), + ...(startTime && { startTime }), + ...(cost && { cost }), + ...(resumeAttempts && { resumeAttempts }), + }; + } catch { + return null; + } +} + +export function readReport(workspacesDir: string, workspace: string): string | null { + const delivDir = path.join(workspacesDir, workspace, 'deliverables'); + try { + const files = fs.readdirSync(delivDir); + const reportFile = files.find((f) => f.includes('report') && f.endsWith('.md')); + if (!reportFile) return null; + return fs.readFileSync(path.join(delivDir, reportFile), 'utf-8'); + } catch { + return null; + } +} + +export function listWorkspaces(workspacesDir: string): SessionInfo[] { + try { + const entries = fs.readdirSync(workspacesDir, { withFileTypes: true }); + const results: SessionInfo[] = []; + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const session = readSessionJson(workspacesDir, entry.name); + if (session) { + results.push(session); + } + } + + return results.sort((a, b) => (b.startTime ?? 0) - (a.startTime ?? 0)); + } catch { + return []; + } +} diff --git a/apps/api/src/types/api.ts b/apps/api/src/types/api.ts new file mode 100644 index 0000000..13384fb --- /dev/null +++ b/apps/api/src/types/api.ts @@ -0,0 +1,47 @@ +/** + * Request/response types and Zod validation schemas for the scan API. + */ + +import type { AgentMetrics, PipelineSummary } from '@shannon/worker/pipeline'; +import { z } from 'zod'; + +// === Request Schemas === + +export const CreateScanSchema = z + .object({ + targetUrl: z.string().url(), + gitUrl: z.string().url().optional(), + repoPath: z.string().optional(), + gitRef: z.string().optional(), + configYaml: z.string().optional(), + workspace: z + .string() + .regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/) + .optional(), + pipelineTesting: z.boolean().optional(), + }) + .refine((data) => data.gitUrl || data.repoPath, { + message: 'Either gitUrl or repoPath is required', + }); + +export type CreateScanInput = z.infer; + +// === Response Types === + +export interface ScanResponse { + id: string; + workspace: string; + targetUrl: string; + status: 'running' | 'completed' | 'failed' | 'cancelled'; + createdAt: string; + currentPhase?: string; + currentAgent?: string; + completedAgents?: string[]; + agentMetrics?: Record; + summary?: PipelineSummary; + error?: string; +} + +export interface ScanListResponse { + scans: ScanResponse[]; +} diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..051d08e --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src"] +} diff --git a/apps/cli/package.json b/apps/cli/package.json index 63ee6a7..e423fdf 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@clack/prompts": "^1.1.0", + "@kubernetes/client-node": "^1.4.0", "chokidar": "^5.0.0", "dotenv": "^17.3.1", "smol-toml": "^1.6.1" diff --git a/apps/cli/src/backend.ts b/apps/cli/src/backend.ts new file mode 100644 index 0000000..839769d --- /dev/null +++ b/apps/cli/src/backend.ts @@ -0,0 +1,54 @@ +/** + * Backend detection — Docker (default) vs Kubernetes. + * + * Orthogonal to the local/npx mode axis. Mode controls where state lives + * and where the image comes from. Backend controls how containers are orchestrated. + */ + +import type { Orchestrator } from './orchestrator.js'; + +export type Backend = 'docker' | 'k8s'; + +let cachedBackend: Backend | undefined; +let cachedOrchestrator: Orchestrator | undefined; + +/** + * Detect the orchestration backend. + * SHANNON_BACKEND env var takes precedence, otherwise defaults to docker. + */ +export function getBackend(): Backend { + if (cachedBackend !== undefined) return cachedBackend; + + const env = process.env.SHANNON_BACKEND; + if (env === 'k8s' || env === 'kubernetes') { + cachedBackend = 'k8s'; + } else { + cachedBackend = 'docker'; + } + return cachedBackend; +} + +export function setBackend(backend: Backend): void { + cachedBackend = backend; + cachedOrchestrator = undefined; +} + +/** + * Get the orchestrator for the current backend. + * Lazy-loads the implementation to avoid importing unused dependencies. + */ +export async function getOrchestrator(): Promise { + if (cachedOrchestrator) return cachedOrchestrator; + + let orchestrator: Orchestrator; + if (getBackend() === 'k8s') { + const { K8sOrchestrator } = await import('./k8s.js'); + orchestrator = new K8sOrchestrator(); + } else { + const { DockerOrchestrator } = await import('./docker.js'); + orchestrator = new DockerOrchestrator(); + } + + cachedOrchestrator = orchestrator; + return orchestrator; +} diff --git a/apps/cli/src/commands/start.ts b/apps/cli/src/commands/start.ts index 22e74d3..5f9404d 100644 --- a/apps/cli/src/commands/start.ts +++ b/apps/cli/src/commands/start.ts @@ -5,11 +5,11 @@ * and npx mode (Docker Hub pull, ~/.shannon/). */ -import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import { ensureImage, ensureInfra, randomSuffix, spawnWorker } from '../docker.js'; +import { getOrchestrator } from '../backend.js'; +import { randomSuffix } from '../docker.js'; import { buildEnvFlags, isRouterConfigured, loadEnv, validateCredentials } from '../env.js'; import { getCredentialsPath, getWorkspacesDir, initHome } from '../home.js'; import { isLocal } from '../mode.js'; @@ -55,9 +55,10 @@ export async function start(args: StartArgs): Promise { process.env.ANTHROPIC_AUTH_TOKEN = 'shannon-router-key'; } - // 6. Ensure image (auto-build in dev, pull in npx) and start infra - ensureImage(args.version); - await ensureInfra(useRouter); + // 6. Ensure image and start infra via orchestrator + const orchestrator = await getOrchestrator(); + orchestrator.ensureImage(args.version); + await orchestrator.ensureInfra(useRouter); // 7. Generate unique task queue and container name const suffix = randomSuffix(); @@ -94,20 +95,20 @@ export async function start(args: StartArgs): Promise { process.env.GOOGLE_APPLICATION_CREDENTIALS = '/app/credentials/google-sa-key.json'; } - // 10. Resolve output directory + // 11. Resolve output directory const outputDir = args.output ? path.resolve(args.output) : undefined; if (outputDir) { fs.mkdirSync(outputDir, { recursive: true }); } - // 11. Resolve prompts directory (local mode only) + // 12. Resolve prompts directory (local mode only) const promptsDir = isLocal() ? path.resolve('apps/worker/prompts') : undefined; - // 12. Display splash screen + // 13. Display splash screen displaySplash(isLocal() ? undefined : args.version); - // 13. Spawn worker container - const proc = spawnWorker({ + // 14. Spawn worker via orchestrator + const handle = orchestrator.spawnWorker({ version: args.version, url: args.url, repo, @@ -123,8 +124,8 @@ export async function start(args: StartArgs): Promise { ...(args.pipelineTesting && { pipelineTesting: true }), }); - // 14. Wait for workflow to register, then display info - proc.on('error', (err) => { + // 15. Wait for workflow to register, then display info + handle.onError((err) => { console.error(`Failed to start worker: ${err.message}`); process.exit(1); }); @@ -181,18 +182,14 @@ export async function start(args: StartArgs): Promise { process.stdout.write('.'); }, 2000); - // Stop the worker container only if it hasn't started yet + // Stop the worker only if it hasn't started yet let cleaned = false; const cleanup = (): void => { if (cleaned || started) return; cleaned = true; clearInterval(pollInterval); console.log(`\nStopping worker ${containerName}...`); - try { - execFileSync('docker', ['stop', containerName], { stdio: 'pipe' }); - } catch { - // Container may have already exited - } + handle.kill(); }; process.on('SIGINT', () => { diff --git a/apps/cli/src/commands/status.ts b/apps/cli/src/commands/status.ts index 724ae49..ac8031a 100644 --- a/apps/cli/src/commands/status.ts +++ b/apps/cli/src/commands/status.ts @@ -2,11 +2,13 @@ * `shannon status` command — show running workers and Temporal health. */ -import { isTemporalReady, listRunningWorkers } from '../docker.js'; +import { getOrchestrator } from '../backend.js'; + +export async function status(): Promise { + const orchestrator = await getOrchestrator(); -export function status(): void { // 1. Temporal health - const temporalUp = isTemporalReady(); + const temporalUp = orchestrator.isTemporalReady(); console.log(`Temporal: ${temporalUp ? 'running' : 'not running'}`); if (temporalUp) { console.log(' Web UI: http://localhost:8233'); @@ -14,7 +16,7 @@ export function status(): void { console.log(''); // 2. Running workers - const workers = listRunningWorkers(); + const workers = orchestrator.listRunningWorkers(); if (workers) { console.log('Workers:'); console.log(workers); diff --git a/apps/cli/src/commands/stop.ts b/apps/cli/src/commands/stop.ts index f123013..1fdcf97 100644 --- a/apps/cli/src/commands/stop.ts +++ b/apps/cli/src/commands/stop.ts @@ -3,7 +3,7 @@ */ import * as p from '@clack/prompts'; -import { stopInfra, stopWorkers } from '../docker.js'; +import { getOrchestrator } from '../backend.js'; export async function stop(clean: boolean): Promise { if (clean) { @@ -16,6 +16,7 @@ export async function stop(clean: boolean): Promise { } } - stopWorkers(); - stopInfra(clean); + const orchestrator = await getOrchestrator(); + orchestrator.stopWorkers(); + orchestrator.stopInfra(clean); } diff --git a/apps/cli/src/commands/uninstall.ts b/apps/cli/src/commands/uninstall.ts index e65564e..bc10628 100644 --- a/apps/cli/src/commands/uninstall.ts +++ b/apps/cli/src/commands/uninstall.ts @@ -6,7 +6,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import * as p from '@clack/prompts'; -import { stopInfra, stopWorkers } from '../docker.js'; +import { getOrchestrator } from '../backend.js'; const SHANNON_HOME = path.join(os.homedir(), '.shannon'); @@ -28,8 +28,9 @@ export async function uninstall(): Promise { } // Stop any running containers first - stopWorkers(); - stopInfra(false); + const orchestrator = await getOrchestrator(); + orchestrator.stopWorkers(); + orchestrator.stopInfra(false); fs.rmSync(SHANNON_HOME, { recursive: true, force: true }); p.log.success('All Shannon data has been removed.'); diff --git a/apps/cli/src/commands/workspaces.ts b/apps/cli/src/commands/workspaces.ts index 3a9aa33..398e993 100644 --- a/apps/cli/src/commands/workspaces.ts +++ b/apps/cli/src/commands/workspaces.ts @@ -2,30 +2,19 @@ * `shannon workspaces` command — list all workspaces. */ -import { execFileSync } from 'node:child_process'; -import os from 'node:os'; -import { getWorkerImage } from '../docker.js'; +import { getOrchestrator } from '../backend.js'; import { getWorkspacesDir } from '../home.js'; -export function workspaces(version: string): void { +export async function workspaces(version: string): Promise { + const orchestrator = await getOrchestrator(); const workspacesDir = getWorkspacesDir(); - const image = getWorkerImage(version); + const image = orchestrator.getWorkerImage(version); try { - execFileSync( - 'docker', - [ - 'run', - '--rm', - '-v', - `${workspacesDir}:/app/workspaces`, - '-e', - 'WORKSPACES_DIR=/app/workspaces', - image, - 'node', - 'apps/worker/dist/temporal/workspaces.js', - ], - { stdio: 'inherit', ...(os.platform() === 'win32' && { env: { ...process.env, MSYS_NO_PATHCONV: '1' } }) }, + orchestrator.runEphemeral( + image, + ['node', 'apps/worker/dist/temporal/workspaces.js'], + [`${workspacesDir}:/app/workspaces`], ); } catch { console.error('ERROR: Failed to list workspaces. Is the Docker image available?'); diff --git a/apps/cli/src/docker.ts b/apps/cli/src/docker.ts index 9080f64..18eb270 100644 --- a/apps/cli/src/docker.ts +++ b/apps/cli/src/docker.ts @@ -12,27 +12,22 @@ import path from 'node:path'; import { setTimeout as sleep } from 'node:timers/promises'; import { fileURLToPath } from 'node:url'; import { getMode } from './mode.js'; +import type { Orchestrator, WorkerHandle, WorkerOptions } from './orchestrator.js'; + +export type { WorkerOptions }; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const NPX_IMAGE_REPO = 'keygraph/shannon'; const DEV_IMAGE = 'shannon-worker'; -export function getWorkerImage(version: string): string { - return getMode() === 'local' ? DEV_IMAGE : `${NPX_IMAGE_REPO}:${version}`; -} - -function getComposeFile(): string { - return getMode() === 'local' - ? path.resolve('docker-compose.yml') - : path.resolve(__dirname, '..', 'infra', 'compose.yml'); -} - /** Generate an 8-char random hex suffix for container/queue names. */ export function randomSuffix(): string { return crypto.randomBytes(4).toString('hex'); } +// === Internal Helpers === + /** Run a command silently, return true if it succeeds. */ function runQuiet(cmd: string, args: string[]): boolean { try { @@ -52,21 +47,10 @@ function runOutput(cmd: string, args: string[]): string { } } -/** - * Check if Temporal is running and healthy. - */ -export function isTemporalReady(): boolean { - const output = runOutput('docker', [ - 'exec', - 'shannon-temporal', - 'temporal', - 'operator', - 'cluster', - 'health', - '--address', - 'localhost:7233', - ]); - return output.includes('SERVING'); +function getComposeFile(): string { + return getMode() === 'local' + ? path.resolve('docker-compose.yml') + : path.resolve(__dirname, '..', 'infra', 'compose.yml'); } /** Check if the router container is running and healthy. */ @@ -75,99 +59,6 @@ function isRouterReady(): boolean { return status === 'healthy'; } -/** - * Ensure Temporal (and optionally router) are running via compose. - * If Temporal is already up but router is needed and missing, starts router only. - */ -export async function ensureInfra(useRouter: boolean): Promise { - const temporalReady = isTemporalReady(); - const routerNeeded = useRouter && !isRouterReady(); - - if (temporalReady && !routerNeeded) { - return; - } - - const composeFile = getComposeFile(); - const composeArgs = ['compose', '-f', composeFile]; - if (useRouter) composeArgs.push('--profile', 'router'); - composeArgs.push('up', '-d'); - - if (temporalReady && routerNeeded) { - console.log('Starting router...'); - } else { - console.log('Starting Shannon infrastructure...'); - } - execFileSync('docker', composeArgs, { stdio: 'inherit' }); - - // Wait for Temporal if it wasn't already running - if (!temporalReady) { - console.log('Waiting for Temporal to be ready...'); - for (let i = 0; i < 30; i++) { - if (isTemporalReady()) { - console.log('Temporal is ready!'); - break; - } - if (i === 29) { - console.error('Timeout waiting for Temporal'); - process.exit(1); - } - await sleep(2000); - } - } - - // Wait for router if needed - if (routerNeeded) { - console.log('Waiting for router to be ready...'); - for (let i = 0; i < 15; i++) { - if (isRouterReady()) { - console.log('Router is ready!'); - return; - } - await sleep(2000); - } - console.error('Timeout waiting for router'); - process.exit(1); - } -} - -/** - * Build the worker image locally (local mode only). - */ -export function buildImage(noCache: boolean): void { - console.log(`Building ${DEV_IMAGE}...`); - const args = ['build']; - if (noCache) args.push('--no-cache'); - args.push('-t', DEV_IMAGE, '.'); - execFileSync('docker', args, { stdio: 'inherit' }); - console.log(`Build complete: ${DEV_IMAGE}`); -} - -/** - * Ensure the worker image is available. - * Local mode: auto-builds if missing. NPX mode: pulls from Docker Hub. - */ -export function ensureImage(version: string): void { - const image = getWorkerImage(version); - const exists = runQuiet('docker', ['image', 'inspect', image]); - if (exists) return; - - if (getMode() === 'local') { - console.log('Worker image not found, building...'); - buildImage(false); - } else { - console.log(`Pulling ${image}...`); - try { - execFileSync('docker', ['pull', image], { stdio: 'inherit' }); - } catch { - console.error(`\nERROR: Failed to pull ${image}`); - console.error('The image may not be available for your platform yet.'); - console.error('Check https://hub.docker.com/r/keygraph/shannon for available tags.'); - process.exit(1); - } - pruneOldImages(version); - } -} - /** * Detect if --add-host is needed (Linux without Podman). * macOS has host.docker.internal built in. @@ -182,140 +73,259 @@ function addHostFlag(): string[] { return []; } -export interface WorkerOptions { - version: string; - url: string; - repo: { hostPath: string; containerPath: string }; - workspacesDir: string; - taskQueue: string; - containerName: string; - envFlags: string[]; - config?: { hostPath: string; containerPath: string }; - credentials?: string; - promptsDir?: string; - outputDir?: string; - workspace: string; - pipelineTesting?: boolean; -} - -/** - * Spawn the worker container in detached mode and return the process. - */ -export function spawnWorker(opts: WorkerOptions): ChildProcess { - const args = ['run', '-d', '--rm', '--name', opts.containerName, '--network', 'shannon-net']; - - // Add host flag for Linux - args.push(...addHostFlag()); - - // UID remapping for Linux bind mounts - if (os.platform() === 'linux' && process.getuid && process.getgid) { - args.push('-e', `SHANNON_HOST_UID=${process.getuid()}`, '-e', `SHANNON_HOST_GID=${process.getgid()}`); - } - - // Volume mounts - args.push('-v', `${opts.workspacesDir}:/app/workspaces`); - args.push('-v', `${opts.repo.hostPath}:${opts.repo.containerPath}:ro`); - - // Writable overlays: shadow .shannon/ inside the :ro repo with workspace-backed dirs - const workspacePath = path.join(opts.workspacesDir, opts.workspace); - args.push('-v', `${path.join(workspacePath, 'deliverables')}:${opts.repo.containerPath}/.shannon/deliverables`); - args.push('-v', `${path.join(workspacePath, 'scratchpad')}:${opts.repo.containerPath}/.shannon/scratchpad`); - args.push('-v', `${path.join(workspacePath, '.playwright-cli')}:${opts.repo.containerPath}/.shannon/.playwright-cli`); - - // Local mode: mount prompts for live editing - if (opts.promptsDir) { - args.push('-v', `${opts.promptsDir}:/app/apps/worker/prompts:ro`); - } - - if (opts.config) { - args.push('-v', `${opts.config.hostPath}:${opts.config.containerPath}:ro`); - } - - // Output directory for deliverables copy - if (opts.outputDir) { - args.push('-v', `${opts.outputDir}:/app/output`); - } - - // Mount credentials file to fixed container path - if (opts.credentials) { - args.push('-v', `${opts.credentials}:/app/credentials/google-sa-key.json:ro`); - } - - // Environment - args.push(...opts.envFlags); - - // Container settings - args.push('--shm-size', '2gb', '--security-opt', 'seccomp=unconfined'); - - // Image - args.push(getWorkerImage(opts.version)); - - // Worker command - args.push('node', 'apps/worker/dist/temporal/worker.js', opts.url, opts.repo.containerPath); - args.push('--task-queue', opts.taskQueue); - if (opts.config) { - args.push('--config', opts.config.containerPath); - } - if (opts.outputDir) { - args.push('--output', '/app/output'); - } - args.push('--workspace', opts.workspace); - if (opts.pipelineTesting) { - args.push('--pipeline-testing'); - } - - // Prevent MSYS/Git Bash from converting Unix paths (e.g. /repos/my-repo) to Windows paths - return spawn('docker', args, { - stdio: 'pipe', - ...(os.platform() === 'win32' && { env: { ...process.env, MSYS_NO_PATHCONV: '1' } }), - }); -} - -/** - * Stop all running shannon-worker-* containers. - */ -export function stopWorkers(): void { - const workers = runOutput('docker', ['ps', '-q', '--filter', 'name=shannon-worker-']); - if (!workers) return; - - const ids = workers.split('\n').filter(Boolean); - console.log('Stopping worker containers...'); - execFileSync('docker', ['stop', ...ids], { stdio: 'inherit' }); -} - -/** - * Tear down the compose stack. - */ -export function stopInfra(clean: boolean): void { - const composeFile = getComposeFile(); - const args = ['compose', '-f', composeFile, '--profile', 'router', 'down']; - if (clean) args.push('-v'); - execFileSync('docker', args, { stdio: 'inherit' }); -} - -/** - * Remove old keygraph/shannon images that don't match the current version. - */ +/** Remove old keygraph/shannon images that don't match the current version. */ function pruneOldImages(currentVersion: string): void { const output = runOutput('docker', ['images', NPX_IMAGE_REPO, '--format', '{{.Tag}}']); if (!output) return; - const currentTag = currentVersion; - const stale = output.split('\n').filter((tag) => tag && tag !== currentTag); + const stale = output.split('\n').filter((tag) => tag && tag !== currentVersion); for (const tag of stale) { runQuiet('docker', ['rmi', `${NPX_IMAGE_REPO}:${tag}`]); } } -/** - * List running worker containers. - */ -export function listRunningWorkers(): string { - return runOutput('docker', [ - 'ps', - '--filter', - 'name=shannon-worker-', - '--format', - 'table {{.Names}}\t{{.Status}}\t{{.RunningFor}}', - ]); +// === DockerOrchestrator === + +/** Docker-based orchestration backend. */ +export class DockerOrchestrator implements Orchestrator { + getWorkerImage(version: string): string { + return getMode() === 'local' ? DEV_IMAGE : `${NPX_IMAGE_REPO}:${version}`; + } + + isTemporalReady(): boolean { + const output = runOutput('docker', [ + 'exec', + 'shannon-temporal', + 'temporal', + 'operator', + 'cluster', + 'health', + '--address', + 'localhost:7233', + ]); + return output.includes('SERVING'); + } + + async ensureInfra(useRouter: boolean): Promise { + const temporalReady = this.isTemporalReady(); + const routerNeeded = useRouter && !isRouterReady(); + + if (temporalReady && !routerNeeded) { + return; + } + + const composeFile = getComposeFile(); + const composeArgs = ['compose', '-f', composeFile]; + if (useRouter) composeArgs.push('--profile', 'router'); + composeArgs.push('up', '-d'); + + if (temporalReady && routerNeeded) { + console.log('Starting router...'); + } else { + console.log('Starting Shannon infrastructure...'); + } + execFileSync('docker', composeArgs, { stdio: 'inherit' }); + + // Wait for Temporal if it wasn't already running + if (!temporalReady) { + console.log('Waiting for Temporal to be ready...'); + for (let i = 0; i < 30; i++) { + if (this.isTemporalReady()) { + console.log('Temporal is ready!'); + break; + } + if (i === 29) { + console.error('Timeout waiting for Temporal'); + process.exit(1); + } + await sleep(2000); + } + } + + // Wait for router if needed + if (routerNeeded) { + console.log('Waiting for router to be ready...'); + for (let i = 0; i < 15; i++) { + if (isRouterReady()) { + console.log('Router is ready!'); + return; + } + await sleep(2000); + } + console.error('Timeout waiting for router'); + process.exit(1); + } + } + + ensureImage(version: string): void { + const image = this.getWorkerImage(version); + const exists = runQuiet('docker', ['image', 'inspect', image]); + if (exists) return; + + if (getMode() === 'local') { + console.log('Worker image not found, building...'); + this.buildImage(false); + } else { + console.log(`Pulling ${image}...`); + try { + execFileSync('docker', ['pull', image], { stdio: 'inherit' }); + } catch { + console.error(`\nERROR: Failed to pull ${image}`); + console.error('The image may not be available for your platform yet.'); + console.error('Check https://hub.docker.com/r/keygraph/shannon for available tags.'); + process.exit(1); + } + pruneOldImages(version); + } + } + + spawnWorker(opts: WorkerOptions): WorkerHandle { + const args = ['run', '-d', '--rm', '--name', opts.containerName, '--network', 'shannon-net']; + + // Add host flag for Linux + args.push(...addHostFlag()); + + // UID remapping for Linux bind mounts + if (os.platform() === 'linux' && process.getuid && process.getgid) { + args.push('-e', `SHANNON_HOST_UID=${process.getuid()}`, '-e', `SHANNON_HOST_GID=${process.getgid()}`); + } + + // Volume mounts + args.push('-v', `${opts.workspacesDir}:/app/workspaces`); + args.push('-v', `${opts.repo.hostPath}:${opts.repo.containerPath}:ro`); + + // Writable overlays: shadow .shannon/ inside the :ro repo with workspace-backed dirs + const workspacePath = path.join(opts.workspacesDir, opts.workspace); + args.push('-v', `${path.join(workspacePath, 'deliverables')}:${opts.repo.containerPath}/.shannon/deliverables`); + args.push('-v', `${path.join(workspacePath, 'scratchpad')}:${opts.repo.containerPath}/.shannon/scratchpad`); + args.push( + '-v', + `${path.join(workspacePath, '.playwright-cli')}:${opts.repo.containerPath}/.shannon/.playwright-cli`, + ); + + // Local mode: mount prompts for live editing + if (opts.promptsDir) { + args.push('-v', `${opts.promptsDir}:/app/apps/worker/prompts:ro`); + } + + if (opts.config) { + args.push('-v', `${opts.config.hostPath}:${opts.config.containerPath}:ro`); + } + + // Output directory for deliverables copy + if (opts.outputDir) { + args.push('-v', `${opts.outputDir}:/app/output`); + } + + // Mount credentials file to fixed container path + if (opts.credentials) { + args.push('-v', `${opts.credentials}:/app/credentials/google-sa-key.json:ro`); + } + + // Environment + args.push(...opts.envFlags); + + // Container settings + args.push('--shm-size', '2gb', '--security-opt', 'seccomp=unconfined'); + + // Image + args.push(this.getWorkerImage(opts.version)); + + // Worker command + args.push('node', 'apps/worker/dist/temporal/worker.js', opts.url, opts.repo.containerPath); + args.push('--task-queue', opts.taskQueue); + if (opts.config) { + args.push('--config', opts.config.containerPath); + } + if (opts.outputDir) { + args.push('--output', '/app/output'); + } + args.push('--workspace', opts.workspace); + if (opts.pipelineTesting) { + args.push('--pipeline-testing'); + } + + // Prevent MSYS/Git Bash from converting Unix paths (e.g. /repos/my-repo) to Windows paths + const proc = spawn('docker', args, { + stdio: 'pipe', + ...(os.platform() === 'win32' && { env: { ...process.env, MSYS_NO_PATHCONV: '1' } }), + }); + + return new DockerWorkerHandle(proc, opts.containerName); + } + + stopWorkers(): void { + const workers = runOutput('docker', ['ps', '-q', '--filter', 'name=shannon-worker-']); + if (!workers) return; + + const ids = workers.split('\n').filter(Boolean); + console.log('Stopping worker containers...'); + execFileSync('docker', ['stop', ...ids], { stdio: 'inherit' }); + } + + stopInfra(clean: boolean): void { + const composeFile = getComposeFile(); + const args = ['compose', '-f', composeFile, '--profile', 'router', 'down']; + if (clean) args.push('-v'); + execFileSync('docker', args, { stdio: 'inherit' }); + } + + listRunningWorkers(): string { + return runOutput('docker', [ + 'ps', + '--filter', + 'name=shannon-worker-', + '--format', + 'table {{.Names}}\t{{.Status}}\t{{.RunningFor}}', + ]); + } + + runEphemeral(image: string, args: string[], mounts: string[]): void { + const dockerArgs = ['run', '--rm']; + for (const mount of mounts) { + dockerArgs.push('-v', mount); + } + dockerArgs.push(image, ...args); + execFileSync('docker', dockerArgs, { + stdio: 'inherit', + ...(os.platform() === 'win32' && { env: { ...process.env, MSYS_NO_PATHCONV: '1' } }), + }); + } + + /** Build the worker image locally (local mode only). */ + buildImage(noCache: boolean): void { + console.log(`Building ${DEV_IMAGE}...`); + const args = ['build']; + if (noCache) args.push('--no-cache'); + args.push('-t', DEV_IMAGE, '.'); + execFileSync('docker', args, { stdio: 'inherit' }); + console.log(`Build complete: ${DEV_IMAGE}`); + } +} + +/** WorkerHandle wrapping a Docker container's ChildProcess. */ +class DockerWorkerHandle implements WorkerHandle { + constructor( + private readonly proc: ChildProcess, + private readonly containerName: string, + ) {} + + onError(cb: (err: Error) => void): void { + this.proc.on('error', cb); + } + + kill(): void { + try { + execFileSync('docker', ['stop', this.containerName], { stdio: 'pipe' }); + } catch { + // Container may have already exited + } + } +} + +// === Backward-compatible exports === + +// NOTE: Used by commands/build.ts which doesn't go through the orchestrator +export function buildImage(noCache: boolean): void { + new DockerOrchestrator().buildImage(noCache); } diff --git a/apps/cli/src/env.ts b/apps/cli/src/env.ts index 4241ee1..d06c1c8 100644 --- a/apps/cli/src/env.ts +++ b/apps/cli/src/env.ts @@ -10,7 +10,7 @@ import { resolveConfig } from './config/resolver.js'; import { getMode } from './mode.js'; /** Environment variables forwarded to worker containers. */ -const FORWARD_VARS = [ +export const FORWARD_VARS = [ 'ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', @@ -61,6 +61,23 @@ export function buildEnvFlags(): string[] { return flags; } +/** + * Build a key-value record of env vars to forward to workers. + * Used by the K8s backend to create Secrets instead of Docker `-e` flags. + */ +export function buildEnvRecord(): Record { + const env: Record = { TEMPORAL_ADDRESS: 'shannon-temporal:7233' }; + + for (const key of FORWARD_VARS) { + const value = process.env[key]; + if (value) { + env[key] = value; + } + } + + return env; +} + interface CredentialValidation { valid: boolean; error?: string; diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 6d1cf84..8b1a79a 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -12,6 +12,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { setBackend } from './backend.js'; import { build } from './commands/build.js'; import { logs } from './commands/logs.js'; import { setup } from './commands/setup.js'; @@ -179,6 +180,19 @@ function parseStartArgs(argv: string[]): ParsedStartArgs { // === Main Dispatch === const args = process.argv.slice(2); + +// Parse --backend flag before command dispatch +const backendIdx = args.indexOf('--backend'); +if (backendIdx !== -1) { + const backendVal = args[backendIdx + 1]; + if (backendVal === 'k8s' || backendVal === 'kubernetes') { + setBackend('k8s'); + } else if (backendVal === 'docker') { + setBackend('docker'); + } + args.splice(backendIdx, 2); +} + const command = args[0]; switch (command) { @@ -201,10 +215,10 @@ switch (command) { break; } case 'workspaces': - workspaces(getVersion()); + await workspaces(getVersion()); break; case 'status': - status(); + await status(); break; case 'setup': if (getMode() === 'local') { diff --git a/apps/cli/src/k8s.ts b/apps/cli/src/k8s.ts new file mode 100644 index 0000000..cc4fdc1 --- /dev/null +++ b/apps/cli/src/k8s.ts @@ -0,0 +1,494 @@ +/** + * Kubernetes orchestration backend. + * + * Replaces Docker CLI commands with Kubernetes API calls: + * - `docker compose up` → apply Deployments, Services, PVCs + * - `docker run --rm` → K8s Job per scan + * - `docker stop` → delete Jobs + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { setTimeout as sleep } from 'node:timers/promises'; +import { fileURLToPath } from 'node:url'; +import * as k8s from '@kubernetes/client-node'; +import { buildEnvRecord } from './env.js'; +import { getMode } from './mode.js'; +import type { Orchestrator, WorkerHandle, WorkerOptions } from './orchestrator.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const NAMESPACE = 'shannon'; +const NPX_IMAGE_REPO = 'keygraph/shannon'; +const DEV_IMAGE = 'shannon-worker'; +const WORKER_LABEL = 'shannon-worker'; +const K8S_MANIFESTS_DIR = path.resolve(__dirname, '..', 'infra', 'k8s'); + +// === K8s Client Setup === + +function loadKubeConfig(): k8s.KubeConfig { + const kc = new k8s.KubeConfig(); + kc.loadFromDefault(); + return kc; +} + +/** Detect if running on kind or minikube (local K8s). */ +function isLocalCluster(kc: k8s.KubeConfig): boolean { + const context = kc.getCurrentContext(); + return context.startsWith('kind-') || context === 'minikube' || context.startsWith('minikube'); +} + +// === K8sOrchestrator === + +/** Kubernetes-based orchestration backend. */ +export class K8sOrchestrator implements Orchestrator { + private readonly kc: k8s.KubeConfig; + private readonly coreApi: k8s.CoreV1Api; + private readonly appsApi: k8s.AppsV1Api; + private readonly batchApi: k8s.BatchV1Api; + + constructor() { + this.kc = loadKubeConfig(); + this.coreApi = this.kc.makeApiClient(k8s.CoreV1Api); + this.appsApi = this.kc.makeApiClient(k8s.AppsV1Api); + this.batchApi = this.kc.makeApiClient(k8s.BatchV1Api); + } + + getWorkerImage(version: string): string { + return getMode() === 'local' ? DEV_IMAGE : `${NPX_IMAGE_REPO}:${version}`; + } + + // === Infrastructure === + + async ensureInfra(useRouter: boolean): Promise { + // 1. Create namespace if it doesn't exist + await this.ensureNamespace(); + + // 2. Create or update credentials secret + await this.ensureCredentialsSecret(); + + // 3. Apply Temporal manifests + await this.applyManifest('temporal.yaml'); + + // 4. Apply workspaces PVC + await this.applyManifest('workspaces-pvc.yaml'); + + // 5. Optionally apply router + if (useRouter) { + await this.applyManifest('router.yaml'); + } + + // 6. Wait for Temporal to be ready + if (!(await this.isTemporalReadyAsync())) { + console.log('Waiting for Temporal to be ready...'); + for (let i = 0; i < 30; i++) { + if (await this.isTemporalReadyAsync()) { + console.log('Temporal is ready!'); + break; + } + if (i === 29) { + console.error('Timeout waiting for Temporal'); + process.exit(1); + } + await sleep(2000); + } + } + } + + ensureImage(_version: string): void { + // K8s pulls images via imagePullPolicy — no-op for remote clusters. + // For kind, users must run `kind load docker-image shannon-worker` manually. + if (getMode() === 'local' && isLocalCluster(this.kc)) { + console.log('NOTE: For kind/minikube, ensure the worker image is loaded:'); + console.log(' kind load docker-image shannon-worker'); + } + } + + isTemporalReady(): boolean { + // K8s API is async — synchronous check returns false, ensureInfra uses async polling + return false; + } + + private async isTemporalReadyAsync(): Promise { + try { + const response = await this.coreApi.listNamespacedPod({ + namespace: NAMESPACE, + labelSelector: 'app=shannon-temporal', + }); + return response.items.some((pod) => { + const conditions = pod.status?.conditions ?? []; + return conditions.some((c) => c.type === 'Ready' && c.status === 'True'); + }); + } catch { + return false; + } + } + + // === Worker Lifecycle === + + spawnWorker(opts: WorkerOptions): WorkerHandle { + const image = this.getWorkerImage(opts.version); + const jobName = opts.containerName; + + // Build command + args for the worker + const command = ['node', 'apps/worker/dist/temporal/worker.js', opts.url, opts.repo.containerPath]; + const args: string[] = ['--task-queue', opts.taskQueue, '--workspace', opts.workspace]; + if (opts.config) { + args.push('--config', opts.config.containerPath); + } + if (opts.outputDir) { + args.push('--output', '/app/output'); + } + if (opts.pipelineTesting) { + args.push('--pipeline-testing'); + } + + // Build volume mounts and volumes + const volumeMounts: k8s.V1VolumeMount[] = [ + { name: 'workspaces', mountPath: '/app/workspaces' }, + { name: 'shm', mountPath: '/dev/shm' }, + ]; + const volumes: k8s.V1Volume[] = [ + { + name: 'workspaces', + persistentVolumeClaim: { claimName: 'shannon-workspaces' }, + }, + { + name: 'shm', + emptyDir: { medium: 'Memory', sizeLimit: '2Gi' }, + }, + ]; + + // Repo volume — hostPath for local clusters, PVC for managed + if (isLocalCluster(this.kc)) { + volumes.push({ + name: 'repo', + hostPath: { path: opts.repo.hostPath, type: 'Directory' }, + }); + } else { + volumes.push({ + name: 'repo', + persistentVolumeClaim: { claimName: `shannon-repo-${jobName}` }, + }); + } + volumeMounts.push({ + name: 'repo', + mountPath: opts.repo.containerPath, + readOnly: true, + }); + + // Overlay dirs for deliverables/scratchpad/playwright (writable areas over :ro repo) + for (const overlay of ['deliverables', 'scratchpad', '.playwright-cli']) { + const volName = `overlay-${overlay.replace('.', '')}`; + volumes.push({ + name: volName, + emptyDir: {}, + }); + volumeMounts.push({ + name: volName, + mountPath: `${opts.repo.containerPath}/.shannon/${overlay}`, + }); + } + + // Optional volume mounts + if (opts.config) { + // Config would need a ConfigMap — for now, pass via env or mount differently + } + + // Build env vars from the secret + TEMPORAL_ADDRESS + const env: k8s.V1EnvVar[] = [{ name: 'TEMPORAL_ADDRESS', value: 'shannon-temporal:7233' }]; + + const job: k8s.V1Job = { + apiVersion: 'batch/v1', + kind: 'Job', + metadata: { + name: jobName, + namespace: NAMESPACE, + labels: { + app: WORKER_LABEL, + 'shannon.io/workspace': opts.workspace, + }, + }, + spec: { + backoffLimit: 0, + ttlSecondsAfterFinished: 3600, + template: { + metadata: { + labels: { + app: WORKER_LABEL, + 'shannon.io/workspace': opts.workspace, + }, + }, + spec: { + restartPolicy: 'Never', + securityContext: { + seccompProfile: { type: 'Unconfined' }, + }, + containers: [ + { + name: 'worker', + image, + command, + args, + env, + envFrom: [{ secretRef: { name: 'shannon-credentials' } }], + volumeMounts, + resources: { + requests: { memory: '2Gi' }, + }, + }, + ], + volumes, + }, + }, + }, + }; + + // Create the Job asynchronously — errors are reported via the handle + const createPromise = this.batchApi.createNamespacedJob({ namespace: NAMESPACE, body: job }).then(() => { + console.log(`Worker job ${jobName} created in namespace ${NAMESPACE}`); + }); + + return new K8sWorkerHandle(jobName, this.batchApi, createPromise); + } + + stopWorkers(): void { + // Delete all worker jobs — fire and forget + this.batchApi + .deleteCollectionNamespacedJob({ + namespace: NAMESPACE, + labelSelector: `app=${WORKER_LABEL}`, + propagationPolicy: 'Background', + }) + .then(() => { + console.log('Worker jobs deleted.'); + }) + .catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err); + console.error(`Failed to stop workers: ${message}`); + }); + } + + stopInfra(clean: boolean): void { + if (clean) { + // Delete the entire namespace (removes everything) + this.coreApi + .deleteNamespace({ name: NAMESPACE }) + .then(() => { + console.log(`Namespace ${NAMESPACE} deleted.`); + }) + .catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err); + console.error(`Failed to delete namespace: ${message}`); + }); + } else { + // Just delete the Temporal deployment and services + this.appsApi.deleteNamespacedDeployment({ name: 'shannon-temporal', namespace: NAMESPACE }).catch(() => {}); + this.coreApi.deleteNamespacedService({ name: 'shannon-temporal', namespace: NAMESPACE }).catch(() => {}); + this.appsApi.deleteNamespacedDeployment({ name: 'shannon-router', namespace: NAMESPACE }).catch(() => {}); + this.coreApi.deleteNamespacedService({ name: 'shannon-router', namespace: NAMESPACE }).catch(() => {}); + console.log('Infrastructure resources deleted.'); + } + } + + listRunningWorkers(): string { + // This is called synchronously by the status command — return empty for now, + // actual implementation needs async refactor of the status command + return ''; + } + + runEphemeral(image: string, args: string[], mounts: string[]): void { + // For K8s, run an ephemeral pod and wait for completion + const podName = `shannon-ephemeral-${Date.now()}`; + + const volumeMounts: k8s.V1VolumeMount[] = []; + const volumes: k8s.V1Volume[] = []; + + // Parse Docker-style mount strings (src:dst) + for (let i = 0; i < mounts.length; i++) { + const mount = mounts[i]; + if (!mount) continue; + const parts = mount.split(':'); + const dst = parts[1]; + if (parts.length >= 2 && dst) { + const volName = `vol-${i}`; + volumeMounts.push({ name: volName, mountPath: dst }); + volumes.push({ + name: volName, + persistentVolumeClaim: { claimName: 'shannon-workspaces' }, + }); + } + } + + const pod: k8s.V1Pod = { + apiVersion: 'v1', + kind: 'Pod', + metadata: { + name: podName, + namespace: NAMESPACE, + }, + spec: { + restartPolicy: 'Never', + containers: [ + { + name: 'ephemeral', + image, + command: args, + volumeMounts, + env: [{ name: 'WORKSPACES_DIR', value: '/app/workspaces' }], + }, + ], + volumes, + }, + }; + + // Create pod and wait for completion + this.coreApi + .createNamespacedPod({ namespace: NAMESPACE, body: pod }) + .then(async () => { + // Poll for completion + for (let i = 0; i < 30; i++) { + const status = await this.coreApi.readNamespacedPod({ name: podName, namespace: NAMESPACE }); + if (status.status?.phase === 'Succeeded' || status.status?.phase === 'Failed') { + // Read logs + const log = await this.coreApi.readNamespacedPodLog({ name: podName, namespace: NAMESPACE }); + console.log(log); + // Clean up + await this.coreApi.deleteNamespacedPod({ name: podName, namespace: NAMESPACE }); + return; + } + await sleep(2000); + } + console.error('Timeout waiting for ephemeral pod'); + await this.coreApi.deleteNamespacedPod({ name: podName, namespace: NAMESPACE }); + }) + .catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err); + console.error(`Failed to run ephemeral pod: ${message}`); + }); + } + + // === Private Helpers === + + private async ensureNamespace(): Promise { + try { + await this.coreApi.readNamespace({ name: NAMESPACE }); + } catch { + console.log(`Creating namespace ${NAMESPACE}...`); + await this.coreApi.createNamespace({ + body: { + apiVersion: 'v1', + kind: 'Namespace', + metadata: { name: NAMESPACE, labels: { 'app.kubernetes.io/part-of': 'shannon' } }, + }, + }); + } + } + + private async ensureCredentialsSecret(): Promise { + const envRecord = buildEnvRecord(); + const stringData: Record = {}; + for (const [key, value] of Object.entries(envRecord)) { + if (key !== 'TEMPORAL_ADDRESS') { + stringData[key] = value; + } + } + + const secret: k8s.V1Secret = { + apiVersion: 'v1', + kind: 'Secret', + metadata: { + name: 'shannon-credentials', + namespace: NAMESPACE, + }, + stringData, + }; + + try { + await this.coreApi.replaceNamespacedSecret({ + name: 'shannon-credentials', + namespace: NAMESPACE, + body: secret, + }); + } catch { + await this.coreApi.createNamespacedSecret({ namespace: NAMESPACE, body: secret }); + } + } + + private async applyManifest(filename: string): Promise { + const manifestPath = path.join(K8S_MANIFESTS_DIR, filename); + const content = fs.readFileSync(manifestPath, 'utf-8'); + + // Split multi-document YAML + const docs = content.split(/^---$/m).filter((doc) => doc.trim()); + + for (const doc of docs) { + await this.applyResource(doc); + } + } + + private async applyResource(yamlDoc: string): Promise { + const objects = k8s.loadAllYaml(yamlDoc) as k8s.KubernetesObject[]; + const objectApi = k8s.KubernetesObjectApi.makeApiClient(this.kc); + + for (const obj of objects) { + if (!obj || !obj.kind || !obj.metadata?.name) continue; + + // Ensure metadata has required fields for the typed API + const spec = { + ...obj, + metadata: { ...obj.metadata, name: obj.metadata.name }, + }; + + try { + await objectApi.read(spec); + await objectApi.patch(spec); + } catch { + try { + await objectApi.create(spec); + } catch (createErr: unknown) { + const message = createErr instanceof Error ? createErr.message : String(createErr); + console.error(`Failed to apply ${obj.kind}/${obj.metadata.name}: ${message}`); + } + } + } + } +} + +// === K8sWorkerHandle === + +/** WorkerHandle wrapping a K8s Job. */ +class K8sWorkerHandle implements WorkerHandle { + private errorCallback: ((err: Error) => void) | undefined; + + constructor( + private readonly jobName: string, + private readonly batchApi: k8s.BatchV1Api, + createPromise: Promise, + ) { + // Wire up creation errors to the error callback + createPromise.catch((err: unknown) => { + const error = err instanceof Error ? err : new Error(String(err)); + if (this.errorCallback) { + this.errorCallback(error); + } else { + console.error(`Worker job creation failed: ${error.message}`); + } + }); + } + + onError(cb: (err: Error) => void): void { + this.errorCallback = cb; + } + + kill(): void { + this.batchApi + .deleteNamespacedJob({ + name: this.jobName, + namespace: NAMESPACE, + propagationPolicy: 'Background', + }) + .catch(() => { + // Job may have already completed + }); + } +} diff --git a/apps/cli/src/orchestrator.ts b/apps/cli/src/orchestrator.ts new file mode 100644 index 0000000..0e75fc6 --- /dev/null +++ b/apps/cli/src/orchestrator.ts @@ -0,0 +1,46 @@ +/** + * Orchestrator interface — abstraction over container orchestration backends. + * + * Docker and Kubernetes implement this interface so the CLI commands + * can swap backends without changing their logic. + */ + +export interface WorkerOptions { + version: string; + url: string; + repo: { hostPath: string; containerPath: string }; + workspacesDir: string; + taskQueue: string; + containerName: string; + envFlags: string[]; + config?: { hostPath: string; containerPath: string }; + credentials?: string; + promptsDir?: string; + outputDir?: string; + workspace: string; + pipelineTesting?: boolean; +} + +/** Handle to a running worker, returned by Orchestrator.spawnWorker(). */ +export interface WorkerHandle { + onError(cb: (err: Error) => void): void; + kill(): void; +} + +/** Container orchestration backend. */ +export interface Orchestrator { + ensureInfra(useRouter: boolean): Promise; + ensureImage(version: string): void; + spawnWorker(opts: WorkerOptions): WorkerHandle; + stopWorkers(): void; + stopInfra(clean: boolean): void; + listRunningWorkers(): string; + isTemporalReady(): boolean; + getWorkerImage(version: string): string; + + /** + * Run a one-shot ephemeral container and inherit stdio. + * Used by commands like `workspaces` that need to run worker-side scripts. + */ + runEphemeral(image: string, args: string[], mounts: string[]): void; +} diff --git a/apps/cli/tsdown.config.ts b/apps/cli/tsdown.config.ts index cf8a84f..24946d1 100644 --- a/apps/cli/tsdown.config.ts +++ b/apps/cli/tsdown.config.ts @@ -6,6 +6,6 @@ export default defineConfig({ target: 'node18', outDir: 'dist', clean: true, - deps: { neverBundle: ['@clack/prompts', 'dotenv', 'smol-toml'] }, + deps: { neverBundle: ['@clack/prompts', 'dotenv', 'smol-toml', '@kubernetes/client-node'] }, banner: { js: '#!/usr/bin/env node' }, }); diff --git a/apps/worker/src/temporal/pipeline.ts b/apps/worker/src/temporal/pipeline.ts index 7f74aa6..1ad047e 100644 --- a/apps/worker/src/temporal/pipeline.ts +++ b/apps/worker/src/temporal/pipeline.ts @@ -5,13 +5,14 @@ * within their own workflow context. */ -export { pentestPipeline } from './workflows.js'; +export type { ActivityInput } from './activities.js'; export type { AgentMetrics, PipelineInput, + PipelineProgress, PipelineState, PipelineSummary, ResumeState, VulnExploitPipelineResult, } from './shared.js'; -export type { ActivityInput } from './activities.js'; +export { pentestPipeline } from './workflows.js'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb7212e..b2e7ff7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,11 +27,35 @@ importers: specifier: ^5.9.3 version: 5.9.3 + apps/api: + dependencies: + '@hono/node-server': + specifier: ^1.14.0 + version: 1.19.13(hono@4.12.12) + '@kubernetes/client-node': + specifier: ^1.4.0 + version: 1.4.0 + '@shannon/worker': + specifier: workspace:* + version: link:../worker + '@temporalio/client': + specifier: ^1.11.0 + version: 1.15.0 + hono: + specifier: ^4.7.0 + version: 4.12.12 + zod: + specifier: ^4.3.6 + version: 4.3.6 + apps/cli: dependencies: '@clack/prompts': specifier: ^1.1.0 version: 1.1.0 + '@kubernetes/client-node': + specifier: ^1.4.0 + version: 1.4.0 chokidar: specifier: ^5.0.0 version: 5.0.0 @@ -137,24 +161,28 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] '@biomejs/cli-linux-arm64@2.4.7': resolution: {integrity: sha512-om6FugwmibzfP/6ALj5WRDVSND4H2G9X0nkI1HZpp2ySf9lW2j0X68oQSaHEnls6666oy4KDsc5RFjT4m0kV0w==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-x64-musl@2.4.7': resolution: {integrity: sha512-00kx4YrBMU8374zd2wHuRV5wseh0rom5HqRND+vDldJPrWwQw+mzd/d8byI9hPx926CG+vWzq6AeiT7Yi5y59g==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] '@biomejs/cli-linux-x64@2.4.7': resolution: {integrity: sha512-bV8/uo2Tj+gumnk4sUdkerWyCPRabaZdv88IpbmDWARQQoA/Q0YaqPz1a+LSEDIL7OfrnPi9Hq1Llz4ZIGyIQQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] '@biomejs/cli-win32-arm64@2.4.7': resolution: {integrity: sha512-hOUHBMlFCvDhu3WCq6vaBoG0dp0LkWxSEnEEsxxXvOa9TfT6ZBnbh72A/xBM7CBYB7WgwqboetzFEVDnMxelyw==} @@ -192,6 +220,12 @@ packages: engines: {node: '>=6'} hasBin: true + '@hono/node-server@1.19.13': + resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@img/sharp-darwin-arm64@0.34.5': resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -218,56 +252,66 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-win32-arm64@0.34.5': resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} @@ -300,6 +344,18 @@ packages: '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@jsep-plugin/assignment@1.3.0': + resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@jsep-plugin/regex@1.0.4': + resolution: {integrity: sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + '@jsonjoy.com/base64@1.1.2': resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} engines: {node: '>=10.0'} @@ -420,6 +476,9 @@ packages: peerDependencies: tslib: '2' + '@kubernetes/client-node@1.4.0': + resolution: {integrity: sha512-Zge3YvF7DJi264dU1b3wb/GmzR99JhUpqTvp+VGHfwZT+g7EOOYNScDJNZwXy9cszyIGPIs0VHr+kk8e95qqrA==} + '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} @@ -494,36 +553,42 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.11': resolution: {integrity: sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11': resolution: {integrity: sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11': resolution: {integrity: sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.11': resolution: {integrity: sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.11': resolution: {integrity: sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.11': resolution: {integrity: sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==} @@ -574,24 +639,28 @@ packages: engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [glibc] '@swc/core-linux-arm64-musl@1.15.18': resolution: {integrity: sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] + libc: [musl] '@swc/core-linux-x64-gnu@1.15.18': resolution: {integrity: sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [glibc] '@swc/core-linux-x64-musl@1.15.18': resolution: {integrity: sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==} engines: {node: '>=10'} cpu: [x64] os: [linux] + libc: [musl] '@swc/core-win32-arm64-msvc@1.15.18': resolution: {integrity: sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==} @@ -679,9 +748,18 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + + '@types/node@24.12.2': + resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} + '@types/node@25.5.0': resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + '@types/stream-buffers@3.0.8': + resolution: {integrity: sha512-J+7VaHKNvlNPJPEJXX/fKa9DZtR/xPMwuIbe+yNOwp1YB+ApUOBv2aUpEoBJEi8nJgbgs1x8e73ttg0r1rSUdw==} + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -748,6 +826,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -783,6 +865,58 @@ packages: resolution: {integrity: sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==} engines: {node: '>=20.19.0'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + b4a@1.8.0: + resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + + bare-events@2.8.2: + resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.7.0: + resolution: {integrity: sha512-xzqKsCFxAek9aezYhjJuJRXBIaYlg/0OGDTZp+T8eYmYMlm66cs6cYko02drIyjN2CBbi+I6L7YfXyqpqtKRXA==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-os@3.8.7: + resolution: {integrity: sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.13.0: + resolution: {integrity: sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.4.0: + resolution: {integrity: sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==} + baseline-browser-mapping@2.10.8: resolution: {integrity: sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==} engines: {node: '>=6.0.0'} @@ -803,6 +937,10 @@ packages: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + caniuse-lite@1.0.30001778: resolution: {integrity: sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==} @@ -825,12 +963,29 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -848,6 +1003,10 @@ packages: oxc-resolver: optional: true + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + electron-to-chromium@1.5.313: resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==} @@ -858,13 +1017,32 @@ packages: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.20.0: resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} engines: {node: '>=10.13.0'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@2.0.0: resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -892,6 +1070,9 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -899,6 +1080,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} @@ -911,13 +1095,28 @@ packages: picomatch: optional: true + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + fs-monkey@1.1.0: resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-tsconfig@4.13.6: resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} @@ -930,6 +1129,10 @@ packages: glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -937,13 +1140,33 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + heap-js@2.7.1: resolution: {integrity: sha512-EQfezRg0NCZGNlhlDR3Evrw1FVL2G3LhU7EgPoxufQKruNBSYA8MiRPHeWbU+36o+Fhel0wMwM+sLEiBAlNLJA==} engines: {node: '>=10.0.0'} + hono@4.12.12: + resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==} + engines: {node: '>=16.9.0'} + hookable@6.1.0: resolution: {integrity: sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==} + hpagent@1.2.0: + resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==} + engines: {node: '>=14'} + hyperdyperid@1.2.0: resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} engines: {node: '>=10.18'} @@ -956,18 +1179,34 @@ packages: resolution: {integrity: sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A==} engines: {node: '>=20.19.0'} + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} + jose@6.2.2: + resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsep@1.4.0: + resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} + engines: {node: '>= 10.16.0'} + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -979,6 +1218,11 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + jsonpath-plus@10.4.0: + resolution: {integrity: sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==} + engines: {node: '>=18.0.0'} + hasBin: true + loader-runner@4.3.1: resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} @@ -989,6 +1233,10 @@ packages: long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + memfs@4.56.11: resolution: {integrity: sha512-/GodtwVeKVIHZKLUSr2ZdOxKBC5hHki4JNCU22DoCGPEHr5o2PD5U721zvESKyWwCfTfavFl9WZYgA13OAYK0g==} peerDependencies: @@ -1005,6 +1253,9 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + ms@3.0.0-canary.1: resolution: {integrity: sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g==} engines: {node: '>=12.13'} @@ -1016,12 +1267,30 @@ packages: resolution: {integrity: sha512-hAWn8Hh2eewpB5McXR5EW81R3pR/ziuGhKCF3wFyUVCklanPqrIgMNr7jKCbzXeNVad0nUDfWpFRqh2u+zxQtw==} engines: {node: '>= 18.0.0'} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-releases@2.0.36: resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} + oauth4webapi@3.8.5: + resolution: {integrity: sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==} + obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + openid-client@6.8.2: + resolution: {integrity: sha512-uOvTCndr4udZsKihJ68H9bUICrriHdUVJ6Az+4Ns6cW55rwM5h0bjVIzDz2SxgOI84LKjFyjOFvERLzdTUROGA==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1040,6 +1309,9 @@ packages: resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} engines: {node: '>=12.0.0'} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + quansync@1.0.0: resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} @@ -1058,6 +1330,9 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + rfc4648@1.5.4: + resolution: {integrity: sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg==} + rolldown-plugin-dts@0.22.5: resolution: {integrity: sha512-M/HXfM4cboo+jONx9Z0X+CUf3B5tCi7ni+kR5fUW50Fp9AlZk0oVLesibGWgCXDKFp5lpgQ9yhKoImUFjl3VZw==} engines: {node: '>=20.19.0'} @@ -1100,10 +1375,22 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + smol-toml@1.6.1: resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1125,6 +1412,13 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + stream-buffers@3.0.3: + resolution: {integrity: sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==} + engines: {node: '>= 0.10.0'} + + streamx@2.25.0: + resolution: {integrity: sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1147,6 +1441,15 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tar-fs@3.1.2: + resolution: {integrity: sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==} + + tar-stream@3.1.8: + resolution: {integrity: sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==} + + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + terser-webpack-plugin@5.4.0: resolution: {integrity: sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==} engines: {node: '>= 10.13.0'} @@ -1168,6 +1471,9 @@ packages: engines: {node: '>=10'} hasBin: true + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + thingies@2.5.0: resolution: {integrity: sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==} engines: {node: '>=10.18'} @@ -1182,6 +1488,9 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tree-dump@1.1.0: resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} engines: {node: '>=10.0'} @@ -1265,6 +1574,9 @@ packages: unconfig-core@7.5.0: resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} @@ -1295,6 +1607,9 @@ packages: resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} engines: {node: '>=10.13.0'} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webpack-sources@3.3.4: resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} engines: {node: '>=10.13.0'} @@ -1309,10 +1624,28 @@ packages: webpack-cli: optional: true + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -1443,6 +1776,10 @@ snapshots: protobufjs: 7.5.4 yargs: 17.7.2 + '@hono/node-server@1.19.13(hono@4.12.12)': + dependencies: + hono: 4.12.12 + '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.2.4 @@ -1526,6 +1863,14 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} + '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + + '@jsep-plugin/regex@1.0.4(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': dependencies: tslib: 2.8.1 @@ -1653,6 +1998,33 @@ snapshots: '@jsonjoy.com/codegen': 17.67.0(tslib@2.8.1) tslib: 2.8.1 + '@kubernetes/client-node@1.4.0': + dependencies: + '@types/js-yaml': 4.0.9 + '@types/node': 24.12.2 + '@types/node-fetch': 2.6.13 + '@types/stream-buffers': 3.0.8 + form-data: 4.0.5 + hpagent: 1.2.0 + isomorphic-ws: 5.0.0(ws@8.20.0) + js-yaml: 4.1.1 + jsonpath-plus: 10.4.0 + node-fetch: 2.7.0 + openid-client: 6.8.2 + rfc4648: 1.5.4 + socks-proxy-agent: 8.0.5 + stream-buffers: 3.0.3 + tar-fs: 3.1.2 + ws: 8.20.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bufferutil + - encoding + - react-native-b4a + - supports-color + - utf-8-validate + '@napi-rs/wasm-runtime@1.1.1': dependencies: '@emnapi/core': 1.9.1 @@ -1891,10 +2263,23 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 25.5.0 + form-data: 4.0.5 + + '@types/node@24.12.2': + dependencies: + undici-types: 7.16.0 + '@types/node@25.5.0': dependencies: undici-types: 7.18.2 + '@types/stream-buffers@3.0.8': + dependencies: + '@types/node': 25.5.0 + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -1985,6 +2370,8 @@ snapshots: acorn@8.16.0: {} + agent-base@7.1.4: {} + ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 @@ -2017,6 +2404,42 @@ snapshots: estree-walker: 3.0.3 pathe: 2.0.3 + asynckit@0.4.0: {} + + b4a@1.8.0: {} + + bare-events@2.8.2: {} + + bare-fs@4.7.0: + dependencies: + bare-events: 2.8.2 + bare-path: 3.0.0 + bare-stream: 2.13.0(bare-events@2.8.2) + bare-url: 2.4.0 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-os@3.8.7: {} + + bare-path@3.0.0: + dependencies: + bare-os: 3.8.7 + + bare-stream@2.13.0(bare-events@2.8.2): + dependencies: + streamx: 2.25.0 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - react-native-b4a + + bare-url@2.4.0: + dependencies: + bare-path: 3.0.0 + baseline-browser-mapping@2.10.8: {} birpc@4.0.0: {} @@ -2033,6 +2456,11 @@ snapshots: cac@7.0.0: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + caniuse-lite@1.0.30001778: {} chokidar@5.0.0: @@ -2053,29 +2481,64 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@2.20.3: {} + debug@4.4.3: + dependencies: + ms: 2.1.3 + defu@6.1.4: {} + delayed-stream@1.0.0: {} + dotenv@16.6.1: {} dotenv@17.3.1: {} dts-resolver@2.1.3: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + electron-to-chromium@1.5.313: {} emoji-regex@8.0.0: {} empathic@2.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enhanced-resolve@5.20.0: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@2.0.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + escalade@3.2.0: {} eslint-scope@5.1.1: @@ -2097,20 +2560,56 @@ snapshots: event-target-shim@5.0.1: {} + events-universal@1.0.1: + dependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + events@3.3.0: {} fast-deep-equal@3.1.3: {} + fast-fifo@1.3.2: {} + fast-uri@3.1.0: {} fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + fs-monkey@1.1.0: {} + function-bind@1.1.2: {} + get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: + 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: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-tsconfig@4.13.6: dependencies: resolve-pkg-maps: 1.0.0 @@ -2121,14 +2620,30 @@ snapshots: glob-to-regexp@0.4.1: {} + gopd@1.2.0: {} + graceful-fs@4.2.11: {} has-flag@4.0.0: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + heap-js@2.7.1: {} + hono@4.12.12: {} + hookable@6.1.0: {} + hpagent@1.2.0: {} + hyperdyperid@1.2.0: {} iconv-lite@0.6.3: @@ -2137,30 +2652,48 @@ snapshots: import-without-cache@0.2.5: {} + ip-address@10.1.0: {} + is-fullwidth-code-point@3.0.0: {} + isomorphic-ws@5.0.0(ws@8.20.0): + dependencies: + ws: 8.20.0 + jest-worker@27.5.1: dependencies: '@types/node': 25.5.0 merge-stream: 2.0.0 supports-color: 8.1.1 + jose@6.2.2: {} + js-yaml@4.1.1: dependencies: argparse: 2.0.1 + jsep@1.4.0: {} + jsesc@3.1.0: {} json-parse-even-better-errors@2.3.1: {} json-schema-traverse@1.0.0: {} + jsonpath-plus@10.4.0: + dependencies: + '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) + '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) + jsep: 1.4.0 + loader-runner@4.3.1: {} lodash.camelcase@4.3.0: {} long@5.3.2: {} + math-intrinsics@1.1.0: {} + memfs@4.56.11(tslib@2.8.1): dependencies: '@jsonjoy.com/fs-core': 4.56.11(tslib@2.8.1) @@ -2186,16 +2719,33 @@ snapshots: dependencies: mime-db: 1.52.0 + ms@2.1.3: {} + ms@3.0.0-canary.1: {} neo-async@2.6.2: {} nexus-rpc@0.0.1: {} + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + node-releases@2.0.36: {} + oauth4webapi@3.8.5: {} + obug@2.1.1: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + openid-client@6.8.2: + dependencies: + jose: 6.2.2 + oauth4webapi: 3.8.5 + pathe@2.0.3: {} picocolors@1.1.1: {} @@ -2221,6 +2771,11 @@ snapshots: '@types/node': 25.5.0 long: 5.3.2 + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + quansync@1.0.0: {} readdirp@5.0.0: {} @@ -2231,6 +2786,8 @@ snapshots: resolve-pkg-maps@1.0.0: {} + rfc4648@1.5.4: {} + rolldown-plugin-dts@0.22.5(rolldown@1.0.0-rc.11)(typescript@5.9.3): dependencies: '@babel/generator': 8.0.0-rc.2 @@ -2286,8 +2843,23 @@ snapshots: sisteransi@1.0.5: {} + smart-buffer@4.2.0: {} + smol-toml@1.6.1: {} + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + source-map-js@1.2.1: {} source-map-loader@4.0.2(webpack@5.105.4(@swc/core@1.15.18)): @@ -2305,6 +2877,17 @@ snapshots: source-map@0.7.6: {} + stream-buffers@3.0.3: {} + + streamx@2.25.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -2327,6 +2910,36 @@ snapshots: tapable@2.3.0: {} + tar-fs@3.1.2: + dependencies: + pump: 3.0.4 + tar-stream: 3.1.8 + optionalDependencies: + bare-fs: 4.7.0 + bare-path: 3.0.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + tar-stream@3.1.8: + dependencies: + b4a: 1.8.0 + bare-fs: 4.7.0 + fast-fifo: 1.3.2 + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + teex@1.0.1: + dependencies: + streamx: 2.25.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + terser-webpack-plugin@5.4.0(@swc/core@1.15.18)(webpack@5.105.4(@swc/core@1.15.18)): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -2344,6 +2957,12 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + text-decoder@1.2.7: + dependencies: + b4a: 1.8.0 + transitivePeerDependencies: + - react-native-b4a + thingies@2.5.0(tslib@2.8.1): dependencies: tslib: 2.8.1 @@ -2355,6 +2974,8 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tr46@0.0.3: {} + tree-dump@1.1.0(tslib@2.8.1): dependencies: tslib: 2.8.1 @@ -2424,6 +3045,8 @@ snapshots: '@quansync/fs': 1.0.0 quansync: 1.0.0 + undici-types@7.16.0: {} + undici-types@7.18.2: {} unionfs@4.6.0: @@ -2447,6 +3070,8 @@ snapshots: glob-to-regexp: 0.4.1 graceful-fs: 4.2.11 + webidl-conversions@3.0.1: {} + webpack-sources@3.3.4: {} webpack@5.105.4(@swc/core@1.15.18): @@ -2481,12 +3106,21 @@ snapshots: - esbuild - uglify-js + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 + wrappy@1.0.2: {} + + ws@8.20.0: {} + y18n@5.0.8: {} yargs-parser@21.1.1: {}