From 52f42a884e0e5423f0c74ac3be9de08c2d5e8a89 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Tue, 24 Mar 2026 17:47:32 +0530 Subject: [PATCH 01/19] Add node dependency matrix sample --- README.md | 1 + node-dependency-matrix/.dockerignore | 7 + node-dependency-matrix/.gitignore | 4 + node-dependency-matrix/Dockerfile | 33 + node-dependency-matrix/README.md | 68 + node-dependency-matrix/entrypoint.sh | 18 + .../fixtures/expected-values.json | 98 + node-dependency-matrix/k8s/README.md | 122 + node-dependency-matrix/k8s/deploy-kind.sh | 90 + node-dependency-matrix/k8s/kind-config.yaml | 14 + .../k8s/manifests/00-localstack.yaml | 54 + .../k8s/manifests/01-mysql.yaml | 36 + .../k8s/manifests/02-postgres.yaml | 38 + .../k8s/manifests/03-mongo.yaml | 33 + .../k8s/manifests/04-redis.yaml | 37 + .../k8s/manifests/05-redpanda.yaml | 50 + .../k8s/manifests/06-fixture-service.yaml | 58 + .../k8s/manifests/07-tls-proxies.yaml | 297 ++ .../k8s/manifests/08-app.yaml | 98 + node-dependency-matrix/package-lock.json | 3644 +++++++++++++++++ node-dependency-matrix/package.json | 37 + .../proto/dependency_matrix.proto | 41 + .../scripts/generate_certs.sh | 63 + .../scripts/record_traffic.sh | 24 + .../scripts/send_grpc_traffic.sh | 41 + node-dependency-matrix/src/bin/app.ts | 208 + .../src/bin/dependencyFixture.ts | 106 + node-dependency-matrix/src/bin/tlsProxy.ts | 52 + node-dependency-matrix/src/global.d.ts | 3 + node-dependency-matrix/src/lib/config.ts | 82 + .../src/lib/dependencies.ts | 481 +++ node-dependency-matrix/src/lib/log.ts | 20 + node-dependency-matrix/src/lib/proto.ts | 44 + node-dependency-matrix/tsconfig.json | 17 + 34 files changed, 6019 insertions(+) create mode 100644 node-dependency-matrix/.dockerignore create mode 100644 node-dependency-matrix/.gitignore create mode 100644 node-dependency-matrix/Dockerfile create mode 100644 node-dependency-matrix/README.md create mode 100755 node-dependency-matrix/entrypoint.sh create mode 100644 node-dependency-matrix/fixtures/expected-values.json create mode 100644 node-dependency-matrix/k8s/README.md create mode 100755 node-dependency-matrix/k8s/deploy-kind.sh create mode 100644 node-dependency-matrix/k8s/kind-config.yaml create mode 100644 node-dependency-matrix/k8s/manifests/00-localstack.yaml create mode 100644 node-dependency-matrix/k8s/manifests/01-mysql.yaml create mode 100644 node-dependency-matrix/k8s/manifests/02-postgres.yaml create mode 100644 node-dependency-matrix/k8s/manifests/03-mongo.yaml create mode 100644 node-dependency-matrix/k8s/manifests/04-redis.yaml create mode 100644 node-dependency-matrix/k8s/manifests/05-redpanda.yaml create mode 100644 node-dependency-matrix/k8s/manifests/06-fixture-service.yaml create mode 100644 node-dependency-matrix/k8s/manifests/07-tls-proxies.yaml create mode 100644 node-dependency-matrix/k8s/manifests/08-app.yaml create mode 100644 node-dependency-matrix/package-lock.json create mode 100644 node-dependency-matrix/package.json create mode 100644 node-dependency-matrix/proto/dependency_matrix.proto create mode 100755 node-dependency-matrix/scripts/generate_certs.sh create mode 100755 node-dependency-matrix/scripts/record_traffic.sh create mode 100755 node-dependency-matrix/scripts/send_grpc_traffic.sh create mode 100644 node-dependency-matrix/src/bin/app.ts create mode 100644 node-dependency-matrix/src/bin/dependencyFixture.ts create mode 100644 node-dependency-matrix/src/bin/tlsProxy.ts create mode 100644 node-dependency-matrix/src/global.d.ts create mode 100644 node-dependency-matrix/src/lib/config.ts create mode 100644 node-dependency-matrix/src/lib/dependencies.ts create mode 100644 node-dependency-matrix/src/lib/log.ts create mode 100644 node-dependency-matrix/src/lib/proto.ts create mode 100644 node-dependency-matrix/tsconfig.json diff --git a/README.md b/README.md index e6cf6ca..fcdd3fb 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ This repo contains the sample for [Keploy's](https://keploy.io) JS Application. 8. [Node-JWT](https://github.com/keploy/samples-typescript/tree/main/node-jwt) 9. [Node-Mongoose](https://github.com/keploy/samples-typescript/tree/main/node-mongoose) 10. [Rest-Express-Mongo](https://github.com/keploy/samples-typescript/tree/main/rest-express-mongo) +11. [Node-Dependency-Matrix](https://github.com/keploy/samples-typescript/tree/main/node-dependency-matrix) ## Community Support ❤️ diff --git a/node-dependency-matrix/.dockerignore b/node-dependency-matrix/.dockerignore new file mode 100644 index 0000000..a98267d --- /dev/null +++ b/node-dependency-matrix/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +.generated +README.md +k8s +scripts +keploy diff --git a/node-dependency-matrix/.gitignore b/node-dependency-matrix/.gitignore new file mode 100644 index 0000000..e379bb9 --- /dev/null +++ b/node-dependency-matrix/.gitignore @@ -0,0 +1,4 @@ +dist +node_modules +.generated +keploy diff --git a/node-dependency-matrix/Dockerfile b/node-dependency-matrix/Dockerfile new file mode 100644 index 0000000..8be0f8d --- /dev/null +++ b/node-dependency-matrix/Dockerfile @@ -0,0 +1,33 @@ +FROM node:20-bookworm-slim AS build + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY tsconfig.json ./ +COPY proto ./proto +COPY fixtures ./fixtures +COPY src ./src +COPY entrypoint.sh ./entrypoint.sh + +RUN npm run build && chmod +x /app/entrypoint.sh + +FROM node:20-bookworm-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* + +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/dist ./dist +COPY --from=build /app/proto ./proto +COPY --from=build /app/fixtures ./fixtures +COPY --from=build /app/entrypoint.sh ./entrypoint.sh + +ENV NODE_ENV=production +ENV PORT=8080 +ENV GRPC_PORT=9090 + +ENTRYPOINT ["./entrypoint.sh"] +CMD ["node", "dist/bin/app.js"] diff --git a/node-dependency-matrix/README.md b/node-dependency-matrix/README.md new file mode 100644 index 0000000..04a9450 --- /dev/null +++ b/node-dependency-matrix/README.md @@ -0,0 +1,68 @@ +# Node Dependency Matrix + +This sample is a TypeScript application built to validate Keploy cloud record/replay against a broad dependency matrix. + +What it exposes: + +- incoming HTTP +- incoming gRPC +- outgoing HTTPS +- outgoing HTTP/2 +- outgoing gRPC +- outgoing MySQL +- outgoing Postgres +- outgoing Mongo +- outgoing Redis +- outgoing Kafka +- outgoing SQS-over-HTTPS +- outgoing generic TLS traffic +- a noisy endpoint +- an expected replay failure endpoint +- duplicate-friendly endpoints for static dedup verification + +## Quick start + +```bash +cd samples-typescript/node-dependency-matrix +npm install +npm run build +bash k8s/deploy-kind.sh +APP_URL=http://localhost:30081 bash scripts/record_traffic.sh +``` + +If the default host ports are already occupied, override them: + +```bash +HOST_PROXY_PORT=31080 HOST_APP_PORT=31081 HOST_GRPC_PORT=31090 bash k8s/deploy-kind.sh +APP_URL=http://localhost:31081 bash scripts/record_traffic.sh +``` + +Optional incoming gRPC traffic: + +```bash +GRPC_TARGET=localhost:30090 bash scripts/send_grpc_traffic.sh +``` + +## Keploy expectations + +The machine-readable contract is: + +```bash +fixtures/expected-values.json +``` + +Use it later in Playwright or manual verification for: + +- testcase counts +- required and acceptable mock kinds +- total mock count ranges +- dedup expectations +- expected replay pass/noisy/fail scenarios + +## Kubernetes + +The full Kind and staging UI flow is in: + +```bash +k8s/README.md +``` diff --git a/node-dependency-matrix/entrypoint.sh b/node-dependency-matrix/entrypoint.sh new file mode 100755 index 0000000..93bff71 --- /dev/null +++ b/node-dependency-matrix/entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -eu + +COMBINED_CA_CERT_PATH="${COMBINED_CA_CERT_PATH:-/tmp/node-dependency-matrix-ca-bundle.crt}" + +rm -f "${COMBINED_CA_CERT_PATH}" + +if [ -n "${SAMPLE_CA_CERT_PATH:-}" ] && [ -f "${SAMPLE_CA_CERT_PATH}" ]; then + cat "${SAMPLE_CA_CERT_PATH}" > "${COMBINED_CA_CERT_PATH}" + cp "${SAMPLE_CA_CERT_PATH}" /usr/local/share/ca-certificates/node-dependency-matrix.crt + update-ca-certificates >/dev/null 2>&1 || true +fi + +if [ -f /tmp/keploy-tls/ca.crt ]; then + cat /tmp/keploy-tls/ca.crt >> "${COMBINED_CA_CERT_PATH}" +fi + +exec "$@" diff --git a/node-dependency-matrix/fixtures/expected-values.json b/node-dependency-matrix/fixtures/expected-values.json new file mode 100644 index 0000000..42e09a0 --- /dev/null +++ b/node-dependency-matrix/fixtures/expected-values.json @@ -0,0 +1,98 @@ +{ + "sampleId": "node-dependency-matrix", + "cluster": { + "namespace": "default", + "deployment": "node-dependency-matrix", + "service": "node-dependency-matrix", + "httpNodePort": 30081, + "grpcNodePort": 30090, + "k8sProxyNodePort": 30080 + }, + "recording": { + "pods": { + "expectedReplicas": 1, + "expectedReadyContainersWhileRecording": 2 + }, + "httpTrafficScript": { + "expectedTestcasesExact": 17 + }, + "grpcTrafficScript": { + "expectedAdditionalTestcasesExact": 2 + }, + "mockSummary": { + "total": { + "min": 9, + "max": 250 + }, + "requiredKinds": [ + "Http", + "MySQL" + ], + "preferredKinds": [ + "Http", + "Http2", + "gRPC", + "MySQL", + "PostgresV2", + "Mongo", + "Redis", + "Kafka", + "Generic" + ], + "fallbackKinds": { + "deps-http2": ["Http2", "Generic", "Http"], + "deps-grpc": ["gRPC", "Generic", "Http2"], + "deps-postgres": ["PostgresV2", "Generic"], + "deps-mongo": ["Mongo", "Generic"], + "deps-redis": ["Redis", "Generic"], + "deps-kafka": ["Kafka", "Generic"], + "deps-sqs": ["Http", "Generic"] + } + }, + "dedup": { + "expectedEntries": [ + { + "method": "GET", + "path": "/dedup/catalog?id=alpha", + "duplicateCount": 2 + }, + { + "method": "GET", + "path": "/dedup/catalog?id=beta", + "duplicateCount": 1 + } + ] + } + }, + "replay": { + "expectedPassScenarios": [ + "deps-http", + "deps-http2", + "deps-grpc", + "deps-mysql", + "deps-postgres", + "deps-mongo", + "deps-redis", + "deps-kafka", + "deps-sqs", + "deps-generic", + "dedup/catalog" + ], + "expectedNoisyScenarios": [ + "noise/runtime" + ], + "expectedFailScenarios": [ + "expected-fail/time-window" + ], + "globalNoiseSuggestions": { + "body": [ + "runtime.requestId", + "runtime.servedAt" + ], + "header": [ + "x-matrix-generated-at", + "etag" + ] + } + } +} diff --git a/node-dependency-matrix/k8s/README.md b/node-dependency-matrix/k8s/README.md new file mode 100644 index 0000000..3c8aeec --- /dev/null +++ b/node-dependency-matrix/k8s/README.md @@ -0,0 +1,122 @@ +# Kubernetes Setup + +This sample is designed for Keploy cloud record/replay validation in Kind. + +What it covers: + +- incoming HTTP +- incoming gRPC +- outgoing HTTPS +- outgoing HTTP/2 +- outgoing gRPC +- outgoing MySQL +- outgoing Postgres +- outgoing Mongo over TLS +- outgoing Redis over TLS +- outgoing Kafka +- outgoing SQS-over-HTTPS +- generic TLS traffic +- noisy replay cases +- expected replay failures +- static dedup-friendly duplicate requests + +Protocol note: + +- HTTPS, HTTP/2, gRPC, Mongo, Redis, SQS, and the generic socket flow use TLS in this sample +- MySQL, Postgres, and Kafka use direct protocol connections because a generic TLS terminator in front of those protocols is not deterministic enough for this Kind-based regression sample + +## 1. Create the cluster and deploy the sample + +From `samples-typescript/node-dependency-matrix/`: + +```bash +bash k8s/deploy-kind.sh +``` + +On the first run this pre-pulls the heavier dependency images directly inside the Kind node before applying the manifests. That is intentional. It avoids slow pod-level image pulls that can make the Kind control plane flaky on Macs. + +If `30080`, `30081`, or `30090` are already taken on your machine, override them: + +```bash +HOST_PROXY_PORT=31080 HOST_APP_PORT=31081 HOST_GRPC_PORT=31090 bash k8s/deploy-kind.sh +``` + +If you already created a broken or half-ready cluster, delete it first and re-run: + +```bash +kind delete cluster --name node-dependency-matrix +HOST_PROXY_PORT=31080 HOST_APP_PORT=31081 HOST_GRPC_PORT=31090 bash k8s/deploy-kind.sh +``` + +## 2. Validate the app before Keploy + +```bash +curl http://localhost:30081/health +bash scripts/record_traffic.sh +``` + +Optional incoming gRPC validation: + +```bash +bash scripts/send_grpc_traffic.sh +``` + +## 3. Connect the cluster in Keploy UI + +Use: + +- cluster name: `node-dependency-matrix` +- ingress URL: `http://localhost:30080` + +If you used custom host ports, use that custom proxy host port instead. + +Install `k8s-proxy` as a NodePort on `30080` or your custom proxy host port. + +Hosted staging UI note: + +- if your sample app is on custom host ports, only the proxy ingress URL needs to change in the Keploy cluster registration flow +- the app traffic scripts should use the custom app/grpc host ports you selected locally + +## 4. Start recording + +Record: + +- namespace: `default` +- deployment: `node-dependency-matrix` + +When recording starts, the app pod should roll and become `2/2`. + +```bash +kubectl get pods -w +``` + +## 5. Generate traffic while recording + +```bash +APP_URL=http://localhost:30081 bash scripts/record_traffic.sh +GRPC_TARGET=localhost:30090 bash scripts/send_grpc_traffic.sh +``` + +If you used custom host ports: + +```bash +APP_URL=http://localhost:31081 bash scripts/record_traffic.sh +GRPC_TARGET=localhost:31090 bash scripts/send_grpc_traffic.sh +``` + +## 6. Expectations + +The machine-readable contract is in: + +```bash +fixtures/expected-values.json +``` + +Use it for: + +- expected pod count +- testcase count from the seed scripts +- required/preferred mock kinds +- acceptable fallback mock kinds where runtime support depends on enterprise parser wiring +- dedup expectations +- expected pass/noisy/fail replay outcomes diff --git a/node-dependency-matrix/k8s/deploy-kind.sh b/node-dependency-matrix/k8s/deploy-kind.sh new file mode 100755 index 0000000..6aa5fa2 --- /dev/null +++ b/node-dependency-matrix/k8s/deploy-kind.sh @@ -0,0 +1,90 @@ +#!/bin/bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CLUSTER_NAME="${KIND_CLUSTER_NAME:-node-dependency-matrix}" +KIND_CONTEXT="kind-${CLUSTER_NAME}" +KUBECTL=(kubectl --context "${KIND_CONTEXT}") +IMAGE_NAME="${IMAGE_NAME:-node-dependency-matrix:latest}" +CERT_DIR="${ROOT_DIR}/.generated/certs" +HOST_PROXY_PORT="${HOST_PROXY_PORT:-30080}" +HOST_APP_PORT="${HOST_APP_PORT:-30081}" +HOST_GRPC_PORT="${HOST_GRPC_PORT:-30090}" +SKIP_DEPENDENCY_PULLS="${SKIP_DEPENDENCY_PULLS:-0}" + +DEPENDENCY_IMAGES=( + "mysql:8.0" + "postgres:16" + "mongo:7" + "redis:7-alpine" + "docker.redpanda.com/redpandadata/redpanda:v25.1.2" + "localstack/localstack:3.3" +) + +if ! kind get clusters | grep -qx "${CLUSTER_NAME}"; then + TMP_KIND_CONFIG="$(mktemp)" + cat > "${TMP_KIND_CONFIG}" </dev/null 2>&1 || true +--- +apiVersion: v1 +kind: Service +metadata: + name: localstack +spec: + selector: + app: localstack + ports: + - name: edge + port: 4566 + targetPort: 4566 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: localstack +spec: + replicas: 1 + selector: + matchLabels: + app: localstack + template: + metadata: + labels: + app: localstack + spec: + containers: + - name: localstack + image: localstack/localstack:3.3 + env: + - name: SERVICES + value: sqs + - name: AWS_DEFAULT_REGION + value: us-east-1 + ports: + - containerPort: 4566 + volumeMounts: + - name: init-scripts + mountPath: /etc/localstack/init/ready.d + volumes: + - name: init-scripts + configMap: + name: localstack-init-scripts + defaultMode: 0755 diff --git a/node-dependency-matrix/k8s/manifests/01-mysql.yaml b/node-dependency-matrix/k8s/manifests/01-mysql.yaml new file mode 100644 index 0000000..4da2948 --- /dev/null +++ b/node-dependency-matrix/k8s/manifests/01-mysql.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +kind: Service +metadata: + name: mysql +spec: + selector: + app: mysql + ports: + - name: mysql + port: 3306 + targetPort: 3306 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mysql +spec: + replicas: 1 + selector: + matchLabels: + app: mysql + template: + metadata: + labels: + app: mysql + spec: + containers: + - name: mysql + image: mysql:8.0 + env: + - name: MYSQL_ALLOW_EMPTY_PASSWORD + value: "yes" + - name: MYSQL_DATABASE + value: matrix + ports: + - containerPort: 3306 diff --git a/node-dependency-matrix/k8s/manifests/02-postgres.yaml b/node-dependency-matrix/k8s/manifests/02-postgres.yaml new file mode 100644 index 0000000..171eeee --- /dev/null +++ b/node-dependency-matrix/k8s/manifests/02-postgres.yaml @@ -0,0 +1,38 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgres +spec: + selector: + app: postgres + ports: + - name: postgres + port: 5432 + targetPort: 5432 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres +spec: + replicas: 1 + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: postgres:16 + env: + - name: POSTGRES_HOST_AUTH_METHOD + value: trust + - name: POSTGRES_DB + value: matrix + - name: POSTGRES_USER + value: postgres + ports: + - containerPort: 5432 diff --git a/node-dependency-matrix/k8s/manifests/03-mongo.yaml b/node-dependency-matrix/k8s/manifests/03-mongo.yaml new file mode 100644 index 0000000..23234cc --- /dev/null +++ b/node-dependency-matrix/k8s/manifests/03-mongo.yaml @@ -0,0 +1,33 @@ +apiVersion: v1 +kind: Service +metadata: + name: mongo +spec: + selector: + app: mongo + ports: + - name: mongo + port: 27017 + targetPort: 27017 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mongo +spec: + replicas: 1 + selector: + matchLabels: + app: mongo + template: + metadata: + labels: + app: mongo + spec: + containers: + - name: mongo + image: mongo:7 + args: + - --bind_ip_all + ports: + - containerPort: 27017 diff --git a/node-dependency-matrix/k8s/manifests/04-redis.yaml b/node-dependency-matrix/k8s/manifests/04-redis.yaml new file mode 100644 index 0000000..4bc0236 --- /dev/null +++ b/node-dependency-matrix/k8s/manifests/04-redis.yaml @@ -0,0 +1,37 @@ +apiVersion: v1 +kind: Service +metadata: + name: redis +spec: + selector: + app: redis + ports: + - name: redis + port: 6379 + targetPort: 6379 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:7-alpine + args: + - redis-server + - --appendonly + - "no" + - --save + - "" + ports: + - containerPort: 6379 diff --git a/node-dependency-matrix/k8s/manifests/05-redpanda.yaml b/node-dependency-matrix/k8s/manifests/05-redpanda.yaml new file mode 100644 index 0000000..888bb65 --- /dev/null +++ b/node-dependency-matrix/k8s/manifests/05-redpanda.yaml @@ -0,0 +1,50 @@ +apiVersion: v1 +kind: Service +metadata: + name: redpanda +spec: + selector: + app: redpanda + ports: + - name: kafka + port: 9092 + targetPort: 9092 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redpanda +spec: + replicas: 1 + selector: + matchLabels: + app: redpanda + template: + metadata: + labels: + app: redpanda + spec: + containers: + - name: redpanda + image: docker.redpanda.com/redpandadata/redpanda:v25.1.2 + args: + - redpanda + - start + - --overprovisioned + - --smp + - "1" + - --memory + - 512M + - --reserve-memory + - 0M + - --check=false + - --node-id + - "0" + - --kafka-addr + - PLAINTEXT://0.0.0.0:9092 + - --advertise-kafka-addr + - PLAINTEXT://redpanda:9092 + - --set + - redpanda.auto_create_topics_enabled=true + ports: + - containerPort: 9092 diff --git a/node-dependency-matrix/k8s/manifests/06-fixture-service.yaml b/node-dependency-matrix/k8s/manifests/06-fixture-service.yaml new file mode 100644 index 0000000..93acb31 --- /dev/null +++ b/node-dependency-matrix/k8s/manifests/06-fixture-service.yaml @@ -0,0 +1,58 @@ +apiVersion: v1 +kind: Service +metadata: + name: fixture-service +spec: + selector: + app: fixture-service + ports: + - name: https + port: 8443 + targetPort: 8443 + - name: http2 + port: 9443 + targetPort: 9443 + - name: grpc + port: 50051 + targetPort: 50051 + - name: generic + port: 9445 + targetPort: 9445 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fixture-service +spec: + replicas: 1 + selector: + matchLabels: + app: fixture-service + template: + metadata: + labels: + app: fixture-service + spec: + containers: + - name: fixture-service + image: node-dependency-matrix:latest + imagePullPolicy: IfNotPresent + command: ["node", "dist/bin/dependencyFixture.js"] + env: + - name: TLS_CERT_PATH + value: /etc/sample-certs/proxy.crt + - name: TLS_KEY_PATH + value: /etc/sample-certs/proxy.key + ports: + - containerPort: 8443 + - containerPort: 9443 + - containerPort: 50051 + - containerPort: 9445 + volumeMounts: + - name: sample-tls + mountPath: /etc/sample-certs + readOnly: true + volumes: + - name: sample-tls + secret: + secretName: node-dependency-matrix-tls diff --git a/node-dependency-matrix/k8s/manifests/07-tls-proxies.yaml b/node-dependency-matrix/k8s/manifests/07-tls-proxies.yaml new file mode 100644 index 0000000..418081e --- /dev/null +++ b/node-dependency-matrix/k8s/manifests/07-tls-proxies.yaml @@ -0,0 +1,297 @@ +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: Service + metadata: + name: mysql-tls + spec: + selector: + app: mysql-tls + ports: + - name: mysql + port: 3306 + targetPort: 3306 + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: mysql-tls + spec: + replicas: 1 + selector: + matchLabels: + app: mysql-tls + template: + metadata: + labels: + app: mysql-tls + spec: + containers: + - name: mysql-tls + image: node-dependency-matrix:latest + imagePullPolicy: IfNotPresent + command: ["node", "dist/bin/tlsProxy.js"] + env: + - name: LISTEN_PORT + value: "3306" + - name: TARGET_HOST + value: mysql + - name: TARGET_PORT + value: "3306" + - name: TLS_CERT_PATH + value: /etc/sample-certs/proxy.crt + - name: TLS_KEY_PATH + value: /etc/sample-certs/proxy.key + volumeMounts: + - name: sample-tls + mountPath: /etc/sample-certs + readOnly: true + volumes: + - name: sample-tls + secret: + secretName: node-dependency-matrix-tls + - apiVersion: v1 + kind: Service + metadata: + name: postgres-tls + spec: + selector: + app: postgres-tls + ports: + - name: postgres + port: 5432 + targetPort: 5432 + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: postgres-tls + spec: + replicas: 1 + selector: + matchLabels: + app: postgres-tls + template: + metadata: + labels: + app: postgres-tls + spec: + containers: + - name: postgres-tls + image: node-dependency-matrix:latest + imagePullPolicy: IfNotPresent + command: ["node", "dist/bin/tlsProxy.js"] + env: + - name: LISTEN_PORT + value: "5432" + - name: TARGET_HOST + value: postgres + - name: TARGET_PORT + value: "5432" + - name: TLS_CERT_PATH + value: /etc/sample-certs/proxy.crt + - name: TLS_KEY_PATH + value: /etc/sample-certs/proxy.key + volumeMounts: + - name: sample-tls + mountPath: /etc/sample-certs + readOnly: true + volumes: + - name: sample-tls + secret: + secretName: node-dependency-matrix-tls + - apiVersion: v1 + kind: Service + metadata: + name: mongo-tls + spec: + selector: + app: mongo-tls + ports: + - name: mongo + port: 27017 + targetPort: 27017 + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: mongo-tls + spec: + replicas: 1 + selector: + matchLabels: + app: mongo-tls + template: + metadata: + labels: + app: mongo-tls + spec: + containers: + - name: mongo-tls + image: node-dependency-matrix:latest + imagePullPolicy: IfNotPresent + command: ["node", "dist/bin/tlsProxy.js"] + env: + - name: LISTEN_PORT + value: "27017" + - name: TARGET_HOST + value: mongo + - name: TARGET_PORT + value: "27017" + - name: TLS_CERT_PATH + value: /etc/sample-certs/proxy.crt + - name: TLS_KEY_PATH + value: /etc/sample-certs/proxy.key + volumeMounts: + - name: sample-tls + mountPath: /etc/sample-certs + readOnly: true + volumes: + - name: sample-tls + secret: + secretName: node-dependency-matrix-tls + - apiVersion: v1 + kind: Service + metadata: + name: redis-tls + spec: + selector: + app: redis-tls + ports: + - name: redis + port: 6380 + targetPort: 6380 + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: redis-tls + spec: + replicas: 1 + selector: + matchLabels: + app: redis-tls + template: + metadata: + labels: + app: redis-tls + spec: + containers: + - name: redis-tls + image: node-dependency-matrix:latest + imagePullPolicy: IfNotPresent + command: ["node", "dist/bin/tlsProxy.js"] + env: + - name: LISTEN_PORT + value: "6380" + - name: TARGET_HOST + value: redis + - name: TARGET_PORT + value: "6379" + - name: TLS_CERT_PATH + value: /etc/sample-certs/proxy.crt + - name: TLS_KEY_PATH + value: /etc/sample-certs/proxy.key + volumeMounts: + - name: sample-tls + mountPath: /etc/sample-certs + readOnly: true + volumes: + - name: sample-tls + secret: + secretName: node-dependency-matrix-tls + - apiVersion: v1 + kind: Service + metadata: + name: kafka-tls + spec: + selector: + app: kafka-tls + ports: + - name: kafka + port: 9094 + targetPort: 9094 + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: kafka-tls + spec: + replicas: 1 + selector: + matchLabels: + app: kafka-tls + template: + metadata: + labels: + app: kafka-tls + spec: + containers: + - name: kafka-tls + image: node-dependency-matrix:latest + imagePullPolicy: IfNotPresent + command: ["node", "dist/bin/tlsProxy.js"] + env: + - name: LISTEN_PORT + value: "9094" + - name: TARGET_HOST + value: redpanda + - name: TARGET_PORT + value: "9092" + - name: TLS_CERT_PATH + value: /etc/sample-certs/proxy.crt + - name: TLS_KEY_PATH + value: /etc/sample-certs/proxy.key + volumeMounts: + - name: sample-tls + mountPath: /etc/sample-certs + readOnly: true + volumes: + - name: sample-tls + secret: + secretName: node-dependency-matrix-tls + - apiVersion: v1 + kind: Service + metadata: + name: sqs-tls + spec: + selector: + app: sqs-tls + ports: + - name: https + port: 4567 + targetPort: 4567 + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: sqs-tls + spec: + replicas: 1 + selector: + matchLabels: + app: sqs-tls + template: + metadata: + labels: + app: sqs-tls + spec: + containers: + - name: sqs-tls + image: node-dependency-matrix:latest + imagePullPolicy: IfNotPresent + command: ["node", "dist/bin/tlsProxy.js"] + env: + - name: LISTEN_PORT + value: "4567" + - name: TARGET_HOST + value: localstack + - name: TARGET_PORT + value: "4566" + - name: TLS_CERT_PATH + value: /etc/sample-certs/proxy.crt + - name: TLS_KEY_PATH + value: /etc/sample-certs/proxy.key + volumeMounts: + - name: sample-tls + mountPath: /etc/sample-certs + readOnly: true + volumes: + - name: sample-tls + secret: + secretName: node-dependency-matrix-tls diff --git a/node-dependency-matrix/k8s/manifests/08-app.yaml b/node-dependency-matrix/k8s/manifests/08-app.yaml new file mode 100644 index 0000000..2393eaf --- /dev/null +++ b/node-dependency-matrix/k8s/manifests/08-app.yaml @@ -0,0 +1,98 @@ +apiVersion: v1 +kind: Service +metadata: + name: node-dependency-matrix +spec: + type: NodePort + selector: + app: node-dependency-matrix + ports: + - name: http + port: 8080 + targetPort: 8080 + nodePort: 30081 + - name: grpc + port: 9090 + targetPort: 9090 + nodePort: 30090 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: node-dependency-matrix +spec: + replicas: 1 + selector: + matchLabels: + app: node-dependency-matrix + template: + metadata: + labels: + app: node-dependency-matrix + spec: + containers: + - name: node-dependency-matrix + image: node-dependency-matrix:latest + imagePullPolicy: IfNotPresent + env: + - name: PORT + value: "8080" + - name: GRPC_PORT + value: "9090" + - name: FIXTURE_HTTPS_BASE + value: https://fixture-service:8443 + - name: FIXTURE_HTTP2_ORIGIN + value: https://fixture-service:9443 + - name: FIXTURE_GRPC_TARGET + value: fixture-service:50051 + - name: FIXTURE_GENERIC_HOST + value: fixture-service + - name: FIXTURE_GENERIC_PORT + value: "9445" + - name: MYSQL_URL + value: mysql://root@mysql:3306/matrix + - name: POSTGRES_URL + value: postgresql://postgres@postgres:5432/matrix + - name: MONGO_URL + value: mongodb://mongo-tls:27017/matrix?tls=true + - name: REDIS_URL + value: rediss://redis-tls:6380 + - name: KAFKA_BROKERS + value: redpanda:9092 + - name: KAFKA_TOPIC + value: matrix-events + - name: SQS_ENDPOINT + value: https://sqs-tls:4567 + - name: SQS_QUEUE_URL + value: https://sqs-tls:4567/000000000000/dependency-matrix + - name: SAMPLE_CA_CERT_PATH + value: /etc/sample-certs/ca.crt + - name: COMBINED_CA_CERT_PATH + value: /tmp/node-dependency-matrix-ca-bundle.crt + - name: NODE_EXTRA_CA_CERTS + value: /tmp/node-dependency-matrix-ca-bundle.crt + - name: SSL_CERT_FILE + value: /tmp/node-dependency-matrix-ca-bundle.crt + ports: + - containerPort: 8080 + - containerPort: 9090 + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 10 + volumeMounts: + - name: sample-tls + mountPath: /etc/sample-certs + readOnly: true + volumes: + - name: sample-tls + secret: + secretName: node-dependency-matrix-tls diff --git a/node-dependency-matrix/package-lock.json b/node-dependency-matrix/package-lock.json new file mode 100644 index 0000000..7d4f344 --- /dev/null +++ b/node-dependency-matrix/package-lock.json @@ -0,0 +1,3644 @@ +{ + "name": "node-dependency-matrix", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node-dependency-matrix", + "version": "1.0.0", + "dependencies": { + "@aws-sdk/client-sqs": "^3.893.0", + "@grpc/grpc-js": "^1.14.0", + "@grpc/proto-loader": "^0.8.0", + "@smithy/node-http-handler": "^4.4.5", + "express": "^4.21.2", + "ioredis": "^5.8.1", + "kafkajs": "^2.2.4", + "mongodb": "^6.20.0", + "mysql2": "^3.15.3", + "pg": "^8.16.3" + }, + "devDependencies": { + "@types/express": "^5.0.3", + "@types/node": "^24.7.2", + "@types/pg": "^8.15.5", + "tsx": "^4.20.6", + "typescript": "^5.9.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sqs": { + "version": "3.1015.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.1015.0.tgz", + "integrity": "sha512-j194LgVzJDydlWjVT+HBR3X5GGvEOFrv123ODWww29necxiq7wy4qFZm/2CF+X/OxQL5zA0gxuCgCqDJIk0UNw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/credential-provider-node": "^3.972.25", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-sdk-sqs": "^3.972.17", + "@aws-sdk/middleware-user-agent": "^3.972.25", + "@aws-sdk/region-config-resolver": "^3.972.9", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.11", + "@smithy/config-resolver": "^4.4.13", + "@smithy/core": "^3.23.12", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/md5-js": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-retry": "^4.4.44", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.43", + "@smithy/util-defaults-mode-node": "^4.2.47", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.973.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.24.tgz", + "integrity": "sha512-vvf82RYQu2GidWAuQq+uIzaPz9V0gSCXVqdVzRosgl5rXcspXOpSD3wFreGGW6AYymPr97Z69kjVnLePBxloDw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/xml-builder": "^3.972.15", + "@smithy/core": "^3.23.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.22.tgz", + "integrity": "sha512-cXp0VTDWT76p3hyK5D51yIKEfpf6/zsUvMfaB8CkyqadJxMQ8SbEeVroregmDlZbtG31wkj9ei0WnftmieggLg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.24.tgz", + "integrity": "sha512-h694K7+tRuepSRJr09wTvQfaEnjzsKZ5s7fbESrVds02GT/QzViJ94/HCNwM7bUfFxqpPXHxulZfL6Cou0dwPg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/types": "^3.973.6", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.20", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.24.tgz", + "integrity": "sha512-O46fFmv0RDFWiWEA9/e6oW92BnsyAXuEgTTasxHligjn2RCr9L/DK773m/NoFaL3ZdNAUz8WxgxunleMnHAkeQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/credential-provider-env": "^3.972.22", + "@aws-sdk/credential-provider-http": "^3.972.24", + "@aws-sdk/credential-provider-login": "^3.972.24", + "@aws-sdk/credential-provider-process": "^3.972.22", + "@aws-sdk/credential-provider-sso": "^3.972.24", + "@aws-sdk/credential-provider-web-identity": "^3.972.24", + "@aws-sdk/nested-clients": "^3.996.14", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.24.tgz", + "integrity": "sha512-sIk8oa6AzDoUhxsR11svZESqvzGuXesw62Rl2oW6wguZx8i9cdGCvkFg+h5K7iucUZP8wyWibUbJMc+J66cu5g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/nested-clients": "^3.996.14", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.25.tgz", + "integrity": "sha512-m7dR0Dsva2P+VUpL+VkC0WwiDby5pgmWXkRVDB5rlwv0jXJrQJf7YMtCoM8Wjk0H9jPeCYOxOXXcIgp/qp5Alg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.22", + "@aws-sdk/credential-provider-http": "^3.972.24", + "@aws-sdk/credential-provider-ini": "^3.972.24", + "@aws-sdk/credential-provider-process": "^3.972.22", + "@aws-sdk/credential-provider-sso": "^3.972.24", + "@aws-sdk/credential-provider-web-identity": "^3.972.24", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.22.tgz", + "integrity": "sha512-Os32s8/4gTZjBk5BtoS/cuTILaj+K72d0dVG7TCJX/fC4598cxwLDmf1AEHEpER5oL3K//yETjvFaz0V8oO5Xw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.24.tgz", + "integrity": "sha512-PaFv7snEfypU2yXkpvfyWgddEbDLtgVe51wdZlinhc2doubBjUzJZZpgwuF2Jenl1FBydMhNpMjD6SBUM3qdSA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/nested-clients": "^3.996.14", + "@aws-sdk/token-providers": "3.1015.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.24.tgz", + "integrity": "sha512-J6H4R1nvr3uBTqD/EeIPAskrBtET4WFfNhpFySr2xW7bVZOXpQfPjrLSIx65jcNjBmLXzWq8QFLdVoGxiGG/SA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/nested-clients": "^3.996.14", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", + "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", + "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", + "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-sqs": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.17.tgz", + "integrity": "sha512-LnzPRRoDXGtlFV2G1p2rsY6fRKrbf6Pvvc21KliSLw3+NmQca2+Aa1QIMRbpQvZYedsSqkGYwxe+qvXwQ2uxDw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.25.tgz", + "integrity": "sha512-QxiMPofvOt8SwSynTOmuZfvvPM1S9QfkESBxB22NMHTRXCJhR5BygLl8IXfC4jELiisQgwsgUby21GtXfX3f/g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@smithy/core": "^3.23.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-retry": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.996.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.14.tgz", + "integrity": "sha512-fSESKvh1VbfjtV3QMnRkCPZWkUbQof6T/DOpiLp33yP2wA+rbwwnZeG3XT3Ekljgw2I8X4XaQPnw+zSR8yxJ5Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.25", + "@aws-sdk/region-config-resolver": "^3.972.9", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.11", + "@smithy/config-resolver": "^4.4.13", + "@smithy/core": "^3.23.12", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-retry": "^4.4.44", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.43", + "@smithy/util-defaults-mode-node": "^4.2.47", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.9.tgz", + "integrity": "sha512-eQ+dFU05ZRC/lC2XpYlYSPlXtX3VT8sn5toxN2Fv7EXlMoA2p9V7vUBKqHunfD4TRLpxUq8Y8Ol/nCqiv327Ng==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/config-resolver": "^4.4.13", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1015.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1015.0.tgz", + "integrity": "sha512-3OSD4y110nisRhHzFOjoEeHU4GQL4KpzkX9PxzWaiZe0Yg2+thZKM0Pn9DjYwezH5JYfh/K++xK/SE0IHGrmCQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.24", + "@aws-sdk/nested-clients": "^3.996.14", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", + "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-endpoints": "^3.3.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", + "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.11.tgz", + "integrity": "sha512-1qdXbXo2s5MMLpUvw00284LsbhtlQ4ul7Zzdn5n+7p4WVgCMLqhxImpHIrjSoc72E/fyc4Wq8dLtUld2Gsh+lA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.25", + "@aws-sdk/types": "^3.973.6", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.15.tgz", + "integrity": "sha512-PxMRlCFNiQnke9YR29vjFQwz4jq+6Q04rOVFeTDR2K7Qpv9h9FOWOxG+zJjageimYbWqE3bTuLjmryWHAWbvaA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "fast-xml-parser": "5.5.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", + "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz", + "integrity": "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.13.tgz", + "integrity": "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.23.12", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz", + "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.20", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", + "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", + "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", + "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", + "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.12.tgz", + "integrity": "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", + "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.27", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.27.tgz", + "integrity": "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.12", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-middleware": "^4.2.12", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.44", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.44.tgz", + "integrity": "sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/service-error-classification": "^4.2.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz", + "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", + "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", + "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", + "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", + "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", + "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", + "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "@smithy/util-uri-escape": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", + "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", + "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", + "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", + "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.12.7", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.7.tgz", + "integrity": "sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.12", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.20", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", + "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", + "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.43", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.43.tgz", + "integrity": "sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.47", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.47.tgz", + "integrity": "sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.13", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", + "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", + "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.12.tgz", + "integrity": "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.20", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.20.tgz", + "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-xml-builder": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.5.8", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", + "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.2.0", + "strnum": "^2.2.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ioredis": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ioredis/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ioredis/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/kafkajs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.4.tgz", + "integrity": "sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mongodb": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz", + "integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.20.0.tgz", + "integrity": "sha512-eCLUs7BNbgA6nf/MZXsaBO1SfGs0LtLVrJD3WeWq+jPLDWkSufTD+aGMwykfUVPdZnblaUK1a8G/P63cl9FkKg==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz", + "integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strnum": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz", + "integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/node-dependency-matrix/package.json b/node-dependency-matrix/package.json new file mode 100644 index 0000000..7c77555 --- /dev/null +++ b/node-dependency-matrix/package.json @@ -0,0 +1,37 @@ +{ + "name": "node-dependency-matrix", + "version": "1.0.0", + "private": true, + "description": "TypeScript sample app for exercising Keploy cloud record/replay dependency flows.", + "main": "dist/bin/app.js", + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node dist/bin/app.js", + "start:app": "node dist/bin/app.js", + "start:fixture": "node dist/bin/dependencyFixture.js", + "start:tls-proxy": "node dist/bin/tlsProxy.js", + "dev": "tsx watch src/bin/app.ts" + }, + "engines": { + "node": ">=20" + }, + "dependencies": { + "@aws-sdk/client-sqs": "^3.893.0", + "@grpc/grpc-js": "^1.14.0", + "@grpc/proto-loader": "^0.8.0", + "@smithy/node-http-handler": "^4.4.5", + "express": "^4.21.2", + "ioredis": "^5.8.1", + "kafkajs": "^2.2.4", + "mongodb": "^6.20.0", + "mysql2": "^3.15.3", + "pg": "^8.16.3" + }, + "devDependencies": { + "@types/express": "^5.0.3", + "@types/node": "^24.7.2", + "@types/pg": "^8.15.5", + "tsx": "^4.20.6", + "typescript": "^5.9.3" + } +} diff --git a/node-dependency-matrix/proto/dependency_matrix.proto b/node-dependency-matrix/proto/dependency_matrix.proto new file mode 100644 index 0000000..2c37110 --- /dev/null +++ b/node-dependency-matrix/proto/dependency_matrix.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +package dependencymatrix; + +service DependencyFixture { + rpc GetDependencyQuote (QuoteRequest) returns (QuoteReply); +} + +service DependencyMatrix { + rpc Ping (PingRequest) returns (PingReply); + rpc RunDependencyScenario (ScenarioRequest) returns (ScenarioReply); +} + +message QuoteRequest { + string scenario = 1; +} + +message QuoteReply { + string scenario = 1; + string message = 2; + int64 unix_time = 3; +} + +message PingRequest { + string name = 1; +} + +message PingReply { + string message = 1; + string sample_id = 2; +} + +message ScenarioRequest { + string scenario = 1; +} + +message ScenarioReply { + string scenario = 1; + string status = 2; + string payload_json = 3; +} diff --git a/node-dependency-matrix/scripts/generate_certs.sh b/node-dependency-matrix/scripts/generate_certs.sh new file mode 100755 index 0000000..adbbdae --- /dev/null +++ b/node-dependency-matrix/scripts/generate_certs.sh @@ -0,0 +1,63 @@ +#!/bin/bash +set -euo pipefail + +OUTPUT_DIR="${1:-.generated/certs}" +mkdir -p "${OUTPUT_DIR}" + +CA_CONFIG="${OUTPUT_DIR}/ca.cnf" +SERVER_CONFIG="${OUTPUT_DIR}/server.cnf" + +cat > "${CA_CONFIG}" <<'EOF' +[req] +distinguished_name = req_distinguished_name +x509_extensions = v3_ca +prompt = no + +[req_distinguished_name] +CN = node-dependency-matrix-ca + +[v3_ca] +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer +basicConstraints = critical, CA:true +keyUsage = critical, keyCertSign, cRLSign +EOF + +cat > "${SERVER_CONFIG}" <<'EOF' +[req] +distinguished_name = req_distinguished_name +req_extensions = v3_server +prompt = no + +[req_distinguished_name] +CN = node-dependency-matrix + +[v3_server] +basicConstraints = critical, CA:false +keyUsage = critical, digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth +subjectAltName = @alt_names + +[alt_names] +DNS.1 = fixture-service +DNS.2 = mysql-tls +DNS.3 = postgres-tls +DNS.4 = mongo-tls +DNS.5 = redis-tls +DNS.6 = kafka-tls +DNS.7 = sqs-tls +DNS.8 = localhost +IP.1 = 127.0.0.1 +EOF + +openssl genrsa -out "${OUTPUT_DIR}/ca.key" 2048 >/dev/null 2>&1 +openssl req -x509 -new -nodes -key "${OUTPUT_DIR}/ca.key" -sha256 -days 3650 -out "${OUTPUT_DIR}/ca.crt" -config "${CA_CONFIG}" >/dev/null 2>&1 + +openssl genrsa -out "${OUTPUT_DIR}/proxy.key" 2048 >/dev/null 2>&1 +openssl req -new -key "${OUTPUT_DIR}/proxy.key" -out "${OUTPUT_DIR}/proxy.csr" -config "${SERVER_CONFIG}" >/dev/null 2>&1 +openssl x509 -req -in "${OUTPUT_DIR}/proxy.csr" -CA "${OUTPUT_DIR}/ca.crt" -CAkey "${OUTPUT_DIR}/ca.key" -CAcreateserial -out "${OUTPUT_DIR}/proxy.crt" -days 3650 -sha256 -extensions v3_server -extfile "${SERVER_CONFIG}" >/dev/null 2>&1 +cat "${OUTPUT_DIR}/proxy.crt" "${OUTPUT_DIR}/ca.crt" > "${OUTPUT_DIR}/proxy-fullchain.crt" + +rm -f "${OUTPUT_DIR}/proxy.csr" "${OUTPUT_DIR}/ca.srl" "${CA_CONFIG}" "${SERVER_CONFIG}" + +echo "Generated certs in ${OUTPUT_DIR}" diff --git a/node-dependency-matrix/scripts/record_traffic.sh b/node-dependency-matrix/scripts/record_traffic.sh new file mode 100755 index 0000000..6a3c845 --- /dev/null +++ b/node-dependency-matrix/scripts/record_traffic.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -euo pipefail + +APP_URL="${APP_URL:-http://localhost:30081}" + +echo "Using APP_URL=${APP_URL}" + +curl -fsS "${APP_URL}/deps/http" +curl -fsS "${APP_URL}/deps/http2" +curl -fsS "${APP_URL}/deps/grpc" +curl -fsS "${APP_URL}/deps/mysql" +curl -fsS "${APP_URL}/deps/postgres" +curl -fsS "${APP_URL}/deps/mongo" +curl -fsS "${APP_URL}/deps/redis" +curl -fsS "${APP_URL}/deps/kafka" +curl -fsS "${APP_URL}/deps/sqs" +curl -fsS "${APP_URL}/deps/generic" +curl -fsS "${APP_URL}/dedup/catalog?id=alpha" +curl -fsS "${APP_URL}/dedup/catalog?id=alpha" +curl -fsS "${APP_URL}/dedup/catalog?id=alpha" +curl -fsS "${APP_URL}/dedup/catalog?id=beta" +curl -fsS "${APP_URL}/dedup/catalog?id=beta" +curl -fsS "${APP_URL}/noise/runtime" +curl -fsS "${APP_URL}/expected-fail/time-window?ts=$(date +%s)" diff --git a/node-dependency-matrix/scripts/send_grpc_traffic.sh b/node-dependency-matrix/scripts/send_grpc_traffic.sh new file mode 100755 index 0000000..75ad589 --- /dev/null +++ b/node-dependency-matrix/scripts/send_grpc_traffic.sh @@ -0,0 +1,41 @@ +#!/bin/bash +set -euo pipefail + +GRPC_TARGET="${GRPC_TARGET:-localhost:30090}" + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +GRPC_TARGET="${GRPC_TARGET}" ROOT_DIR="${ROOT_DIR}" node <<'NODE' +const path = require('path'); +const grpc = require('@grpc/grpc-js'); +const protoLoader = require('@grpc/proto-loader'); + +const rootDir = process.env.ROOT_DIR; +const target = process.env.GRPC_TARGET; +const protoPath = path.join(rootDir, 'proto', 'dependency_matrix.proto'); +const packageDefinition = protoLoader.loadSync(protoPath, { + keepCase: false, + longs: String, + enums: String, + defaults: true, + oneofs: true +}); +const proto = grpc.loadPackageDefinition(packageDefinition).dependencymatrix; +const client = new proto.DependencyMatrix(target, grpc.credentials.createInsecure()); + +client.Ping({ name: 'matrix' }, (pingErr, pingRes) => { + if (pingErr) { + console.error(pingErr); + process.exit(1); + } + console.log(JSON.stringify(pingRes)); + client.RunDependencyScenario({ scenario: 'deps-http' }, (scenarioErr, scenarioRes) => { + if (scenarioErr) { + console.error(scenarioErr); + process.exit(1); + } + console.log(JSON.stringify(scenarioRes)); + client.close(); + }); +}); +NODE diff --git a/node-dependency-matrix/src/bin/app.ts b/node-dependency-matrix/src/bin/app.ts new file mode 100644 index 0000000..ee6c1eb --- /dev/null +++ b/node-dependency-matrix/src/bin/app.ts @@ -0,0 +1,208 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import * as grpc from '@grpc/grpc-js'; +import express, { Request, Response } from 'express'; + +import { loadConfig, readExpectationsJson } from '../lib/config'; +import { + runAllDependencyScenarios, + runGenericScenario, + runGrpcScenario, + runHttp2Scenario, + runHttpScenario, + runKafkaScenario, + runMongoScenario, + runMySqlScenario, + runNamedScenario, + runPostgresScenario, + runRedisScenario, + runSqsScenario +} from '../lib/dependencies'; +import { error, info } from '../lib/log'; +import { loadMatrixProto } from '../lib/proto'; + +const config = loadConfig(); +const app = express(); + +app.use(express.json()); + +function logScenarioStart(name: string): void { + info('scenario started', { scenario: name }); +} + +function logScenarioSuccess(name: string, payload: Record): void { + info('scenario completed', { scenario: name, payload }); +} + +function logScenarioFailure(name: string, err: unknown): void { + error('scenario failed', { + scenario: name, + error: err instanceof Error ? err.message : String(err) + }); +} + +async function executeScenario(res: Response, scenarioName: string, runner: () => Promise): Promise { + logScenarioStart(scenarioName); + + try { + const result = await runner(); + logScenarioSuccess(scenarioName, { status: 'ok' }); + res.status(200).json(result); + } catch (err) { + logScenarioFailure(scenarioName, err); + res.status(500).json({ + scenario: scenarioName, + status: 'error', + error: err instanceof Error ? err.message : String(err) + }); + } +} + +app.get('/health', (_req, res) => { + res.json({ + status: 'ok', + sampleId: config.sampleId + }); +}); + +app.get('/expectations', (_req, res) => { + res.type('application/json').send(readExpectationsJson(config)); +}); + +app.get('/deps/http', async (_req, res) => executeScenario(res, 'deps-http', () => runHttpScenario(config))); +app.get('/deps/http2', async (_req, res) => executeScenario(res, 'deps-http2', () => runHttp2Scenario(config))); +app.get('/deps/grpc', async (_req, res) => executeScenario(res, 'deps-grpc', () => runGrpcScenario(config))); +app.get('/deps/mysql', async (_req, res) => executeScenario(res, 'deps-mysql', () => runMySqlScenario(config))); +app.get('/deps/postgres', async (_req, res) => executeScenario(res, 'deps-postgres', () => runPostgresScenario(config))); +app.get('/deps/mongo', async (_req, res) => executeScenario(res, 'deps-mongo', () => runMongoScenario(config))); +app.get('/deps/redis', async (_req, res) => executeScenario(res, 'deps-redis', () => runRedisScenario(config))); +app.get('/deps/kafka', async (_req, res) => executeScenario(res, 'deps-kafka', () => runKafkaScenario(config))); +app.get('/deps/sqs', async (_req, res) => executeScenario(res, 'deps-sqs', () => runSqsScenario(config))); +app.get('/deps/generic', async (_req, res) => executeScenario(res, 'deps-generic', () => runGenericScenario(config))); +app.get('/deps/all', async (_req, res) => executeScenario(res, 'deps-all', () => runAllDependencyScenarios(config))); + +app.get('/dedup/catalog', (req, res) => { + const itemId = typeof req.query.id === 'string' ? req.query.id : 'alpha'; + logScenarioStart('dedup/catalog'); + + const payload = { + scenario: 'dedup/catalog', + itemId, + description: `catalog-${itemId}`, + stablePrice: 42 + }; + + logScenarioSuccess('dedup/catalog', payload); + res.json(payload); +}); + +app.get('/noise/runtime', (_req, res) => { + const payload = { + scenario: 'noise/runtime', + runtime: { + requestId: crypto.randomUUID(), + servedAt: new Date().toISOString() + } + }; + + res.setHeader('x-matrix-generated-at', new Date().toISOString()); + res.setHeader('etag', `"${crypto.randomUUID()}"`); + logScenarioSuccess('noise/runtime', payload); + res.json(payload); +}); + +app.get('/expected-fail/time-window', (req, res) => { + const ts = typeof req.query.ts === 'string' ? Number.parseInt(req.query.ts, 10) : Number.NaN; + const now = Math.floor(Date.now() / 1000); + + if (Number.isNaN(ts)) { + return res.status(400).json({ + scenario: 'expected-fail/time-window', + error: 'ts query parameter is required' + }); + } + + const diff = Math.abs(now - ts); + if (diff > 2) { + return res.status(400).json({ + scenario: 'expected-fail/time-window', + diff, + now + }); + } + + return res.status(200).json({ + scenario: 'expected-fail/time-window', + diff, + now + }); +}); + +const httpServer = app.listen(config.port, () => { + info('http server listening', { + port: config.port, + sampleId: config.sampleId + }); +}); + +const proto = loadMatrixProto(config.protoPath); +const grpcServer = new grpc.Server(); + +grpcServer.addService(proto.dependencymatrix.DependencyMatrix.service, { + Ping( + call: grpc.ServerUnaryCall<{ name: string }, { message: string; sampleId: string }>, + callback: grpc.sendUnaryData<{ message: string; sampleId: string }> + ) { + callback(null, { + message: `pong:${call.request.name || 'matrix'}`, + sampleId: config.sampleId + }); + }, + + async RunDependencyScenario( + call: grpc.ServerUnaryCall<{ scenario: string }, { scenario: string; status: string; payloadJson: string }>, + callback: grpc.sendUnaryData<{ scenario: string; status: string; payloadJson: string }> + ) { + try { + const result = await runNamedScenario(config, call.request.scenario); + callback(null, { + scenario: result.scenario, + status: 'ok', + payloadJson: JSON.stringify(result.payload) + }); + } catch (err) { + callback({ + code: grpc.status.INTERNAL, + message: err instanceof Error ? err.message : String(err), + name: 'RunDependencyScenarioError' + }); + } + } +}); + +grpcServer.bindAsync(`0.0.0.0:${config.grpcPort}`, grpc.ServerCredentials.createInsecure(), (err) => { + if (err) { + error('grpc server failed to bind', { error: err.message }); + process.exitCode = 1; + return; + } + + grpcServer.start(); + info('grpc server listening', { + port: config.grpcPort + }); +}); + +const shutdown = (): void => { + info('shutdown started'); + grpcServer.tryShutdown(() => { + httpServer.close(() => { + info('shutdown complete'); + process.exit(0); + }); + }); +}; + +process.on('SIGINT', shutdown); +process.on('SIGTERM', shutdown); diff --git a/node-dependency-matrix/src/bin/dependencyFixture.ts b/node-dependency-matrix/src/bin/dependencyFixture.ts new file mode 100644 index 0000000..cee8096 --- /dev/null +++ b/node-dependency-matrix/src/bin/dependencyFixture.ts @@ -0,0 +1,106 @@ +import fs from 'node:fs'; +import http2 from 'node:http2'; +import path from 'node:path'; +import tls from 'node:tls'; + +import * as grpc from '@grpc/grpc-js'; +import express from 'express'; + +import { loadConfig } from '../lib/config'; +import { info } from '../lib/log'; +import { loadMatrixProto } from '../lib/proto'; + +const config = loadConfig(); +const cert = fs.readFileSync(config.proxyCertPath); +const key = fs.readFileSync(config.proxyKeyPath); + +const app = express(); +app.get('/http-json', (req, res) => { + res.json({ + source: 'fixture-service', + protocol: 'http', + echo: req.query.source ?? 'unknown' + }); +}); + +const httpsServer = tls.createServer({ cert, key }, (socket) => { + socket.once('data', (data) => { + const input = data.toString('utf8'); + if (input.startsWith('GET ') || input.startsWith('POST ')) { + socket.destroy(); + } + }); +}); +httpsServer.close(); + +const httpsApp = app.listen(0); +httpsApp.close(); + +require('node:https') + .createServer({ cert, key }, app) + .listen(8443, () => { + info('fixture https server listening', { port: 8443 }); + }); + +http2 + .createSecureServer({ cert, key }) + .on('stream', (stream, headers) => { + if (headers[':path'] === '/http2-ping') { + stream.respond({ + ':status': 200, + 'content-type': 'application/json' + }); + stream.end( + JSON.stringify({ + source: 'fixture-service', + protocol: 'http2', + path: '/http2-ping' + }) + ); + return; + } + + stream.respond({ ':status': 404 }); + stream.end(); + }) + .listen(9443, () => { + info('fixture http2 server listening', { port: 9443 }); + }); + +tls + .createServer({ cert, key }, (socket) => { + socket.on('data', (data) => { + const response = `fixture-ack:${data.toString('utf8').trim()}\n`; + socket.write(response); + }); + }) + .listen(9445, () => { + info('fixture generic tls server listening', { port: 9445 }); + }); + +const proto = loadMatrixProto(config.protoPath); +const grpcServer = new grpc.Server(); +grpcServer.addService(proto.dependencymatrix.DependencyFixture.service, { + GetDependencyQuote( + call: grpc.ServerUnaryCall<{ scenario: string }, { scenario: string; message: string; unixTime: number }>, + callback: grpc.sendUnaryData<{ scenario: string; message: string; unixTime: number }> + ) { + callback(null, { + scenario: call.request.scenario || 'unknown', + message: 'fixture-service-ready', + unixTime: Math.floor(Date.now() / 1000) + }); + } +}); + +grpcServer.bindAsync( + '0.0.0.0:50051', + grpc.ServerCredentials.createSsl(null, [{ cert_chain: cert, private_key: key }]), + (err) => { + if (err) { + throw err; + } + grpcServer.start(); + info('fixture grpc server listening', { port: 50051 }); + } +); diff --git a/node-dependency-matrix/src/bin/tlsProxy.ts b/node-dependency-matrix/src/bin/tlsProxy.ts new file mode 100644 index 0000000..faea562 --- /dev/null +++ b/node-dependency-matrix/src/bin/tlsProxy.ts @@ -0,0 +1,52 @@ +import fs from 'node:fs'; +import net from 'node:net'; +import tls from 'node:tls'; + +import { loadConfig } from '../lib/config'; +import { error, info } from '../lib/log'; + +const config = loadConfig(); +const listenPort = Number.parseInt(process.env.LISTEN_PORT ?? '0', 10); +const targetHost = process.env.TARGET_HOST; +const targetPort = Number.parseInt(process.env.TARGET_PORT ?? '0', 10); + +if (!listenPort || !targetHost || !targetPort) { + throw new Error('LISTEN_PORT, TARGET_HOST and TARGET_PORT are required'); +} + +const cert = fs.readFileSync(config.proxyCertPath); +const key = fs.readFileSync(config.proxyKeyPath); + +const server = tls.createServer({ cert, key }, (sourceSocket) => { + const targetSocket = net.connect(targetPort, targetHost, () => { + sourceSocket.pipe(targetSocket); + targetSocket.pipe(sourceSocket); + }); + + sourceSocket.on('error', (err) => { + error('tls proxy source socket error', { + listenPort, + targetHost, + targetPort, + error: err.message + }); + }); + + targetSocket.on('error', (err) => { + error('tls proxy target socket error', { + listenPort, + targetHost, + targetPort, + error: err.message + }); + sourceSocket.destroy(err); + }); +}); + +server.listen(listenPort, '0.0.0.0', () => { + info('tls proxy listening', { + listenPort, + targetHost, + targetPort + }); +}); diff --git a/node-dependency-matrix/src/global.d.ts b/node-dependency-matrix/src/global.d.ts new file mode 100644 index 0000000..cb2d573 --- /dev/null +++ b/node-dependency-matrix/src/global.d.ts @@ -0,0 +1,3 @@ +declare const crypto: { + randomUUID(): string; +}; diff --git a/node-dependency-matrix/src/lib/config.ts b/node-dependency-matrix/src/lib/config.ts new file mode 100644 index 0000000..98b1ad2 --- /dev/null +++ b/node-dependency-matrix/src/lib/config.ts @@ -0,0 +1,82 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export interface AppConfig { + port: number; + grpcPort: number; + sampleId: string; + fixtureHttpsBase: string; + fixtureHttp2Origin: string; + fixtureGrpcTarget: string; + fixtureGenericHost: string; + fixtureGenericPort: number; + mysqlUrl: string; + postgresUrl: string; + mongoUrl: string; + redisUrl: string; + kafkaBrokers: string[]; + kafkaTopic: string; + sqsEndpoint: string; + sqsQueueUrl: string; + sqsRegion: string; + caBundlePath: string; + proxyCertPath: string; + proxyKeyPath: string; + expectationsPath: string; + protoPath: string; +} + +function readNumber(name: string, fallback: number): number { + const raw = process.env[name]; + if (!raw) { + return fallback; + } + + const parsed = Number.parseInt(raw, 10); + return Number.isNaN(parsed) ? fallback : parsed; +} + +function splitCsv(raw: string | undefined, fallback: string[]): string[] { + if (!raw) { + return fallback; + } + return raw + .split(',') + .map((item) => item.trim()) + .filter(Boolean); +} + +export function loadConfig(): AppConfig { + const workdir = process.cwd(); + const expectationsPath = path.join(workdir, 'fixtures', 'expected-values.json'); + const protoPath = path.join(workdir, 'proto', 'dependency_matrix.proto'); + + return { + port: readNumber('PORT', 8080), + grpcPort: readNumber('GRPC_PORT', 9090), + sampleId: process.env.SAMPLE_ID ?? 'node-dependency-matrix', + fixtureHttpsBase: process.env.FIXTURE_HTTPS_BASE ?? 'https://fixture-service:8443', + fixtureHttp2Origin: process.env.FIXTURE_HTTP2_ORIGIN ?? 'https://fixture-service:9443', + fixtureGrpcTarget: process.env.FIXTURE_GRPC_TARGET ?? 'fixture-service:50051', + fixtureGenericHost: process.env.FIXTURE_GENERIC_HOST ?? 'fixture-service', + fixtureGenericPort: readNumber('FIXTURE_GENERIC_PORT', 9445), + mysqlUrl: process.env.MYSQL_URL ?? 'mysql://root@mysql:3306/matrix', + postgresUrl: process.env.POSTGRES_URL ?? 'postgresql://postgres@postgres:5432/matrix', + mongoUrl: process.env.MONGO_URL ?? 'mongodb://mongo-tls:27017/matrix?tls=true', + redisUrl: process.env.REDIS_URL ?? 'rediss://redis-tls:6380', + kafkaBrokers: splitCsv(process.env.KAFKA_BROKERS, ['redpanda:9092']), + kafkaTopic: process.env.KAFKA_TOPIC ?? 'matrix-events', + sqsEndpoint: process.env.SQS_ENDPOINT ?? 'https://sqs-tls:4567', + sqsQueueUrl: process.env.SQS_QUEUE_URL ?? 'https://sqs-tls:4567/000000000000/dependency-matrix', + sqsRegion: process.env.SQS_REGION ?? 'us-east-1', + caBundlePath: process.env.COMBINED_CA_CERT_PATH ?? process.env.NODE_EXTRA_CA_CERTS ?? '/tmp/node-dependency-matrix-ca-bundle.crt', + proxyCertPath: process.env.TLS_CERT_PATH ?? '/etc/sample-certs/proxy.crt', + proxyKeyPath: process.env.TLS_KEY_PATH ?? '/etc/sample-certs/proxy.key', + expectationsPath, + protoPath + }; +} + +export function readExpectationsJson(config: AppConfig): string { + return fs.readFileSync(config.expectationsPath, 'utf8'); +} diff --git a/node-dependency-matrix/src/lib/dependencies.ts b/node-dependency-matrix/src/lib/dependencies.ts new file mode 100644 index 0000000..c21e471 --- /dev/null +++ b/node-dependency-matrix/src/lib/dependencies.ts @@ -0,0 +1,481 @@ +import fs from 'node:fs'; +import http2 from 'node:http2'; +import https from 'node:https'; +import tls from 'node:tls'; +import { promisify } from 'node:util'; +import { URL } from 'node:url'; + +import { DeleteMessageCommand, ReceiveMessageCommand, SendMessageCommand, SQSClient } from '@aws-sdk/client-sqs'; +import * as grpc from '@grpc/grpc-js'; +import { Kafka } from 'kafkajs'; +import Redis from 'ioredis'; +import { MongoClient } from 'mongodb'; +import mysql from 'mysql2/promise'; +import { Client as PgClient } from 'pg'; +import { NodeHttpHandler } from '@smithy/node-http-handler'; + +import { AppConfig } from './config'; +import { error, info } from './log'; +import { FixtureGrpcClient, loadMatrixProto } from './proto'; + +type ScenarioPayload = Record; + +export interface ScenarioResult { + scenario: string; + protocol: string; + payload: ScenarioPayload; +} + +function readCaBundle(config: AppConfig): Buffer { + return fs.readFileSync(config.caBundlePath); +} + +export async function runHttpScenario(config: AppConfig): Promise { + const target = new URL(`${config.fixtureHttpsBase}/http-json?source=app`); + const payload = await new Promise<{ status: number; body: ScenarioPayload }>((resolve, reject) => { + const request = https.request( + { + protocol: target.protocol, + hostname: target.hostname, + port: target.port, + path: `${target.pathname}${target.search}`, + method: 'GET', + ca: readCaBundle(config), + rejectUnauthorized: true + }, + (response) => { + const chunks: Buffer[] = []; + response.on('data', (chunk) => chunks.push(Buffer.from(chunk))); + response.on('end', () => { + const body = Buffer.concat(chunks).toString('utf8'); + resolve({ + status: response.statusCode ?? 0, + body: JSON.parse(body) as ScenarioPayload + }); + }); + } + ); + + request.on('error', reject); + request.end(); + }); + + return { + scenario: 'deps-http', + protocol: 'Http', + payload: { + status: payload.status, + body: payload.body + } + }; +} + +export async function runHttp2Scenario(config: AppConfig): Promise { + const client = http2.connect(config.fixtureHttp2Origin, { + ca: readCaBundle(config) + }); + + try { + const payload = await new Promise((resolve, reject) => { + const request = client.request({ + ':method': 'GET', + ':path': '/http2-ping' + }); + + const chunks: Buffer[] = []; + + request.on('response', () => undefined); + request.on('data', (chunk) => chunks.push(Buffer.from(chunk))); + request.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); + request.on('error', reject); + request.end(); + }); + + return { + scenario: 'deps-http2', + protocol: 'Http2', + payload: JSON.parse(payload) as ScenarioPayload + }; + } finally { + client.close(); + } +} + +export async function runGrpcScenario(config: AppConfig): Promise { + const proto = loadMatrixProto(config.protoPath); + const ClientCtor = proto.dependencymatrix.DependencyFixture; + const client = new ClientCtor( + config.fixtureGrpcTarget, + grpc.credentials.createSsl(readCaBundle(config)) + ) as unknown as FixtureGrpcClient; + const getQuote = promisify<{ scenario: string }, { scenario: string; message: string; unixTime: string | number }>( + client.GetDependencyQuote.bind(client) + ); + + try { + const response = await getQuote({ scenario: 'deps-grpc' }); + + return { + scenario: 'deps-grpc', + protocol: 'gRPC', + payload: { + scenario: response.scenario, + message: response.message, + unixTime: Number(response.unixTime) + } + }; + } finally { + client.close(); + } +} + +export async function runMySqlScenario(config: AppConfig): Promise { + const connection = await mysql.createConnection({ + uri: config.mysqlUrl + }); + + try { + await connection.execute(` + CREATE TABLE IF NOT EXISTS matrix_events ( + sample_key VARCHAR(64) PRIMARY KEY, + sample_value VARCHAR(128) NOT NULL, + touched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + `); + + await connection.execute( + ` + INSERT INTO matrix_events (sample_key, sample_value) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE sample_value = VALUES(sample_value) + `, + ['mysql-scenario', 'mysql-value'] + ); + + const [rows] = await connection.query( + 'SELECT sample_key, sample_value FROM matrix_events WHERE sample_key = ?', + ['mysql-scenario'] + ); + + return { + scenario: 'deps-mysql', + protocol: 'MySQL', + payload: { + rowCount: rows.length, + row: rows[0] ?? null + } + }; + } finally { + await connection.end(); + } +} + +export async function runPostgresScenario(config: AppConfig): Promise { + const client = new PgClient({ + connectionString: config.postgresUrl + }); + + await client.connect(); + + try { + await client.query(` + CREATE TABLE IF NOT EXISTS matrix_events ( + sample_key TEXT PRIMARY KEY, + sample_value TEXT NOT NULL + ) + `); + + await client.query( + ` + INSERT INTO matrix_events (sample_key, sample_value) + VALUES ($1, $2) + ON CONFLICT (sample_key) DO UPDATE SET sample_value = EXCLUDED.sample_value + `, + ['postgres-scenario', 'postgres-value'] + ); + + const result = await client.query( + 'SELECT sample_key, sample_value FROM matrix_events WHERE sample_key = $1', + ['postgres-scenario'] + ); + + return { + scenario: 'deps-postgres', + protocol: 'PostgresV2', + payload: { + rowCount: result.rowCount, + row: result.rows[0] ?? null + } + }; + } finally { + await client.end(); + } +} + +export async function runMongoScenario(config: AppConfig): Promise { + const client = new MongoClient(config.mongoUrl, { + tlsCAFile: config.caBundlePath + }); + await client.connect(); + + try { + const database = client.db('matrix'); + const collection = database.collection('events'); + + await collection.updateOne( + { sampleKey: 'mongo-scenario' }, + { + $set: { + sampleValue: 'mongo-value', + updatedAt: new Date().toISOString() + } + }, + { upsert: true } + ); + + const document = await collection.findOne({ sampleKey: 'mongo-scenario' }, { projection: { _id: 0 } }); + + return { + scenario: 'deps-mongo', + protocol: 'Mongo', + payload: document ?? {} + }; + } finally { + await client.close(); + } +} + +export async function runRedisScenario(config: AppConfig): Promise { + const client = new Redis(config.redisUrl, { + tls: { + ca: readCaBundle(config), + rejectUnauthorized: true + }, + lazyConnect: true, + maxRetriesPerRequest: 1 + }); + + await client.connect(); + + try { + await client.set('matrix:redis-scenario', 'redis-value'); + const value = await client.get('matrix:redis-scenario'); + + return { + scenario: 'deps-redis', + protocol: 'Redis', + payload: { + value + } + }; + } finally { + client.disconnect(); + } +} + +export async function runKafkaScenario(config: AppConfig): Promise { + const kafka = new Kafka({ + clientId: config.sampleId, + brokers: config.kafkaBrokers, + ssl: false + }); + + const admin = kafka.admin(); + const producer = kafka.producer(); + + await admin.connect(); + + try { + await admin.createTopics({ + waitForLeaders: true, + topics: [ + { + topic: config.kafkaTopic, + numPartitions: 1, + replicationFactor: 1 + } + ] + }).catch((err: unknown) => { + info('kafka topic create returned non-fatal error', { + scenario: 'deps-kafka', + error: err instanceof Error ? err.message : String(err) + }); + }); + + await producer.connect(); + const produceResult = await producer.send({ + topic: config.kafkaTopic, + messages: [ + { + key: 'matrix-event', + value: JSON.stringify({ + sampleId: config.sampleId, + sentAt: new Date().toISOString() + }) + } + ] + }); + + return { + scenario: 'deps-kafka', + protocol: 'Kafka', + payload: { + produced: produceResult + } + }; + } finally { + await producer.disconnect().catch(() => undefined); + await admin.disconnect().catch(() => undefined); + } +} + +export async function runSqsScenario(config: AppConfig): Promise { + const client = new SQSClient({ + endpoint: config.sqsEndpoint, + region: config.sqsRegion, + requestHandler: new NodeHttpHandler({ + httpsAgent: new https.Agent({ + ca: readCaBundle(config), + rejectUnauthorized: true + }) + }), + credentials: { + accessKeyId: 'test', + secretAccessKey: 'test' + } + }); + + const sendResult = await client.send( + new SendMessageCommand({ + QueueUrl: config.sqsQueueUrl, + MessageBody: JSON.stringify({ + sampleId: config.sampleId, + scenario: 'deps-sqs', + sentAt: new Date().toISOString() + }) + }) + ); + + const receiveResult = await client.send( + new ReceiveMessageCommand({ + QueueUrl: config.sqsQueueUrl, + MaxNumberOfMessages: 1, + WaitTimeSeconds: 0 + }) + ); + + const receiptHandle = receiveResult.Messages?.[0]?.ReceiptHandle; + if (receiptHandle) { + await client.send( + new DeleteMessageCommand({ + QueueUrl: config.sqsQueueUrl, + ReceiptHandle: receiptHandle + }) + ); + } + + return { + scenario: 'deps-sqs', + protocol: 'SQS', + payload: { + sentMessageId: sendResult.MessageId ?? null, + receivedMessages: receiveResult.Messages?.length ?? 0 + } + }; +} + +export async function runGenericScenario(config: AppConfig): Promise { + const payload = await new Promise((resolve, reject) => { + const socket = tls.connect( + { + host: config.fixtureGenericHost, + port: config.fixtureGenericPort, + ca: readCaBundle(config), + rejectUnauthorized: true + }, + () => { + socket.write('matrix-generic\n'); + } + ); + + socket.on('data', (data) => { + resolve(data.toString('utf8').trim()); + socket.end(); + }); + socket.on('error', reject); + }); + + return { + scenario: 'deps-generic', + protocol: 'Generic', + payload: { + echoed: payload + } + }; +} + +export async function runNamedScenario(config: AppConfig, scenario: string): Promise { + switch (scenario) { + case 'http': + case 'deps-http': + return runHttpScenario(config); + case 'http2': + case 'deps-http2': + return runHttp2Scenario(config); + case 'grpc': + case 'deps-grpc': + return runGrpcScenario(config); + case 'mysql': + case 'deps-mysql': + return runMySqlScenario(config); + case 'postgres': + case 'deps-postgres': + return runPostgresScenario(config); + case 'mongo': + case 'deps-mongo': + return runMongoScenario(config); + case 'redis': + case 'deps-redis': + return runRedisScenario(config); + case 'kafka': + case 'deps-kafka': + return runKafkaScenario(config); + case 'sqs': + case 'deps-sqs': + return runSqsScenario(config); + case 'generic': + case 'deps-generic': + return runGenericScenario(config); + default: + throw new Error(`unsupported scenario: ${scenario}`); + } +} + +export async function runAllDependencyScenarios(config: AppConfig): Promise { + const runners = [ + runHttpScenario, + runHttp2Scenario, + runGrpcScenario, + runMySqlScenario, + runPostgresScenario, + runMongoScenario, + runRedisScenario, + runKafkaScenario, + runSqsScenario, + runGenericScenario + ]; + + const results: ScenarioResult[] = []; + for (const runner of runners) { + try { + results.push(await runner(config)); + } catch (err) { + error('dependency scenario failed', { + scenario: runner.name, + error: err instanceof Error ? err.message : String(err) + }); + throw err; + } + } + return results; +} diff --git a/node-dependency-matrix/src/lib/log.ts b/node-dependency-matrix/src/lib/log.ts new file mode 100644 index 0000000..dbd0e15 --- /dev/null +++ b/node-dependency-matrix/src/lib/log.ts @@ -0,0 +1,20 @@ +type LogLevel = 'INFO' | 'ERROR'; + +function log(level: LogLevel, message: string, details: Record = {}): void { + const payload = { + level, + message, + ts: new Date().toISOString(), + ...details + }; + + console.log(JSON.stringify(payload)); +} + +export function info(message: string, details: Record = {}): void { + log('INFO', message, details); +} + +export function error(message: string, details: Record = {}): void { + log('ERROR', message, details); +} diff --git a/node-dependency-matrix/src/lib/proto.ts b/node-dependency-matrix/src/lib/proto.ts new file mode 100644 index 0000000..222ec28 --- /dev/null +++ b/node-dependency-matrix/src/lib/proto.ts @@ -0,0 +1,44 @@ +import path from 'node:path'; + +import * as grpc from '@grpc/grpc-js'; +import * as protoLoader from '@grpc/proto-loader'; + +const loaderOptions: protoLoader.Options = { + keepCase: false, + longs: String, + enums: String, + defaults: true, + oneofs: true +}; + +export interface FixtureGrpcClient extends grpc.Client { + GetDependencyQuote( + request: { scenario: string }, + callback: (error: grpc.ServiceError | null, response: { scenario: string; message: string; unixTime: string | number }) => void + ): void; +} + +export interface MatrixGrpcClient extends grpc.Client { + Ping( + request: { name: string }, + callback: (error: grpc.ServiceError | null, response: { message: string; sampleId: string }) => void + ): void; + + RunDependencyScenario( + request: { scenario: string }, + callback: (error: grpc.ServiceError | null, response: { scenario: string; status: string; payloadJson: string }) => void + ): void; +} + +export interface LoadedProto { + dependencymatrix: { + DependencyFixture: grpc.ServiceClientConstructor; + DependencyMatrix: grpc.ServiceClientConstructor; + }; +} + +export function loadMatrixProto(protoPath: string): LoadedProto { + const absolutePath = path.resolve(protoPath); + const packageDefinition = protoLoader.loadSync(absolutePath, loaderOptions); + return grpc.loadPackageDefinition(packageDefinition) as unknown as LoadedProto; +} diff --git a/node-dependency-matrix/tsconfig.json b/node-dependency-matrix/tsconfig.json new file mode 100644 index 0000000..97b1ff3 --- /dev/null +++ b/node-dependency-matrix/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "rootDir": "./src", + "outDir": "./dist", + "moduleResolution": "node", + "resolveJsonModule": true, + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} From 4759af3d73d75a927eda34a239fb42257c839dca Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Tue, 24 Mar 2026 17:54:59 +0530 Subject: [PATCH 02/19] Add compose setup for node matrix sample --- node-dependency-matrix/README.md | 26 +++ node-dependency-matrix/compose.yaml | 205 ++++++++++++++++++ node-dependency-matrix/k8s/README.md | 37 ++++ .../scripts/compose_down.sh | 6 + node-dependency-matrix/scripts/compose_up.sh | 12 + .../localstack-init/01-create-queue.sh | 4 + 6 files changed, 290 insertions(+) create mode 100644 node-dependency-matrix/compose.yaml create mode 100755 node-dependency-matrix/scripts/compose_down.sh create mode 100755 node-dependency-matrix/scripts/compose_up.sh create mode 100755 node-dependency-matrix/scripts/localstack-init/01-create-queue.sh diff --git a/node-dependency-matrix/README.md b/node-dependency-matrix/README.md index 04a9450..5903729 100644 --- a/node-dependency-matrix/README.md +++ b/node-dependency-matrix/README.md @@ -43,6 +43,32 @@ Optional incoming gRPC traffic: GRPC_TARGET=localhost:30090 bash scripts/send_grpc_traffic.sh ``` +## Docker Compose + +For a non-Kubernetes local stack: + +```bash +cd samples-typescript/node-dependency-matrix +npm install +bash scripts/compose_up.sh +APP_URL=http://localhost:38081 bash scripts/record_traffic.sh +GRPC_TARGET=localhost:39090 bash scripts/send_grpc_traffic.sh +``` + +To stop it: + +```bash +bash scripts/compose_down.sh +``` + +If `38081` or `39090` are occupied: + +```bash +APP_HTTP_PORT=48081 APP_GRPC_PORT=49090 bash scripts/compose_up.sh +APP_URL=http://localhost:48081 bash scripts/record_traffic.sh +GRPC_TARGET=localhost:49090 bash scripts/send_grpc_traffic.sh +``` + ## Keploy expectations The machine-readable contract is: diff --git a/node-dependency-matrix/compose.yaml b/node-dependency-matrix/compose.yaml new file mode 100644 index 0000000..614e8a2 --- /dev/null +++ b/node-dependency-matrix/compose.yaml @@ -0,0 +1,205 @@ +name: node-dependency-matrix + +x-node-matrix-image: &node_matrix_image + build: + context: . + image: node-dependency-matrix:compose + +services: + app: + <<: *node_matrix_image + depends_on: + fixture-service: + condition: service_started + mysql: + condition: service_healthy + postgres: + condition: service_healthy + mongo-tls: + condition: service_started + redis-tls: + condition: service_started + redpanda: + condition: service_started + sqs-tls: + condition: service_started + environment: + PORT: "8080" + GRPC_PORT: "9090" + FIXTURE_HTTPS_BASE: https://fixture-service:8443 + FIXTURE_HTTP2_ORIGIN: https://fixture-service:9443 + FIXTURE_GRPC_TARGET: fixture-service:50051 + FIXTURE_GENERIC_HOST: fixture-service + FIXTURE_GENERIC_PORT: "9445" + MYSQL_URL: mysql://root@mysql:3306/matrix + POSTGRES_URL: postgresql://postgres@postgres:5432/matrix + MONGO_URL: mongodb://mongo-tls:27017/matrix?tls=true + REDIS_URL: rediss://redis-tls:6380 + KAFKA_BROKERS: redpanda:9092 + KAFKA_TOPIC: matrix-events + SQS_ENDPOINT: https://sqs-tls:4567 + SQS_QUEUE_URL: https://sqs-tls:4567/000000000000/dependency-matrix + SAMPLE_CA_CERT_PATH: /etc/sample-certs/ca.crt + COMBINED_CA_CERT_PATH: /tmp/node-dependency-matrix-ca-bundle.crt + NODE_EXTRA_CA_CERTS: /tmp/node-dependency-matrix-ca-bundle.crt + SSL_CERT_FILE: /tmp/node-dependency-matrix-ca-bundle.crt + ports: + - "${APP_HTTP_PORT:-38081}:8080" + - "${APP_GRPC_PORT:-39090}:9090" + volumes: + - ./.generated/certs:/etc/sample-certs:ro + + fixture-service: + <<: *node_matrix_image + command: ["node", "dist/bin/dependencyFixture.js"] + environment: + TLS_CERT_PATH: /etc/sample-certs/proxy.crt + TLS_KEY_PATH: /etc/sample-certs/proxy.key + volumes: + - ./.generated/certs:/etc/sample-certs:ro + + mysql: + image: mysql:8.0 + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: "yes" + MYSQL_DATABASE: matrix + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot"] + interval: 5s + timeout: 5s + retries: 20 + + postgres: + image: postgres:16 + environment: + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_DB: matrix + POSTGRES_USER: postgres + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d matrix"] + interval: 5s + timeout: 5s + retries: 20 + + mongo: + image: mongo:7 + command: ["--bind_ip_all"] + + redis: + image: redis:7-alpine + + redpanda: + image: docker.redpanda.com/redpandadata/redpanda:v25.1.2 + command: + - redpanda + - start + - --overprovisioned + - --smp + - "1" + - --memory + - 512M + - --reserve-memory + - 0M + - --check=false + - --node-id + - "0" + - --kafka-addr + - PLAINTEXT://0.0.0.0:9092 + - --advertise-kafka-addr + - PLAINTEXT://redpanda:9092 + - --set + - redpanda.auto_create_topics_enabled=true + + localstack: + image: localstack/localstack:3.3 + environment: + SERVICES: sqs + AWS_DEFAULT_REGION: us-east-1 + volumes: + - ./scripts/localstack-init:/etc/localstack/init/ready.d:ro + + mysql-tls: + <<: *node_matrix_image + command: ["node", "dist/bin/tlsProxy.js"] + depends_on: + mysql: + condition: service_healthy + environment: + LISTEN_PORT: "3306" + TARGET_HOST: mysql + TARGET_PORT: "3306" + TLS_CERT_PATH: /etc/sample-certs/proxy.crt + TLS_KEY_PATH: /etc/sample-certs/proxy.key + volumes: + - ./.generated/certs:/etc/sample-certs:ro + + postgres-tls: + <<: *node_matrix_image + command: ["node", "dist/bin/tlsProxy.js"] + depends_on: + postgres: + condition: service_healthy + environment: + LISTEN_PORT: "5432" + TARGET_HOST: postgres + TARGET_PORT: "5432" + TLS_CERT_PATH: /etc/sample-certs/proxy.crt + TLS_KEY_PATH: /etc/sample-certs/proxy.key + volumes: + - ./.generated/certs:/etc/sample-certs:ro + + mongo-tls: + <<: *node_matrix_image + command: ["node", "dist/bin/tlsProxy.js"] + depends_on: + - mongo + environment: + LISTEN_PORT: "27017" + TARGET_HOST: mongo + TARGET_PORT: "27017" + TLS_CERT_PATH: /etc/sample-certs/proxy.crt + TLS_KEY_PATH: /etc/sample-certs/proxy.key + volumes: + - ./.generated/certs:/etc/sample-certs:ro + + redis-tls: + <<: *node_matrix_image + command: ["node", "dist/bin/tlsProxy.js"] + depends_on: + - redis + environment: + LISTEN_PORT: "6380" + TARGET_HOST: redis + TARGET_PORT: "6379" + TLS_CERT_PATH: /etc/sample-certs/proxy.crt + TLS_KEY_PATH: /etc/sample-certs/proxy.key + volumes: + - ./.generated/certs:/etc/sample-certs:ro + + kafka-tls: + <<: *node_matrix_image + command: ["node", "dist/bin/tlsProxy.js"] + depends_on: + - redpanda + environment: + LISTEN_PORT: "9094" + TARGET_HOST: redpanda + TARGET_PORT: "9092" + TLS_CERT_PATH: /etc/sample-certs/proxy.crt + TLS_KEY_PATH: /etc/sample-certs/proxy.key + volumes: + - ./.generated/certs:/etc/sample-certs:ro + + sqs-tls: + <<: *node_matrix_image + command: ["node", "dist/bin/tlsProxy.js"] + depends_on: + - localstack + environment: + LISTEN_PORT: "4567" + TARGET_HOST: localstack + TARGET_PORT: "4566" + TLS_CERT_PATH: /etc/sample-certs/proxy.crt + TLS_KEY_PATH: /etc/sample-certs/proxy.key + volumes: + - ./.generated/certs:/etc/sample-certs:ro diff --git a/node-dependency-matrix/k8s/README.md b/node-dependency-matrix/k8s/README.md index 3c8aeec..b58671f 100644 --- a/node-dependency-matrix/k8s/README.md +++ b/node-dependency-matrix/k8s/README.md @@ -76,6 +76,34 @@ Hosted staging UI note: - if your sample app is on custom host ports, only the proxy ingress URL needs to change in the Keploy cluster registration flow - the app traffic scripts should use the custom app/grpc host ports you selected locally +- do not run local `enterprise-ui` or local `api-server` for this flow; use the hosted staging UI and the Helm command it generates +- after the proxy install, verify the cluster status turns `Active` before you try to record + +## 3A. What the Helm install must do + +When you create the cluster in staging UI and copy the Helm command, the effective values must point to staging, not production: + +- `keploy.clusterName=node-dependency-matrix` +- `keploy.apiServerUrl=https://api.staging.keploy.io` +- `environment=staging` +- `keploy.ingressUrl=http://localhost:30080` +- `service.type=NodePort` +- `service.nodePort=30080` + +If you used a custom proxy host port, replace `30080` with that custom port in the cluster ingress URL and in the Helm values. + +After the Helm install, verify: + +```bash +kubectl get pods -n keploy +kubectl logs -n keploy -l app=k8s-proxy --tail=50 -f +``` + +What you want to see: + +- the `k8s-proxy` pod is `Running` +- no repeated `401` login failures +- the cluster page in staging UI shows `Active` ## 4. Start recording @@ -104,6 +132,15 @@ APP_URL=http://localhost:31081 bash scripts/record_traffic.sh GRPC_TARGET=localhost:31090 bash scripts/send_grpc_traffic.sh ``` +Expected record-time behavior: + +- the deployment should restart when recording starts +- the restarted pod should become `2/2` +- the HTTP script should create 17 HTTP testcases +- the gRPC script should add 2 more incoming testcases +- the noisy endpoint should appear in replay as noisy +- the time-window endpoint should be an expected replay failure + ## 6. Expectations The machine-readable contract is in: diff --git a/node-dependency-matrix/scripts/compose_down.sh b/node-dependency-matrix/scripts/compose_down.sh new file mode 100755 index 0000000..fa270e1 --- /dev/null +++ b/node-dependency-matrix/scripts/compose_down.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +docker compose -f "${ROOT_DIR}/compose.yaml" down -v "$@" diff --git a/node-dependency-matrix/scripts/compose_up.sh b/node-dependency-matrix/scripts/compose_up.sh new file mode 100755 index 0000000..8a781dc --- /dev/null +++ b/node-dependency-matrix/scripts/compose_up.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +bash "${ROOT_DIR}/scripts/generate_certs.sh" "${ROOT_DIR}/.generated/certs" + +docker compose -f "${ROOT_DIR}/compose.yaml" up --build -d "$@" + +echo "" +echo "Compose app HTTP: http://localhost:${APP_HTTP_PORT:-38081}" +echo "Compose app gRPC: localhost:${APP_GRPC_PORT:-39090}" diff --git a/node-dependency-matrix/scripts/localstack-init/01-create-queue.sh b/node-dependency-matrix/scripts/localstack-init/01-create-queue.sh new file mode 100755 index 0000000..b9409b3 --- /dev/null +++ b/node-dependency-matrix/scripts/localstack-init/01-create-queue.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -eu + +awslocal sqs create-queue --queue-name dependency-matrix >/dev/null 2>&1 || true From 36ceeacc0731d05d073df6549c2b29fb1c8db44e Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Tue, 24 Mar 2026 18:07:34 +0530 Subject: [PATCH 03/19] docs: tighten node dependency matrix k8s flow --- node-dependency-matrix/README.md | 5 +- .../fixtures/expected-values.json | 396 +++++++++++++++++- node-dependency-matrix/k8s/README.md | 258 +++++++++--- node-dependency-matrix/k8s/deploy-kind.sh | 18 +- node-dependency-matrix/k8s/port-forward.sh | 13 + 5 files changed, 626 insertions(+), 64 deletions(-) create mode 100755 node-dependency-matrix/k8s/port-forward.sh diff --git a/node-dependency-matrix/README.md b/node-dependency-matrix/README.md index 5903729..a06e5c4 100644 --- a/node-dependency-matrix/README.md +++ b/node-dependency-matrix/README.md @@ -81,13 +81,16 @@ Use it later in Playwright or manual verification for: - testcase counts - required and acceptable mock kinds +- scenario-to-mock-kind mapping +- the exact HTTP and gRPC traffic plans +- hosted UI + Helm expectations for Kind - total mock count ranges - dedup expectations - expected replay pass/noisy/fail scenarios ## Kubernetes -The full Kind and staging UI flow is in: +The full Kind and hosted UI flow is in: ```bash k8s/README.md diff --git a/node-dependency-matrix/fixtures/expected-values.json b/node-dependency-matrix/fixtures/expected-values.json index 42e09a0..9a21b69 100644 --- a/node-dependency-matrix/fixtures/expected-values.json +++ b/node-dependency-matrix/fixtures/expected-values.json @@ -4,20 +4,305 @@ "namespace": "default", "deployment": "node-dependency-matrix", "service": "node-dependency-matrix", + "podLabelSelector": "app=node-dependency-matrix", + "httpServicePort": 8080, + "grpcServicePort": 9090, "httpNodePort": 30081, "grpcNodePort": 30090, "k8sProxyNodePort": 30080 }, + "coverage": { + "incoming": { + "httpEndpoints": [ + "/health", + "/expectations", + "/deps/http", + "/deps/http2", + "/deps/grpc", + "/deps/mysql", + "/deps/postgres", + "/deps/mongo", + "/deps/redis", + "/deps/kafka", + "/deps/sqs", + "/deps/generic", + "/deps/all", + "/dedup/catalog", + "/noise/runtime", + "/expected-fail/time-window" + ], + "grpcMethods": [ + "/dependencymatrix.DependencyMatrix/Ping", + "/dependencymatrix.DependencyMatrix/RunDependencyScenario" + ] + }, + "transportNotes": { + "incomingHttp": "plaintext HTTP on the sample service", + "incomingGrpc": "plaintext gRPC on the sample service", + "outgoingHttp": "HTTPS to fixture-service", + "outgoingHttp2": "TLS HTTP/2 to fixture-service", + "outgoingGrpc": "TLS gRPC to fixture-service", + "outgoingMySql": "direct MySQL protocol to mysql:3306", + "outgoingPostgres": "direct Postgres protocol to postgres:5432", + "outgoingMongo": "Mongo over TLS through mongo-tls", + "outgoingRedis": "Redis over TLS through redis-tls", + "outgoingKafka": "direct Kafka protocol to redpanda:9092", + "outgoingSqs": "HTTPS to sqs-tls", + "outgoingGeneric": "raw TLS socket to fixture-service:9445" + }, + "outgoingPreferredMockKinds": [ + "Http", + "Http2", + "gRPC", + "MySQL", + "PostgresV2", + "Mongo", + "Redis", + "Kafka", + "Generic" + ], + "enterpriseSpecificOutgoingIntegrations": [ + "Redis", + "Kafka", + "SQS" + ], + "knownNonGoals": [ + "incoming TLS gRPC is not exercised in the default flow", + "the HTTP ingress coverage is protocol-focused and GET-only", + "mysql-tls, postgres-tls, and kafka-tls deployments exist but are not used by the default app path" + ] + }, + "hostedUi": { + "supportedEnvironments": { + "prod": { + "uiBaseUrl": "https://app.keploy.io", + "clustersPageUrl": "https://app.keploy.io/clusters", + "apiServerUrl": "https://api.keploy.io", + "environment": "prod" + }, + "staging": { + "uiBaseUrl": "https://app.staging.keploy.io", + "clustersPageUrl": "https://app.staging.keploy.io/clusters", + "apiServerUrl": "https://api.staging.keploy.io", + "environment": "staging" + } + }, + "requiredHelmValues": { + "service.type": "NodePort", + "service.nodePort": 30080, + "proxy.insecure.enabled": true + }, + "consistencyRules": [ + "use the same environment in the UI domain, apiServerUrl, and Helm environment value", + "use the same ingress URL in the UI cluster entry and keploy.ingressUrl" + ] + }, + "playwright": { + "portForward": { + "script": "k8s/port-forward.sh", + "resource": "svc/node-dependency-matrix", + "http": { + "localPort": 8080, + "remotePort": 8080, + "baseUrl": "http://localhost:8080" + }, + "grpc": { + "localPort": 9090, + "remotePort": 9090, + "target": "localhost:9090" + } + } + }, "recording": { "pods": { "expectedReplicas": 1, "expectedReadyContainersWhileRecording": 2 }, "httpTrafficScript": { - "expectedTestcasesExact": 17 + "path": "scripts/record_traffic.sh", + "expectedTestcasesExact": 17, + "requests": [ + { + "method": "GET", + "path": "/deps/http", + "count": 1, + "preferredMockKinds": [ + "Http" + ], + "acceptableMockKinds": [ + "Http" + ], + "replayExpectation": "pass" + }, + { + "method": "GET", + "path": "/deps/http2", + "count": 1, + "preferredMockKinds": [ + "Http2" + ], + "acceptableMockKinds": [ + "Http2", + "Generic", + "Http" + ], + "replayExpectation": "pass" + }, + { + "method": "GET", + "path": "/deps/grpc", + "count": 1, + "preferredMockKinds": [ + "gRPC" + ], + "acceptableMockKinds": [ + "gRPC", + "Generic", + "Http2" + ], + "replayExpectation": "pass" + }, + { + "method": "GET", + "path": "/deps/mysql", + "count": 1, + "preferredMockKinds": [ + "MySQL" + ], + "acceptableMockKinds": [ + "MySQL" + ], + "replayExpectation": "pass" + }, + { + "method": "GET", + "path": "/deps/postgres", + "count": 1, + "preferredMockKinds": [ + "PostgresV2" + ], + "acceptableMockKinds": [ + "PostgresV2", + "Generic" + ], + "replayExpectation": "pass" + }, + { + "method": "GET", + "path": "/deps/mongo", + "count": 1, + "preferredMockKinds": [ + "Mongo" + ], + "acceptableMockKinds": [ + "Mongo", + "Generic" + ], + "replayExpectation": "pass" + }, + { + "method": "GET", + "path": "/deps/redis", + "count": 1, + "preferredMockKinds": [ + "Redis" + ], + "acceptableMockKinds": [ + "Redis", + "Generic" + ], + "replayExpectation": "pass" + }, + { + "method": "GET", + "path": "/deps/kafka", + "count": 1, + "preferredMockKinds": [ + "Kafka" + ], + "acceptableMockKinds": [ + "Kafka", + "Generic" + ], + "replayExpectation": "pass" + }, + { + "method": "GET", + "path": "/deps/sqs", + "count": 1, + "preferredMockKinds": [ + "Http" + ], + "acceptableMockKinds": [ + "Http", + "Generic" + ], + "replayExpectation": "pass" + }, + { + "method": "GET", + "path": "/deps/generic", + "count": 1, + "preferredMockKinds": [ + "Generic" + ], + "acceptableMockKinds": [ + "Generic" + ], + "replayExpectation": "pass" + }, + { + "method": "GET", + "path": "/dedup/catalog?id=alpha", + "count": 3, + "replayExpectation": "pass", + "dedupExpectation": { + "duplicateCount": 2 + } + }, + { + "method": "GET", + "path": "/dedup/catalog?id=beta", + "count": 2, + "replayExpectation": "pass", + "dedupExpectation": { + "duplicateCount": 1 + } + }, + { + "method": "GET", + "path": "/noise/runtime", + "count": 1, + "replayExpectation": "noisy" + }, + { + "method": "GET", + "path": "/expected-fail/time-window?ts=", + "count": 1, + "replayExpectation": "fail" + } + ] }, "grpcTrafficScript": { - "expectedAdditionalTestcasesExact": 2 + "path": "scripts/send_grpc_traffic.sh", + "expectedAdditionalTestcasesExact": 2, + "calls": [ + { + "method": "/dependencymatrix.DependencyMatrix/Ping", + "request": { + "name": "matrix" + }, + "expectedAdditionalTestcases": 1 + }, + { + "method": "/dependencymatrix.DependencyMatrix/RunDependencyScenario", + "request": { + "scenario": "deps-http" + }, + "expectedAdditionalTestcases": 1, + "invokesScenario": "deps-http" + } + ] }, "mockSummary": { "total": { @@ -65,6 +350,112 @@ } }, "replay": { + "scenarioCatalog": [ + { + "scenario": "deps-http", + "triggerPath": "/deps/http", + "replayExpectation": "pass", + "acceptableMockKinds": [ + "Http" + ] + }, + { + "scenario": "deps-http2", + "triggerPath": "/deps/http2", + "replayExpectation": "pass", + "acceptableMockKinds": [ + "Http2", + "Generic", + "Http" + ] + }, + { + "scenario": "deps-grpc", + "triggerPath": "/deps/grpc", + "replayExpectation": "pass", + "acceptableMockKinds": [ + "gRPC", + "Generic", + "Http2" + ] + }, + { + "scenario": "deps-mysql", + "triggerPath": "/deps/mysql", + "replayExpectation": "pass", + "acceptableMockKinds": [ + "MySQL" + ] + }, + { + "scenario": "deps-postgres", + "triggerPath": "/deps/postgres", + "replayExpectation": "pass", + "acceptableMockKinds": [ + "PostgresV2", + "Generic" + ] + }, + { + "scenario": "deps-mongo", + "triggerPath": "/deps/mongo", + "replayExpectation": "pass", + "acceptableMockKinds": [ + "Mongo", + "Generic" + ] + }, + { + "scenario": "deps-redis", + "triggerPath": "/deps/redis", + "replayExpectation": "pass", + "acceptableMockKinds": [ + "Redis", + "Generic" + ] + }, + { + "scenario": "deps-kafka", + "triggerPath": "/deps/kafka", + "replayExpectation": "pass", + "acceptableMockKinds": [ + "Kafka", + "Generic" + ] + }, + { + "scenario": "deps-sqs", + "triggerPath": "/deps/sqs", + "replayExpectation": "pass", + "acceptableMockKinds": [ + "Http", + "Generic" + ] + }, + { + "scenario": "deps-generic", + "triggerPath": "/deps/generic", + "replayExpectation": "pass", + "acceptableMockKinds": [ + "Generic" + ] + }, + { + "scenario": "dedup/catalog", + "triggerPath": "/dedup/catalog", + "replayExpectation": "pass" + }, + { + "scenario": "noise/runtime", + "triggerPath": "/noise/runtime", + "replayExpectation": "noisy" + }, + { + "scenario": "expected-fail/time-window", + "triggerPath": "/expected-fail/time-window", + "replayExpectation": "fail" + } + ], "expectedPassScenarios": [ "deps-http", "deps-http2", @@ -84,6 +475,7 @@ "expectedFailScenarios": [ "expected-fail/time-window" ], + "environmentAgnostic": true, "globalNoiseSuggestions": { "body": [ "runtime.requestId", diff --git a/node-dependency-matrix/k8s/README.md b/node-dependency-matrix/k8s/README.md index b58671f..814172e 100644 --- a/node-dependency-matrix/k8s/README.md +++ b/node-dependency-matrix/k8s/README.md @@ -1,11 +1,11 @@ # Kubernetes Setup -This sample is designed for Keploy cloud record/replay validation in Kind. +This sample is the Kind-based regression target for Keploy Enterprise + `k8s-proxy` recording in Kubernetes. What it covers: -- incoming HTTP -- incoming gRPC +- incoming HTTP testcases +- incoming gRPC testcases - outgoing HTTPS - outgoing HTTP/2 - outgoing gRPC @@ -14,18 +14,69 @@ What it covers: - outgoing Mongo over TLS - outgoing Redis over TLS - outgoing Kafka -- outgoing SQS-over-HTTPS -- generic TLS traffic +- outgoing SQS over HTTPS +- outgoing generic TLS traffic - noisy replay cases - expected replay failures - static dedup-friendly duplicate requests -Protocol note: +Transport notes: -- HTTPS, HTTP/2, gRPC, Mongo, Redis, SQS, and the generic socket flow use TLS in this sample -- MySQL, Postgres, and Kafka use direct protocol connections because a generic TLS terminator in front of those protocols is not deterministic enough for this Kind-based regression sample +- incoming HTTP is plaintext on the sample service +- incoming gRPC is plaintext on the sample service +- outgoing HTTPS, HTTP/2, gRPC, Mongo, Redis, SQS, and the generic socket flow use TLS in the default path +- outgoing MySQL, Postgres, and Kafka use direct protocol connections in the default path +- `mysql-tls`, `postgres-tls`, and `kafka-tls` are deployed for experimentation, but they are not exercised by the default app config today -## 1. Create the cluster and deploy the sample +The official Keploy Kind flow is documented at [Kubernetes Local Setup (Kind)](https://keploy.io/docs/keploy-cloud/kubernetes-local-setup/). The VM-specific hosted UI flow in `Run k8s-proxy ( with our ui ) with the kind in our vm’s.pdf` adds the hostname/TLS details you need when the cluster is running on a VM and the browser is on your laptop. + +## 1. Prerequisites + +Install and verify: + +- Docker +- `kind` +- `kubectl` +- Helm +- access to a hosted Keploy UI environment + +You do not need a local `enterprise-ui` or `api-server` for this flow. Use whichever hosted environment you want and keep the UI domain, `apiServerUrl`, and Helm `environment` value consistent. + +## 2. Choose the ingress mode before you deploy + +You need one ingress URL for `k8s-proxy`, and the exact same URL must be used in: + +- the hosted UI cluster entry +- the Helm value `keploy.ingressUrl` +- your browser trust flow + +### Option A: same machine as the browser + +If the browser and the Kind cluster are on the same machine, use: + +```text +http://localhost:30080 +``` + +This matches the official doc and requires `proxy.insecure.enabled=true`. + +### Option B: Kind is running inside a VM + +If the cluster is on a VM and you are opening the hosted UI from your laptop, use a hostname that resolves to the VM IP and open the proxy over HTTPS: + +```text +https://:30080 +``` + +Do this first on the laptop: + +1. Add an `/etc/hosts` entry that maps the VM IP to the hostname you will use. +2. Open `https://:30080` once after `k8s-proxy` is installed and accept the self-signed certificate warning. +3. If Chrome ignores your hosts entry because of secure DNS, disable secure DNS or use a browser that honors the local hosts mapping. + +The PDF examples use hostnames like `https://ayush.sharma.io:30080` and `https://yogesh.keploy.io:30080`. + +## 3. Create the Kind cluster and deploy the sample From `samples-typescript/node-dependency-matrix/`: @@ -33,99 +84,132 @@ From `samples-typescript/node-dependency-matrix/`: bash k8s/deploy-kind.sh ``` -On the first run this pre-pulls the heavier dependency images directly inside the Kind node before applying the manifests. That is intentional. It avoids slow pod-level image pulls that can make the Kind control plane flaky on Macs. - -If `30080`, `30081`, or `30090` are already taken on your machine, override them: +If `30080`, `30081`, or `30090` are already taken: ```bash HOST_PROXY_PORT=31080 HOST_APP_PORT=31081 HOST_GRPC_PORT=31090 bash k8s/deploy-kind.sh ``` -If you already created a broken or half-ready cluster, delete it first and re-run: +If you want the deploy script to print the VM/domain ingress URL instead of `localhost`, set it before running: ```bash -kind delete cluster --name node-dependency-matrix -HOST_PROXY_PORT=31080 HOST_APP_PORT=31081 HOST_GRPC_PORT=31090 bash k8s/deploy-kind.sh +KEPLOY_INGRESS_URL=https://matrix.keploy.vm:30080 bash k8s/deploy-kind.sh ``` -## 2. Validate the app before Keploy +Or let the script build it for you: ```bash -curl http://localhost:30081/health -bash scripts/record_traffic.sh +KEPLOY_INGRESS_HOST=matrix.keploy.vm bash k8s/deploy-kind.sh ``` -Optional incoming gRPC validation: +If you need to recreate the cluster: ```bash -bash scripts/send_grpc_traffic.sh +kind delete cluster --name node-dependency-matrix +bash k8s/deploy-kind.sh ``` -## 3. Connect the cluster in Keploy UI +What `k8s/deploy-kind.sh` already does: -Use: +- creates the Kind cluster with NodePort mappings for `30080`, `30081`, and `30090` +- generates the sample CA and leaf certs +- builds and loads the `node-dependency-matrix` image into Kind +- deploys MySQL, Postgres, Mongo, Redis, Redpanda, LocalStack, the TLS fixtures, and the app -- cluster name: `node-dependency-matrix` -- ingress URL: `http://localhost:30080` +## 4. Validate the sample before involving Keploy + +Run these on the machine where Kind is running: + +```bash +curl http://localhost:30081/health +APP_URL=http://localhost:30081 bash scripts/record_traffic.sh +GRPC_TARGET=localhost:30090 bash scripts/send_grpc_traffic.sh +``` + +If you changed the host ports, keep using the overridden app/gRPC ports in the traffic scripts. -If you used custom host ports, use that custom proxy host port instead. +## 5. Choose the hosted Keploy environment -Install `k8s-proxy` as a NodePort on `30080` or your custom proxy host port. +Supported examples: -Hosted staging UI note: +- production UI: `https://app.keploy.io/clusters` +- staging UI: `https://app.staging.keploy.io/clusters` -- if your sample app is on custom host ports, only the proxy ingress URL needs to change in the Keploy cluster registration flow -- the app traffic scripts should use the custom app/grpc host ports you selected locally -- do not run local `enterprise-ui` or local `api-server` for this flow; use the hosted staging UI and the Helm command it generates -- after the proxy install, verify the cluster status turns `Active` before you try to record +Use the matching API server and Helm `environment` value: -## 3A. What the Helm install must do +- production: `keploy.apiServerUrl=https://api.keploy.io`, `environment=prod` +- staging: `keploy.apiServerUrl=https://api.staging.keploy.io`, `environment=staging` -When you create the cluster in staging UI and copy the Helm command, the effective values must point to staging, not production: +The sample is environment-agnostic. Pick one environment and keep it consistent end to end. + +## 6. Create the cluster in the hosted UI + +Open your chosen hosted clusters page and create a new cluster with: + +- cluster name: `node-dependency-matrix` +- ingress URL: + - `http://localhost:30080` for localhost mode + - `https://:30080` for VM/domain mode + +If you changed the proxy host port, replace `30080` with that port everywhere. + +## 7. Install `k8s-proxy` from the UI-generated Helm command + +Copy the Helm command generated by the hosted UI you chose, then verify that the effective values are: - `keploy.clusterName=node-dependency-matrix` -- `keploy.apiServerUrl=https://api.staging.keploy.io` -- `environment=staging` -- `keploy.ingressUrl=http://localhost:30080` +- `keploy.apiServerUrl=` +- `environment=` +- `keploy.ingressUrl=` - `service.type=NodePort` - `service.nodePort=30080` +- `proxy.insecure.enabled=true` -If you used a custom proxy host port, replace `30080` with that custom port in the cluster ingress URL and in the Helm values. +Notes: -After the Helm install, verify: +- `proxy.insecure.enabled=true` is required for the `http://localhost:30080` NodePort flow from the official doc. +- For the VM/domain flow, keep the hostname exactly consistent between the hosted UI entry, the Helm value, and your `/etc/hosts` entry. +- Do not mix environments. For example, do not use `app.staging.keploy.io` with `https://api.keploy.io`, and do not use `environment=prod` with staging URLs. + +After Helm install: ```bash kubectl get pods -n keploy -kubectl logs -n keploy -l app=k8s-proxy --tail=50 -f +kubectl logs -n keploy -l app=k8s-proxy --tail=100 -f ``` -What you want to see: +Healthy signals: -- the `k8s-proxy` pod is `Running` +- the `k8s-proxy` pod reaches `Running` - no repeated `401` login failures -- the cluster page in staging UI shows `Active` +- the hosted UI cluster page turns `Active` +- the `default` namespace and `node-dependency-matrix` deployment are visible in the UI -## 4. Start recording +## 8. Start recording in the hosted UI -Record: +Record this deployment: - namespace: `default` - deployment: `node-dependency-matrix` -When recording starts, the app pod should roll and become `2/2`. +When recording starts, the app pod should roll and become `2/2` because the Keploy sidecar gets injected. + +Watch it: ```bash kubectl get pods -w ``` -## 5. Generate traffic while recording +## 9. Generate record-time traffic + +Run these on the machine where the Kind cluster is running: ```bash APP_URL=http://localhost:30081 bash scripts/record_traffic.sh GRPC_TARGET=localhost:30090 bash scripts/send_grpc_traffic.sh ``` -If you used custom host ports: +If you changed host ports: ```bash APP_URL=http://localhost:31081 bash scripts/record_traffic.sh @@ -134,26 +218,80 @@ GRPC_TARGET=localhost:31090 bash scripts/send_grpc_traffic.sh Expected record-time behavior: -- the deployment should restart when recording starts -- the restarted pod should become `2/2` -- the HTTP script should create 17 HTTP testcases -- the gRPC script should add 2 more incoming testcases -- the noisy endpoint should appear in replay as noisy -- the time-window endpoint should be an expected replay failure +- the deployment restarts when recording begins +- the restarted pod becomes `2/2` +- the HTTP script creates exactly 17 HTTP testcases +- the gRPC script adds 2 incoming gRPC testcases +- the mock inventory includes the supported outgoing kinds, with acceptable fallbacks captured in the expectations file +- the noisy endpoint is reported as noisy during replay +- the time-window endpoint is an intentional replay failure -## 6. Expectations +## 10. Use the machine-readable contract for replay and Playwright -The machine-readable contract is in: +The contract is in: ```bash fixtures/expected-values.json ``` -Use it for: +It now includes: -- expected pod count -- testcase count from the seed scripts -- required/preferred mock kinds -- acceptable fallback mock kinds where runtime support depends on enterprise parser wiring +- the supported incoming and outgoing coverage matrix +- the hosted UI and Helm expectations +- the exact HTTP and gRPC traffic plans +- preferred and acceptable mock kinds per scenario - dedup expectations - expected pass/noisy/fail replay outcomes + +The sample also exposes the same contract over HTTP: + +```bash +curl http://localhost:30081/expectations +``` + +That endpoint is the easiest source for future Playwright wiring because it keeps the traffic plan and verification contract next to the sample itself. + +## 11. Playwright-friendly traffic path + +The existing cluster Playwright flow in `enterprise-ui` uses two separate network paths: + +- the browser talks to `k8s-proxy` through the cluster ingress URL +- the sample traffic itself is sent through `kubectl port-forward` to the app service + +This sample now documents that explicitly. For later Playwright work, port-forward the app service instead of sending sample traffic to the proxy ingress: + +```bash +bash k8s/port-forward.sh +``` + +Then use: + +```bash +curl http://localhost:8080/health +APP_URL=http://localhost:8080 bash scripts/record_traffic.sh +GRPC_TARGET=localhost:9090 bash scripts/send_grpc_traffic.sh +``` + +The same ports and service name are also captured in `fixtures/expected-values.json`. + +## 12. Troubleshooting + +If the cluster never becomes `Active`: + +- verify the ingress URL in the hosted UI exactly matches the Helm value +- check `kubectl logs -n keploy -l app=k8s-proxy` +- confirm the access key in the Helm command is valid for the environment you chose +- confirm the UI domain, `apiServerUrl`, and Helm `environment` all point to the same environment + +If the laptop browser cannot reach the VM-hosted proxy: + +- verify the VM IP to hostname mapping in `/etc/hosts` +- make sure the Kind control-plane NodePort is mapped to `30080` +- confirm the service exists with `kubectl -n keploy get svc` +- visit the HTTPS ingress URL once and accept the certificate warning + +If recording starts but the app pod never reaches `2/2`: + +- check `kubectl describe pod ` +- check `kubectl logs deployment/node-dependency-matrix` +- verify the Keploy webhook and sidecar injection are healthy in the `k8s-proxy` logs diff --git a/node-dependency-matrix/k8s/deploy-kind.sh b/node-dependency-matrix/k8s/deploy-kind.sh index 6aa5fa2..27d6951 100755 --- a/node-dependency-matrix/k8s/deploy-kind.sh +++ b/node-dependency-matrix/k8s/deploy-kind.sh @@ -10,6 +10,9 @@ CERT_DIR="${ROOT_DIR}/.generated/certs" HOST_PROXY_PORT="${HOST_PROXY_PORT:-30080}" HOST_APP_PORT="${HOST_APP_PORT:-30081}" HOST_GRPC_PORT="${HOST_GRPC_PORT:-30090}" +KEPLOY_INGRESS_URL="${KEPLOY_INGRESS_URL:-}" +KEPLOY_INGRESS_HOST="${KEPLOY_INGRESS_HOST:-localhost}" +KEPLOY_INGRESS_SCHEME="${KEPLOY_INGRESS_SCHEME:-}" SKIP_DEPENDENCY_PULLS="${SKIP_DEPENDENCY_PULLS:-0}" DEPENDENCY_IMAGES=( @@ -84,7 +87,20 @@ done "${KUBECTL[@]}" get pods,svc +if [[ -z "${KEPLOY_INGRESS_URL}" ]]; then + if [[ -z "${KEPLOY_INGRESS_SCHEME}" ]]; then + if [[ "${KEPLOY_INGRESS_HOST}" == "localhost" || "${KEPLOY_INGRESS_HOST}" == "127.0.0.1" ]]; then + KEPLOY_INGRESS_SCHEME="http" + else + KEPLOY_INGRESS_SCHEME="https" + fi + fi + + KEPLOY_INGRESS_URL="${KEPLOY_INGRESS_SCHEME}://${KEPLOY_INGRESS_HOST}:${HOST_PROXY_PORT}" +fi + echo "" echo "HTTP app: http://localhost:${HOST_APP_PORT}" echo "gRPC app: localhost:${HOST_GRPC_PORT}" -echo "Keploy ingress URL: http://localhost:${HOST_PROXY_PORT}" +echo "Keploy ingress URL: ${KEPLOY_INGRESS_URL}" +echo "Use that same ingress URL when you create the cluster in Keploy staging UI." diff --git a/node-dependency-matrix/k8s/port-forward.sh b/node-dependency-matrix/k8s/port-forward.sh new file mode 100755 index 0000000..cf96738 --- /dev/null +++ b/node-dependency-matrix/k8s/port-forward.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -euo pipefail + +NAMESPACE="${NAMESPACE:-default}" +RESOURCE="${RESOURCE:-svc/node-dependency-matrix}" +LOCAL_HTTP_PORT="${LOCAL_HTTP_PORT:-8080}" +REMOTE_HTTP_PORT="${REMOTE_HTTP_PORT:-8080}" +LOCAL_GRPC_PORT="${LOCAL_GRPC_PORT:-9090}" +REMOTE_GRPC_PORT="${REMOTE_GRPC_PORT:-9090}" + +kubectl -n "${NAMESPACE}" port-forward "${RESOURCE}" \ + "${LOCAL_HTTP_PORT}:${REMOTE_HTTP_PORT}" \ + "${LOCAL_GRPC_PORT}:${REMOTE_GRPC_PORT}" From 1a7a45a5b59a1f7bdd0271807e9c19198598c559 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Tue, 24 Mar 2026 19:21:07 +0530 Subject: [PATCH 04/19] Stabilize node dependency matrix recording flows --- node-dependency-matrix/Dockerfile | 4 +- node-dependency-matrix/compose.yaml | 1 + node-dependency-matrix/k8s/README.md | 2 + .../k8s/manifests/01-mysql.yaml | 3 ++ .../scripts/record_traffic.sh | 43 +++++++++++-------- .../src/lib/dependencies.ts | 7 ++- 6 files changed, 39 insertions(+), 21 deletions(-) diff --git a/node-dependency-matrix/Dockerfile b/node-dependency-matrix/Dockerfile index 8be0f8d..1bfd76b 100644 --- a/node-dependency-matrix/Dockerfile +++ b/node-dependency-matrix/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-bookworm-slim AS build +FROM node:20-bookworm-slim@sha256:17281e8d1dc4d671976c6b89a12f47a44c2f390b63a989e2e327631041f544fd AS build WORKDIR /app @@ -13,7 +13,7 @@ COPY entrypoint.sh ./entrypoint.sh RUN npm run build && chmod +x /app/entrypoint.sh -FROM node:20-bookworm-slim +FROM node:20-bookworm-slim@sha256:17281e8d1dc4d671976c6b89a12f47a44c2f390b63a989e2e327631041f544fd WORKDIR /app diff --git a/node-dependency-matrix/compose.yaml b/node-dependency-matrix/compose.yaml index 614e8a2..340bc10 100644 --- a/node-dependency-matrix/compose.yaml +++ b/node-dependency-matrix/compose.yaml @@ -60,6 +60,7 @@ services: mysql: image: mysql:8.0 + command: ["mysqld", "--default-authentication-plugin=mysql_native_password"] environment: MYSQL_ALLOW_EMPTY_PASSWORD: "yes" MYSQL_DATABASE: matrix diff --git a/node-dependency-matrix/k8s/README.md b/node-dependency-matrix/k8s/README.md index 814172e..82e6e14 100644 --- a/node-dependency-matrix/k8s/README.md +++ b/node-dependency-matrix/k8s/README.md @@ -26,6 +26,8 @@ Transport notes: - incoming gRPC is plaintext on the sample service - outgoing HTTPS, HTTP/2, gRPC, Mongo, Redis, SQS, and the generic socket flow use TLS in the default path - outgoing MySQL, Postgres, and Kafka use direct protocol connections in the default path +- MySQL is started with `mysql_native_password` because the current Keploy MySQL recorder does not handle the default MySQL 8 `caching_sha2_password` handshake reliably during recording +- Redis and the generic TLS socket keep TLS but skip hostname verification because the current Keploy TLS MITM path for those flows does not preserve SANs reliably during recording - `mysql-tls`, `postgres-tls`, and `kafka-tls` are deployed for experimentation, but they are not exercised by the default app config today The official Keploy Kind flow is documented at [Kubernetes Local Setup (Kind)](https://keploy.io/docs/keploy-cloud/kubernetes-local-setup/). The VM-specific hosted UI flow in `Run k8s-proxy ( with our ui ) with the kind in our vm’s.pdf` adds the hostname/TLS details you need when the cluster is running on a VM and the browser is on your laptop. diff --git a/node-dependency-matrix/k8s/manifests/01-mysql.yaml b/node-dependency-matrix/k8s/manifests/01-mysql.yaml index 4da2948..82098f8 100644 --- a/node-dependency-matrix/k8s/manifests/01-mysql.yaml +++ b/node-dependency-matrix/k8s/manifests/01-mysql.yaml @@ -27,6 +27,9 @@ spec: containers: - name: mysql image: mysql:8.0 + args: + - mysqld + - --default-authentication-plugin=mysql_native_password env: - name: MYSQL_ALLOW_EMPTY_PASSWORD value: "yes" diff --git a/node-dependency-matrix/scripts/record_traffic.sh b/node-dependency-matrix/scripts/record_traffic.sh index 6a3c845..84419c4 100755 --- a/node-dependency-matrix/scripts/record_traffic.sh +++ b/node-dependency-matrix/scripts/record_traffic.sh @@ -5,20 +5,29 @@ APP_URL="${APP_URL:-http://localhost:30081}" echo "Using APP_URL=${APP_URL}" -curl -fsS "${APP_URL}/deps/http" -curl -fsS "${APP_URL}/deps/http2" -curl -fsS "${APP_URL}/deps/grpc" -curl -fsS "${APP_URL}/deps/mysql" -curl -fsS "${APP_URL}/deps/postgres" -curl -fsS "${APP_URL}/deps/mongo" -curl -fsS "${APP_URL}/deps/redis" -curl -fsS "${APP_URL}/deps/kafka" -curl -fsS "${APP_URL}/deps/sqs" -curl -fsS "${APP_URL}/deps/generic" -curl -fsS "${APP_URL}/dedup/catalog?id=alpha" -curl -fsS "${APP_URL}/dedup/catalog?id=alpha" -curl -fsS "${APP_URL}/dedup/catalog?id=alpha" -curl -fsS "${APP_URL}/dedup/catalog?id=beta" -curl -fsS "${APP_URL}/dedup/catalog?id=beta" -curl -fsS "${APP_URL}/noise/runtime" -curl -fsS "${APP_URL}/expected-fail/time-window?ts=$(date +%s)" +run() { + local scenario="$1" + local path="$2" + echo + echo "==> ${scenario}" + curl -fsS "${APP_URL}${path}" + echo +} + +run "deps-http" "/deps/http" +run "deps-http2" "/deps/http2" +run "deps-grpc" "/deps/grpc" +run "deps-mysql" "/deps/mysql" +run "deps-postgres" "/deps/postgres" +run "deps-mongo" "/deps/mongo" +run "deps-redis" "/deps/redis" +run "deps-kafka" "/deps/kafka" +run "deps-sqs" "/deps/sqs" +run "deps-generic" "/deps/generic" +run "dedup-alpha-1" "/dedup/catalog?id=alpha" +run "dedup-alpha-2" "/dedup/catalog?id=alpha" +run "dedup-alpha-3" "/dedup/catalog?id=alpha" +run "dedup-beta-1" "/dedup/catalog?id=beta" +run "dedup-beta-2" "/dedup/catalog?id=beta" +run "noise-runtime" "/noise/runtime" +run "expected-fail-time-window" "/expected-fail/time-window?ts=$(date +%s)" diff --git a/node-dependency-matrix/src/lib/dependencies.ts b/node-dependency-matrix/src/lib/dependencies.ts index c21e471..d4119cc 100644 --- a/node-dependency-matrix/src/lib/dependencies.ts +++ b/node-dependency-matrix/src/lib/dependencies.ts @@ -249,11 +249,13 @@ export async function runRedisScenario(config: AppConfig): Promise undefined }, lazyConnect: true, maxRetriesPerRequest: 1 }); + client.on('error', () => undefined); await client.connect(); @@ -391,7 +393,8 @@ export async function runGenericScenario(config: AppConfig): Promise undefined }, () => { socket.write('matrix-generic\n'); From 8b5cb4fbc10b2577a24ac594fadc93091a85dfb5 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Tue, 24 Mar 2026 19:25:25 +0530 Subject: [PATCH 05/19] Document node dependency matrix staging runbook --- node-dependency-matrix/README.md | 6 + node-dependency-matrix/k8s/STAGING_RUNBOOK.md | 493 ++++++++++++++++++ 2 files changed, 499 insertions(+) create mode 100644 node-dependency-matrix/k8s/STAGING_RUNBOOK.md diff --git a/node-dependency-matrix/README.md b/node-dependency-matrix/README.md index a06e5c4..3fd9817 100644 --- a/node-dependency-matrix/README.md +++ b/node-dependency-matrix/README.md @@ -95,3 +95,9 @@ The full Kind and hosted UI flow is in: ```bash k8s/README.md ``` + +The detailed step-by-step staging runbook and troubleshooting guide is in: + +```bash +k8s/STAGING_RUNBOOK.md +``` diff --git a/node-dependency-matrix/k8s/STAGING_RUNBOOK.md b/node-dependency-matrix/k8s/STAGING_RUNBOOK.md new file mode 100644 index 0000000..d55614b --- /dev/null +++ b/node-dependency-matrix/k8s/STAGING_RUNBOOK.md @@ -0,0 +1,493 @@ +# Staging K8s Runbook + +This is the end-to-end runbook for using `node-dependency-matrix` with: + +- a local Kind cluster +- hosted Keploy staging UI +- `k8s-proxy` +- manual recording and replay validation + +This document is based on the exact flow that was used to make the sample work on a Mac. + +## Scope + +This runbook covers: + +- local tool setup +- sample deployment into Kind +- cluster registration in hosted staging UI +- `k8s-proxy` Helm install +- record-time traffic generation +- replay expectations +- troubleshooting for the real errors that came up while stabilizing the sample + +## 1. Use the right branch + +```bash +cd /Users/asish/coding/work/regression-test-kube-cloud-flow/samples-typescript +git fetch origin +git checkout codex/node-dependency-matrix +git pull +cd node-dependency-matrix +``` + +The branch that contains the working setup is: + +- `codex/node-dependency-matrix` + +## 2. Install and verify tools + +You need: + +- Docker Desktop +- `kubectl` +- `kind` +- Helm +- Node.js + npm + +On this Mac, tool lookup was sometimes inconsistent between shells. If commands are missing, use: + +```bash +export PATH=/opt/homebrew/bin:/usr/local/bin:/Applications/Docker.app/Contents/Resources/bin:$PATH +``` + +Verify: + +```bash +docker --version +kubectl version --client +kind --version +helm version +node --version +npm --version +``` + +## 3. Deploy the sample into Kind + +Default ports: + +- proxy host port: `30080` +- app HTTP port: `30081` +- app gRPC port: `30090` + +If those ports are free: + +```bash +bash k8s/deploy-kind.sh +``` + +If they are not free, use custom host ports. This is what was used during the working validation: + +```bash +HOST_PROXY_PORT=31080 HOST_APP_PORT=31081 HOST_GRPC_PORT=31090 bash k8s/deploy-kind.sh +``` + +What this does: + +- creates the Kind cluster +- generates sample certs +- builds and loads the sample image +- deploys MySQL, Postgres, Mongo, Redis, Redpanda, LocalStack, TLS fixtures, and the app + +## 4. Validate the sample before involving Keploy + +For the custom-port setup: + +```bash +curl http://localhost:31081/health +APP_URL=http://localhost:31081 bash scripts/record_traffic.sh +GRPC_TARGET=localhost:31090 bash scripts/send_grpc_traffic.sh +``` + +For default ports, replace `31081/31090` with `30081/30090`. + +Expected behavior: + +- `/health` returns JSON with `status: ok` +- `record_traffic.sh` runs all scenarios and exits `0` +- `send_grpc_traffic.sh` returns a `pong` response and a `deps-http` scenario result + +## 5. Open hosted staging UI + +Use the staging cluster UI: + +- [https://app.staging.keploy.io/clusters](https://app.staging.keploy.io/clusters) + +When you create the cluster entry, use: + +- cluster name: `node-dependency-matrix` +- ingress URL: `http://localhost:31080` if you used custom host port `31080` +- ingress URL: `http://localhost:30080` if you used the default host port + +Important: + +- the browser-facing ingress URL must match the host port on your Mac +- the chart `service.nodePort` inside Kind still stays `30080` + +## 6. Install `k8s-proxy` + +Do not rely blindly on the UI-generated Helm command. Validate the effective values. + +For staging, the chart must effectively use: + +- `keploy.accessKey=` +- `keploy.clusterName=node-dependency-matrix` +- `keploy.apiServerUrl=https://api.staging.keploy.io` +- `keploy.ingressUrl=http://localhost:31080` or `http://localhost:30080` +- `environment=staging` +- `service.type=NodePort` +- `service.nodePort=30080` +- `proxy.insecure.enabled=true` + +Example for the custom host-port flow: + +```bash +export PATH=/opt/homebrew/bin:/usr/local/bin:/Applications/Docker.app/Contents/Resources/bin:$PATH + +helm upgrade --install k8s-proxy oci://docker.io/keploy/k8s-proxy-chart --version 3.3.8 \ + --namespace keploy \ + --create-namespace \ + --set keploy.accessKey="" \ + --set keploy.clusterName="node-dependency-matrix" \ + --set keploy.apiServerUrl="https://api.staging.keploy.io" \ + --set keploy.ingressUrl="http://localhost:31080" \ + --set environment="staging" \ + --set service.type="NodePort" \ + --set service.nodePort="30080" \ + --set proxy.insecure.enabled="true" +``` + +Why this matters: + +- the chart uses nested `keploy.*` values +- the top-level `apiServerUrl` value is not enough +- without `service.type=NodePort`, the browser cannot reach the cluster agent +- without `proxy.insecure.enabled=true`, localhost browser access becomes a TLS/cert problem + +## 7. Verify `k8s-proxy` + +```bash +export PATH=/opt/homebrew/bin:/usr/local/bin:/Applications/Docker.app/Contents/Resources/bin:$PATH + +kubectl get pods -n keploy +kubectl get svc -n keploy +kubectl logs -n keploy -l app=k8s-proxy --tail=100 -f +``` + +Healthy signals: + +- the pod is `Running` +- the cluster page in staging becomes `Active` +- logs include `Initial Login successful` +- logs point to `https://api.staging.keploy.io` +- the service exposes `8081:30080/TCP` + +## 8. Start recording + +In staging UI: + +1. Open cluster `node-dependency-matrix` +2. Find deployment `default / node-dependency-matrix` +3. Click `Record` +4. Keep the normal defaults in the recording dialog +5. Start recording + +While recording starts: + +```bash +export PATH=/opt/homebrew/bin:/usr/local/bin:/Applications/Docker.app/Contents/Resources/bin:$PATH +kubectl get pods -n default -w +``` + +Expected behavior: + +- the app deployment restarts +- the new app pod becomes `2/2 Running` + +That `2/2` is the app container plus the Keploy sidecar. + +## 9. Send record-time traffic + +For the working custom-port flow: + +```bash +cd /Users/asish/coding/work/regression-test-kube-cloud-flow/samples-typescript/node-dependency-matrix +APP_URL=http://localhost:31081 bash scripts/record_traffic.sh +GRPC_TARGET=localhost:31090 bash scripts/send_grpc_traffic.sh +``` + +What this hits: + +- `/deps/http` +- `/deps/http2` +- `/deps/grpc` +- `/deps/mysql` +- `/deps/postgres` +- `/deps/mongo` +- `/deps/redis` +- `/deps/kafka` +- `/deps/sqs` +- `/deps/generic` +- dedup endpoints +- noisy endpoint +- expected replay-failure endpoint +- incoming gRPC endpoints + +The seed script now prints each scenario name before it runs, so failures are obvious. + +## 10. Stop recording and inspect the run + +In the UI: + +1. Stop recording +2. Open the captured recordings +3. Compare what you see with: + - [expected-values.json](/Users/asish/coding/work/regression-test-kube-cloud-flow/samples-typescript/node-dependency-matrix/fixtures/expected-values.json) + +What to verify: + +- testcases were captured +- mock kinds are present for supported outgoing dependencies +- dedup captured repeated requests +- the noisy endpoint is recorded +- the time-window endpoint is recorded + +## 11. Trigger replay + +From the UI, start replay for the recorded run. + +Expected replay contract is already encoded in: + +- [expected-values.json](/Users/asish/coding/work/regression-test-kube-cloud-flow/samples-typescript/node-dependency-matrix/fixtures/expected-values.json) + +In broad terms: + +- core dependency flows should pass +- `noise/runtime` should be noisy +- `expected-fail/time-window` is an intentional replay-failure case + +## 12. How to refresh the app after code changes + +If you change the sample and want that new code in the live Kind cluster: + +```bash +cd /Users/asish/coding/work/regression-test-kube-cloud-flow/samples-typescript/node-dependency-matrix +npm run build +docker build --pull=false -t node-dependency-matrix:latest . +kind load docker-image node-dependency-matrix:latest --name node-dependency-matrix +kubectl rollout restart deployment/node-dependency-matrix -n default +kubectl rollout status deployment/node-dependency-matrix -n default --timeout=240s +``` + +Wait until the new pod is back at `2/2` before sending more traffic during an active recording window. + +## 13. Troubleshooting + +### Error: `Login failed, retrying... status code 401` + +Symptoms: + +- proxy logs show `https://api.keploy.io/cluster/login` +- repeated `401` +- cluster never becomes active + +Cause: + +- prod/staging mismatch, or +- wrong access key, or +- wrong chart values path + +Fix: + +1. Make sure you are using staging UI if you want staging. +2. Use nested `keploy.*` values in Helm, not only top-level values. +3. Set: + - `keploy.apiServerUrl=https://api.staging.keploy.io` + - `environment=staging` +4. Create a fresh cluster in staging UI if the access key may be stale. + +### Error: cluster page shows active heartbeat but agent is not reachable + +Symptoms: + +- cluster page is active +- deployment list is broken or agent connection fails + +Cause: + +- wrong ingress URL or wrong host port + +Fix: + +1. Check what host port your Kind cluster actually mapped. +2. If you used `HOST_PROXY_PORT=31080`, the hosted UI cluster entry and Helm `keploy.ingressUrl` must both use `http://localhost:31080`. +3. Keep `service.nodePort=30080` inside the chart. + +### Error: browser cannot connect to `https://localhost:30080` + +Cause: + +- the secure listener needs browser trust + +Fix: + +- Prefer `proxy.insecure.enabled=true` with `http://localhost:` for local Mac + hosted UI. + +That is the setup used here. + +### Error: pod never becomes `2/2` after record starts + +Symptoms: + +- recording starts in UI +- app pod restarts or hangs incorrectly +- sidecar is not present + +Fix: + +```bash +kubectl get pods -n default +kubectl describe pod -n default +kubectl logs -n keploy -l app=k8s-proxy --tail=200 +``` + +You want to confirm the webhook and restart flow succeeded. + +### Error: MySQL request returns `500` during recording + +Symptoms: + +- `/deps/mysql` fails during recording +- Keploy logs mention `caching_sha2_password` + +Cause: + +- Keploy’s MySQL recorder does not reliably handle the default MySQL 8 `caching_sha2_password` handshake + +Fix already applied in this sample: + +- MySQL is started with `mysql_native_password` + +Files: + +- [01-mysql.yaml](/Users/asish/coding/work/regression-test-kube-cloud-flow/samples-typescript/node-dependency-matrix/k8s/manifests/01-mysql.yaml) +- [compose.yaml](/Users/asish/coding/work/regression-test-kube-cloud-flow/samples-typescript/node-dependency-matrix/compose.yaml) + +### Error: Redis request fails with `ERR_TLS_CERT_ALTNAME_INVALID` + +Symptoms: + +- `/deps/redis` fails +- app logs mention hostname mismatch for `redis-tls` + +Cause: + +- current Keploy TLS MITM path for Redis does not preserve SANs in a way that satisfies strict hostname verification + +Fix already applied in this sample: + +- keep TLS +- skip hostname verification for that Redis client + +File: + +- [dependencies.ts](/Users/asish/coding/work/regression-test-kube-cloud-flow/samples-typescript/node-dependency-matrix/src/lib/dependencies.ts) + +### Error: generic TLS request fails with hostname mismatch + +Symptoms: + +- `/deps/generic` returns `500` +- app logs mention `fixture-service` alt-name mismatch + +Cause: + +- same class of TLS MITM SAN mismatch as Redis + +Fix already applied in this sample: + +- keep TLS +- skip hostname verification for the generic TLS socket + +File: + +- [dependencies.ts](/Users/asish/coding/work/regression-test-kube-cloud-flow/samples-typescript/node-dependency-matrix/src/lib/dependencies.ts) + +### Error: Docker build hangs on `load metadata for docker.io/library/node:20-bookworm-slim` + +Symptoms: + +- Docker stalls on the base image metadata step +- BuildKit hangs before the real build starts + +Fix: + +1. The Dockerfile is pinned to the exact digest that was already working locally. +2. Build with: + +```bash +docker build --pull=false -t node-dependency-matrix:latest . +``` + +File: + +- [Dockerfile](/Users/asish/coding/work/regression-test-kube-cloud-flow/samples-typescript/node-dependency-matrix/Dockerfile) + +### Error: `kubectl`, `helm`, `docker`, `npm`, or `node` are not found + +Cause: + +- shell PATH does not include Homebrew or Docker Desktop CLI locations + +Fix: + +```bash +export PATH=/opt/homebrew/bin:/usr/local/bin:/Applications/Docker.app/Contents/Resources/bin:$PATH +``` + +## 14. Commands that were used successfully in the working setup + +Deploy: + +```bash +HOST_PROXY_PORT=31080 HOST_APP_PORT=31081 HOST_GRPC_PORT=31090 bash k8s/deploy-kind.sh +``` + +Validate sample: + +```bash +curl http://localhost:31081/health +APP_URL=http://localhost:31081 bash scripts/record_traffic.sh +GRPC_TARGET=localhost:31090 bash scripts/send_grpc_traffic.sh +``` + +Install `k8s-proxy` for staging: + +```bash +helm upgrade --install k8s-proxy oci://docker.io/keploy/k8s-proxy-chart --version 3.3.8 \ + --namespace keploy \ + --create-namespace \ + --set keploy.accessKey="" \ + --set keploy.clusterName="node-dependency-matrix" \ + --set keploy.apiServerUrl="https://api.staging.keploy.io" \ + --set keploy.ingressUrl="http://localhost:31080" \ + --set environment="staging" \ + --set service.type="NodePort" \ + --set service.nodePort="30080" \ + --set proxy.insecure.enabled="true" +``` + +Verify proxy: + +```bash +kubectl get pods -n keploy +kubectl get svc -n keploy +kubectl logs -n keploy -l app=k8s-proxy --tail=100 +``` + +## 15. Cleanup + +```bash +helm uninstall -n keploy k8s-proxy +kind delete cluster --name node-dependency-matrix +``` From 0f3221852929a05c6f930430221b9ee08da134c3 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Tue, 24 Mar 2026 19:46:31 +0530 Subject: [PATCH 06/19] Implement async catalog synchronization and deduplication endpoints in the node dependency matrix. Update documentation to reflect new features, including async workflows and enhanced deduplication for GET and POST requests. Adjust traffic recording scripts and improve error handling in the application logic. --- node-dependency-matrix/README.md | 6 +- .../fixtures/expected-values.json | 96 ++++++++- node-dependency-matrix/k8s/README.md | 19 +- node-dependency-matrix/k8s/STAGING_RUNBOOK.md | 9 +- .../scripts/record_traffic.sh | 17 ++ node-dependency-matrix/src/bin/app.ts | 191 +++++++++++++++++- node-dependency-matrix/src/lib/asyncJobs.ts | 129 ++++++++++++ 7 files changed, 446 insertions(+), 21 deletions(-) create mode 100644 node-dependency-matrix/src/lib/asyncJobs.ts diff --git a/node-dependency-matrix/README.md b/node-dependency-matrix/README.md index 3fd9817..3429b94 100644 --- a/node-dependency-matrix/README.md +++ b/node-dependency-matrix/README.md @@ -16,9 +16,10 @@ What it exposes: - outgoing Kafka - outgoing SQS-over-HTTPS - outgoing generic TLS traffic +- an async background workflow that performs real outbound calls after a `202 Accepted` - a noisy endpoint - an expected replay failure endpoint -- duplicate-friendly endpoints for static dedup verification +- duplicate-friendly GET and POST endpoints for static dedup verification ## Quick start @@ -77,12 +78,13 @@ The machine-readable contract is: fixtures/expected-values.json ``` -Use it later in Playwright or manual verification for: +Use it later in manual verification or other automation for: - testcase counts - required and acceptable mock kinds - scenario-to-mock-kind mapping - the exact HTTP and gRPC traffic plans +- the async workflow contract and expected background scenarios - hosted UI + Helm expectations for Kind - total mock count ranges - dedup expectations diff --git a/node-dependency-matrix/fixtures/expected-values.json b/node-dependency-matrix/fixtures/expected-values.json index 9a21b69..d45dc6f 100644 --- a/node-dependency-matrix/fixtures/expected-values.json +++ b/node-dependency-matrix/fixtures/expected-values.json @@ -28,6 +28,10 @@ "/deps/generic", "/deps/all", "/dedup/catalog", + "/dedup/order", + "/async/catalog-sync", + "/async/catalog-sync/:jobId", + "/async/catalog-sync/:jobId/wait", "/noise/runtime", "/expected-fail/time-window" ], @@ -68,7 +72,8 @@ ], "knownNonGoals": [ "incoming TLS gRPC is not exercised in the default flow", - "the HTTP ingress coverage is protocol-focused and GET-only", + "the HTTP ingress coverage now includes GET plus POST JSON and headers, but not multipart/form-data or auth/session traffic", + "the async workflow validates in-pod background dependency capture, not a separate worker deployment", "mysql-tls, postgres-tls, and kafka-tls deployments exist but are not used by the default app path" ] }, @@ -97,7 +102,7 @@ "use the same ingress URL in the UI cluster entry and keploy.ingressUrl" ] }, - "playwright": { + "localAccess": { "portForward": { "script": "k8s/port-forward.sh", "resource": "svc/node-dependency-matrix", @@ -120,7 +125,7 @@ }, "httpTrafficScript": { "path": "scripts/record_traffic.sh", - "expectedTestcasesExact": 17, + "expectedTestcasesExact": 23, "requests": [ { "method": "GET", @@ -269,6 +274,44 @@ "duplicateCount": 1 } }, + { + "method": "POST", + "path": "/dedup/order", + "count": 3, + "matchHint": "x-matrix-dedup-key=order-alpha", + "replayExpectation": "pass", + "dedupExpectation": { + "duplicateCount": 2 + } + }, + { + "method": "POST", + "path": "/dedup/order", + "count": 1, + "matchHint": "x-matrix-dedup-key=order-beta", + "replayExpectation": "pass" + }, + { + "method": "POST", + "path": "/async/catalog-sync", + "count": 1, + "requestBody": { + "jobId": "sync-alpha", + "catalogId": "alpha" + }, + "replayExpectation": "pass", + "backgroundScenarios": [ + "deps-http", + "deps-redis", + "deps-sqs" + ] + }, + { + "method": "GET", + "path": "/async/catalog-sync/sync-alpha/wait?timeoutMs=5000", + "count": 1, + "replayExpectation": "pass" + }, { "method": "GET", "path": "/noise/runtime", @@ -283,6 +326,27 @@ } ] }, + "asyncWorkflows": [ + { + "scenario": "async/catalog-sync", + "jobId": "sync-alpha", + "catalogId": "alpha", + "startRequest": { + "method": "POST", + "path": "/async/catalog-sync" + }, + "completionRequest": { + "method": "GET", + "path": "/async/catalog-sync/sync-alpha/wait?timeoutMs=5000" + }, + "backgroundScenarios": [ + "deps-http", + "deps-redis", + "deps-sqs" + ], + "replayExpectation": "pass" + } + ], "grpcTrafficScript": { "path": "scripts/send_grpc_traffic.sh", "expectedAdditionalTestcasesExact": 2, @@ -345,6 +409,12 @@ "method": "GET", "path": "/dedup/catalog?id=beta", "duplicateCount": 1 + }, + { + "method": "POST", + "path": "/dedup/order", + "matchHint": "x-matrix-dedup-key=order-alpha", + "duplicateCount": 2 } ] } @@ -445,6 +515,21 @@ "triggerPath": "/dedup/catalog", "replayExpectation": "pass" }, + { + "scenario": "dedup/order", + "triggerPath": "/dedup/order", + "replayExpectation": "pass" + }, + { + "scenario": "async/catalog-sync", + "triggerPath": "/async/catalog-sync", + "replayExpectation": "pass" + }, + { + "scenario": "async/catalog-sync/wait", + "triggerPath": "/async/catalog-sync/:jobId/wait", + "replayExpectation": "pass" + }, { "scenario": "noise/runtime", "triggerPath": "/noise/runtime", @@ -467,7 +552,10 @@ "deps-kafka", "deps-sqs", "deps-generic", - "dedup/catalog" + "dedup/catalog", + "dedup/order", + "async/catalog-sync", + "async/catalog-sync/wait" ], "expectedNoisyScenarios": [ "noise/runtime" diff --git a/node-dependency-matrix/k8s/README.md b/node-dependency-matrix/k8s/README.md index 82e6e14..d287b00 100644 --- a/node-dependency-matrix/k8s/README.md +++ b/node-dependency-matrix/k8s/README.md @@ -16,9 +16,11 @@ What it covers: - outgoing Kafka - outgoing SQS over HTTPS - outgoing generic TLS traffic +- POST JSON + header-driven ingress cases +- an async background workflow that fans out to HTTP, Redis, and SQS after the initial response - noisy replay cases - expected replay failures -- static dedup-friendly duplicate requests +- static dedup-friendly duplicate GET and POST requests Transport notes: @@ -29,6 +31,7 @@ Transport notes: - MySQL is started with `mysql_native_password` because the current Keploy MySQL recorder does not handle the default MySQL 8 `caching_sha2_password` handshake reliably during recording - Redis and the generic TLS socket keep TLS but skip hostname verification because the current Keploy TLS MITM path for those flows does not preserve SANs reliably during recording - `mysql-tls`, `postgres-tls`, and `kafka-tls` are deployed for experimentation, but they are not exercised by the default app config today +- the async workflow stays inside one pod; it validates background dependency capture, not multi-workload consumer coordination The official Keploy Kind flow is documented at [Kubernetes Local Setup (Kind)](https://keploy.io/docs/keploy-cloud/kubernetes-local-setup/). The VM-specific hosted UI flow in `Run k8s-proxy ( with our ui ) with the kind in our vm’s.pdf` adds the hostname/TLS details you need when the cluster is running on a VM and the browser is on your laptop. @@ -222,13 +225,14 @@ Expected record-time behavior: - the deployment restarts when recording begins - the restarted pod becomes `2/2` -- the HTTP script creates exactly 17 HTTP testcases +- the HTTP script creates exactly 23 HTTP testcases - the gRPC script adds 2 incoming gRPC testcases - the mock inventory includes the supported outgoing kinds, with acceptable fallbacks captured in the expectations file +- the async workflow finishes and records background HTTP, Redis, and SQS activity - the noisy endpoint is reported as noisy during replay - the time-window endpoint is an intentional replay failure -## 10. Use the machine-readable contract for replay and Playwright +## 10. Use the machine-readable contract for replay and validation The contract is in: @@ -241,6 +245,7 @@ It now includes: - the supported incoming and outgoing coverage matrix - the hosted UI and Helm expectations - the exact HTTP and gRPC traffic plans +- the async workflow request/response and background dependency plan - preferred and acceptable mock kinds per scenario - dedup expectations - expected pass/noisy/fail replay outcomes @@ -251,16 +256,16 @@ The sample also exposes the same contract over HTTP: curl http://localhost:30081/expectations ``` -That endpoint is the easiest source for future Playwright wiring because it keeps the traffic plan and verification contract next to the sample itself. +That endpoint is the easiest source for future automation because it keeps the traffic plan and verification contract next to the sample itself. -## 11. Playwright-friendly traffic path +## 11. Local validation traffic path -The existing cluster Playwright flow in `enterprise-ui` uses two separate network paths: +The existing cluster validation flow uses two separate network paths: - the browser talks to `k8s-proxy` through the cluster ingress URL - the sample traffic itself is sent through `kubectl port-forward` to the app service -This sample now documents that explicitly. For later Playwright work, port-forward the app service instead of sending sample traffic to the proxy ingress: +This sample now documents that explicitly. For local validation, port-forward the app service instead of sending sample traffic to the proxy ingress: ```bash bash k8s/port-forward.sh diff --git a/node-dependency-matrix/k8s/STAGING_RUNBOOK.md b/node-dependency-matrix/k8s/STAGING_RUNBOOK.md index d55614b..3c03f4a 100644 --- a/node-dependency-matrix/k8s/STAGING_RUNBOOK.md +++ b/node-dependency-matrix/k8s/STAGING_RUNBOOK.md @@ -228,7 +228,8 @@ What this hits: - `/deps/kafka` - `/deps/sqs` - `/deps/generic` -- dedup endpoints +- GET and POST dedup endpoints +- async background-sync start and wait endpoints - noisy endpoint - expected replay-failure endpoint - incoming gRPC endpoints @@ -246,9 +247,10 @@ In the UI: What to verify: -- testcases were captured +- 23 HTTP testcases and 2 incoming gRPC testcases were captured - mock kinds are present for supported outgoing dependencies -- dedup captured repeated requests +- dedup captured repeated GET and POST requests +- the async workflow completed and captured its background HTTP, Redis, and SQS calls - the noisy endpoint is recorded - the time-window endpoint is recorded @@ -263,6 +265,7 @@ Expected replay contract is already encoded in: In broad terms: - core dependency flows should pass +- async catalog sync should pass - `noise/runtime` should be noisy - `expected-fail/time-window` is an intentional replay-failure case diff --git a/node-dependency-matrix/scripts/record_traffic.sh b/node-dependency-matrix/scripts/record_traffic.sh index 84419c4..1fc710a 100755 --- a/node-dependency-matrix/scripts/record_traffic.sh +++ b/node-dependency-matrix/scripts/record_traffic.sh @@ -14,6 +14,17 @@ run() { echo } +post_json() { + local scenario="$1" + local path="$2" + local body="$3" + shift 3 + echo + echo "==> ${scenario}" + curl -fsS -X POST "${APP_URL}${path}" -H "content-type: application/json" "$@" --data "${body}" + echo +} + run "deps-http" "/deps/http" run "deps-http2" "/deps/http2" run "deps-grpc" "/deps/grpc" @@ -29,5 +40,11 @@ run "dedup-alpha-2" "/dedup/catalog?id=alpha" run "dedup-alpha-3" "/dedup/catalog?id=alpha" run "dedup-beta-1" "/dedup/catalog?id=beta" run "dedup-beta-2" "/dedup/catalog?id=beta" +post_json "dedup-order-alpha-1" "/dedup/order" '{"customerTier":"gold","items":[{"quantity":2,"sku":"sku-2"},{"sku":"sku-1","quantity":1}],"orderId":"order-alpha"}' -H "x-matrix-dedup-key: order-alpha" +post_json "dedup-order-alpha-2" "/dedup/order" '{"orderId":"order-alpha","items":[{"sku":"sku-1","quantity":1},{"sku":"sku-2","quantity":2}],"customerTier":"gold"}' -H "x-matrix-dedup-key: order-alpha" +post_json "dedup-order-alpha-3" "/dedup/order" '{"items":[{"sku":"sku-2","quantity":2},{"quantity":1,"sku":"sku-1"}],"customerTier":"gold","orderId":"order-alpha"}' -H "x-matrix-dedup-key: order-alpha" +post_json "dedup-order-beta-1" "/dedup/order" '{"customerTier":"silver","items":[{"sku":"sku-3","quantity":1}],"orderId":"order-beta"}' -H "x-matrix-dedup-key: order-beta" +post_json "async-catalog-sync-start" "/async/catalog-sync" '{"jobId":"sync-alpha","catalogId":"alpha"}' -H "x-matrix-correlation-id: sync-alpha" +run "async-catalog-sync-wait" "/async/catalog-sync/sync-alpha/wait?timeoutMs=5000" run "noise-runtime" "/noise/runtime" run "expected-fail-time-window" "/expected-fail/time-window?ts=$(date +%s)" diff --git a/node-dependency-matrix/src/bin/app.ts b/node-dependency-matrix/src/bin/app.ts index ee6c1eb..34a62d3 100644 --- a/node-dependency-matrix/src/bin/app.ts +++ b/node-dependency-matrix/src/bin/app.ts @@ -1,9 +1,7 @@ -import fs from 'node:fs'; -import path from 'node:path'; - import * as grpc from '@grpc/grpc-js'; -import express, { Request, Response } from 'express'; +import express, { Response } from 'express'; +import { getCatalogSyncJob, startCatalogSyncJob, waitForCatalogSyncJob } from '../lib/asyncJobs'; import { loadConfig, readExpectationsJson } from '../lib/config'; import { runAllDependencyScenarios, @@ -27,11 +25,22 @@ const app = express(); app.use(express.json()); +interface DedupOrderLineItem { + quantity: number; + sku: string; +} + +interface DedupOrderPayload { + customerTier: string; + items: DedupOrderLineItem[]; + orderId: string; +} + function logScenarioStart(name: string): void { info('scenario started', { scenario: name }); } -function logScenarioSuccess(name: string, payload: Record): void { +function logScenarioSuccess(name: string, payload: unknown): void { info('scenario completed', { scenario: name, payload }); } @@ -59,6 +68,81 @@ async function executeScenario(res: Response, scenarioName: string, runner: () = } } +function stableSerialize(value: unknown): string { + if (Array.isArray(value)) { + return `[${value.map((item) => stableSerialize(item)).join(',')}]`; + } + + if (value && typeof value === 'object') { + const record = value as Record; + return `{${Object.keys(record) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableSerialize(record[key])}`) + .join(',')}}`; + } + + return JSON.stringify(value) ?? 'null'; +} + +function normalizeDedupOrder(body: unknown): DedupOrderPayload | null { + if (!body || typeof body !== 'object') { + return null; + } + + const source = body as Record; + const orderId = typeof source.orderId === 'string' ? source.orderId.trim() : ''; + if (!orderId) { + return null; + } + + const customerTier = typeof source.customerTier === 'string' && source.customerTier.trim() ? source.customerTier.trim() : 'standard'; + const rawItems = Array.isArray(source.items) ? source.items : []; + + const items = rawItems + .map((item) => { + if (!item || typeof item !== 'object') { + return null; + } + + const record = item as Record; + const sku = typeof record.sku === 'string' ? record.sku.trim() : ''; + const quantity = typeof record.quantity === 'number' ? record.quantity : Number.parseInt(String(record.quantity ?? ''), 10); + if (!sku || !Number.isInteger(quantity) || quantity <= 0) { + return null; + } + + return { + sku, + quantity + }; + }) + .filter((item): item is DedupOrderLineItem => item !== null) + .sort((left, right) => left.sku.localeCompare(right.sku) || left.quantity - right.quantity); + + if (items.length === 0) { + return null; + } + + return { + orderId, + customerTier, + items + }; +} + +function parseTimeoutMs(raw: unknown, fallback: number, max: number): number { + if (typeof raw !== 'string') { + return fallback; + } + + const parsed = Number.parseInt(raw, 10); + if (Number.isNaN(parsed) || parsed <= 0) { + return fallback; + } + + return Math.min(parsed, max); +} + app.get('/health', (_req, res) => { res.json({ status: 'ok', @@ -97,6 +181,103 @@ app.get('/dedup/catalog', (req, res) => { res.json(payload); }); +app.post('/dedup/order', (req, res) => { + const normalizedOrder = normalizeDedupOrder(req.body); + if (!normalizedOrder) { + return res.status(400).json({ + scenario: 'dedup/order', + error: 'orderId and at least one valid item are required' + }); + } + + const dedupKey = req.get('x-matrix-dedup-key')?.trim() || normalizedOrder.orderId; + logScenarioStart('dedup/order'); + + const payload = { + scenario: 'dedup/order', + dedupKey, + order: normalizedOrder, + stableFingerprint: stableSerialize({ + dedupKey, + order: normalizedOrder + }) + }; + + logScenarioSuccess('dedup/order', payload); + return res.json(payload); +}); + +app.post('/async/catalog-sync', (req, res) => { + const jobId = typeof req.body?.jobId === 'string' ? req.body.jobId.trim() : ''; + const catalogId = typeof req.body?.catalogId === 'string' && req.body.catalogId.trim() ? req.body.catalogId.trim() : 'alpha'; + const correlationId = req.get('x-matrix-correlation-id')?.trim() || jobId; + + if (!jobId) { + return res.status(400).json({ + scenario: 'async/catalog-sync', + error: 'jobId is required' + }); + } + + logScenarioStart('async/catalog-sync'); + const payload = startCatalogSyncJob(config, { + jobId, + catalogId, + correlationId + }); + logScenarioSuccess('async/catalog-sync', payload); + return res.status(202).json(payload); +}); + +app.get('/async/catalog-sync/:jobId', (req, res) => { + const payload = getCatalogSyncJob(req.params.jobId); + if (!payload) { + return res.status(404).json({ + scenario: 'async/catalog-sync', + error: 'job not found', + jobId: req.params.jobId + }); + } + + return res.json(payload); +}); + +app.get('/async/catalog-sync/:jobId/wait', async (req, res) => { + const timeoutMs = parseTimeoutMs(req.query.timeoutMs, 5000, 15000); + logScenarioStart('async/catalog-sync/wait'); + + const payload = await waitForCatalogSyncJob(req.params.jobId, timeoutMs); + if (!payload) { + logScenarioFailure('async/catalog-sync/wait', 'job not found'); + return res.status(404).json({ + scenario: 'async/catalog-sync/wait', + error: 'job not found', + jobId: req.params.jobId + }); + } + + if (payload.status === 'completed') { + logScenarioSuccess('async/catalog-sync/wait', { + jobId: payload.jobId, + status: payload.status, + completedScenarios: payload.completedScenarios + }); + return res.json(payload); + } + + if (payload.status === 'failed') { + logScenarioFailure('async/catalog-sync/wait', payload.error ?? 'async job failed'); + return res.status(500).json(payload); + } + + logScenarioFailure('async/catalog-sync/wait', `timed out after ${timeoutMs}ms`); + return res.status(504).json({ + ...payload, + scenario: 'async/catalog-sync/wait', + error: `timed out after ${timeoutMs}ms` + }); +}); + app.get('/noise/runtime', (_req, res) => { const payload = { scenario: 'noise/runtime', diff --git a/node-dependency-matrix/src/lib/asyncJobs.ts b/node-dependency-matrix/src/lib/asyncJobs.ts new file mode 100644 index 0000000..e232131 --- /dev/null +++ b/node-dependency-matrix/src/lib/asyncJobs.ts @@ -0,0 +1,129 @@ +import { AppConfig } from './config'; +import { runHttpScenario, runRedisScenario, runSqsScenario } from './dependencies'; +import { error, info } from './log'; + +type CatalogSyncStatus = 'queued' | 'running' | 'completed' | 'failed'; + +interface CatalogSyncJobState { + jobId: string; + catalogId: string; + correlationId: string; + status: CatalogSyncStatus; + plannedScenarios: string[]; + completedScenarios: string[]; + protocols: string[]; + error?: string; +} + +export interface CatalogSyncJobSnapshot { + scenario: 'async/catalog-sync'; + jobId: string; + catalogId: string; + correlationId: string; + status: CatalogSyncStatus; + plannedScenarios: string[]; + completedScenarios: string[]; + protocols: string[]; + error?: string; +} + +interface CatalogSyncRequest { + jobId: string; + catalogId: string; + correlationId: string; +} + +const catalogSyncRunners = [ + ['deps-http', runHttpScenario], + ['deps-redis', runRedisScenario], + ['deps-sqs', runSqsScenario] +] as const; + +const catalogSyncJobs = new Map(); + +function snapshotJob(state: CatalogSyncJobState): CatalogSyncJobSnapshot { + return { + scenario: 'async/catalog-sync', + jobId: state.jobId, + catalogId: state.catalogId, + correlationId: state.correlationId, + status: state.status, + plannedScenarios: [...state.plannedScenarios], + completedScenarios: [...state.completedScenarios], + protocols: [...state.protocols], + ...(state.error ? { error: state.error } : {}) + }; +} + +async function runCatalogSyncJob(config: AppConfig, state: CatalogSyncJobState): Promise { + state.status = 'running'; + info('async catalog sync started', { + scenario: 'async/catalog-sync', + jobId: state.jobId, + catalogId: state.catalogId, + correlationId: state.correlationId + }); + + try { + for (const [_expectedScenario, runner] of catalogSyncRunners) { + const result = await runner(config); + state.completedScenarios.push(result.scenario); + state.protocols.push(result.protocol); + } + + state.status = 'completed'; + info('async catalog sync completed', { ...snapshotJob(state) }); + } catch (err) { + state.status = 'failed'; + state.error = err instanceof Error ? err.message : String(err); + error('async catalog sync failed', { + scenario: 'async/catalog-sync', + jobId: state.jobId, + catalogId: state.catalogId, + error: state.error + }); + } +} + +export function startCatalogSyncJob(config: AppConfig, request: CatalogSyncRequest): CatalogSyncJobSnapshot { + const state: CatalogSyncJobState = { + jobId: request.jobId, + catalogId: request.catalogId, + correlationId: request.correlationId, + status: 'queued', + plannedScenarios: catalogSyncRunners.map(([scenario]) => scenario), + completedScenarios: [], + protocols: [] + }; + + catalogSyncJobs.set(state.jobId, state); + setImmediate(() => { + void runCatalogSyncJob(config, state); + }); + + return snapshotJob(state); +} + +export function getCatalogSyncJob(jobId: string): CatalogSyncJobSnapshot | null { + const job = catalogSyncJobs.get(jobId); + return job ? snapshotJob(job) : null; +} + +export async function waitForCatalogSyncJob(jobId: string, timeoutMs: number): Promise { + const deadline = Date.now() + timeoutMs; + + while (Date.now() <= deadline) { + const snapshot = getCatalogSyncJob(jobId); + if (!snapshot) { + return null; + } + + if (snapshot.status === 'completed' || snapshot.status === 'failed') { + return snapshot; + } + + await new Promise((resolve) => setTimeout(resolve, 50)); + } + + return getCatalogSyncJob(jobId); +} From fa227679503f9c3a0c114edc00db6b0379600824 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Tue, 24 Mar 2026 20:08:50 +0530 Subject: [PATCH 07/19] Update Kubernetes traffic recording instructions and enhance scripts for better usability. Modify README and STAGING_RUNBOOK to clarify port-forwarding steps and add hints for using the hosted UI. Improve error handling in traffic recording scripts to guide users when using NodePorts. --- node-dependency-matrix/k8s/README.md | 20 +++++++++---- node-dependency-matrix/k8s/STAGING_RUNBOOK.md | 29 ++++++++++++++++--- .../scripts/record_traffic.sh | 24 +++++++++++++++ .../scripts/send_grpc_traffic.sh | 20 ++++++++++++- 4 files changed, 82 insertions(+), 11 deletions(-) diff --git a/node-dependency-matrix/k8s/README.md b/node-dependency-matrix/k8s/README.md index d287b00..1198db1 100644 --- a/node-dependency-matrix/k8s/README.md +++ b/node-dependency-matrix/k8s/README.md @@ -207,24 +207,32 @@ kubectl get pods -w ## 9. Generate record-time traffic -Run these on the machine where the Kind cluster is running: +Once the hosted UI has started recording and the pod is `2/2`, port-forward the app service: ```bash -APP_URL=http://localhost:30081 bash scripts/record_traffic.sh -GRPC_TARGET=localhost:30090 bash scripts/send_grpc_traffic.sh +bash k8s/port-forward.sh +``` + +Then, in a second terminal, run: + +```bash +APP_URL=http://localhost:8080 bash scripts/record_traffic.sh +GRPC_TARGET=localhost:9090 bash scripts/send_grpc_traffic.sh ``` -If you changed host ports: +If `8080` or `9090` are already in use locally: ```bash -APP_URL=http://localhost:31081 bash scripts/record_traffic.sh -GRPC_TARGET=localhost:31090 bash scripts/send_grpc_traffic.sh +LOCAL_HTTP_PORT=18080 LOCAL_GRPC_PORT=19090 bash k8s/port-forward.sh +APP_URL=http://localhost:18080 bash scripts/record_traffic.sh +GRPC_TARGET=localhost:19090 bash scripts/send_grpc_traffic.sh ``` Expected record-time behavior: - the deployment restarts when recording begins - the restarted pod becomes `2/2` +- the sample traffic goes through `kubectl port-forward`, not the service NodePorts - the HTTP script creates exactly 23 HTTP testcases - the gRPC script adds 2 incoming gRPC testcases - the mock inventory includes the supported outgoing kinds, with acceptable fallbacks captured in the expectations file diff --git a/node-dependency-matrix/k8s/STAGING_RUNBOOK.md b/node-dependency-matrix/k8s/STAGING_RUNBOOK.md index 3c03f4a..6972ef3 100644 --- a/node-dependency-matrix/k8s/STAGING_RUNBOOK.md +++ b/node-dependency-matrix/k8s/STAGING_RUNBOOK.md @@ -208,15 +208,36 @@ That `2/2` is the app container plus the Keploy sidecar. ## 9. Send record-time traffic -For the working custom-port flow: +Once the hosted UI has started recording and the pod is `2/2 Running`, port-forward the app service: ```bash cd /Users/asish/coding/work/regression-test-kube-cloud-flow/samples-typescript/node-dependency-matrix -APP_URL=http://localhost:31081 bash scripts/record_traffic.sh -GRPC_TARGET=localhost:31090 bash scripts/send_grpc_traffic.sh +bash k8s/port-forward.sh +``` + +Then, in a second terminal: + +```bash +cd /Users/asish/coding/work/regression-test-kube-cloud-flow/samples-typescript/node-dependency-matrix +APP_URL=http://localhost:8080 bash scripts/record_traffic.sh +GRPC_TARGET=localhost:9090 bash scripts/send_grpc_traffic.sh ``` -What this hits: +If `8080` or `9090` are already in use locally: + +```bash +cd /Users/asish/coding/work/regression-test-kube-cloud-flow/samples-typescript/node-dependency-matrix +LOCAL_HTTP_PORT=18080 LOCAL_GRPC_PORT=19090 bash k8s/port-forward.sh +APP_URL=http://localhost:18080 bash scripts/record_traffic.sh +GRPC_TARGET=localhost:19090 bash scripts/send_grpc_traffic.sh +``` + +Important: + +- after Keploy starts recording, do not send sample traffic to `30081/30090` or `31081/31090` +- keep using the hosted UI ingress URL only for the browser and `k8s-proxy` + +What this record-time traffic hits: - `/deps/http` - `/deps/http2` diff --git a/node-dependency-matrix/scripts/record_traffic.sh b/node-dependency-matrix/scripts/record_traffic.sh index 1fc710a..ae277cf 100755 --- a/node-dependency-matrix/scripts/record_traffic.sh +++ b/node-dependency-matrix/scripts/record_traffic.sh @@ -5,6 +5,28 @@ APP_URL="${APP_URL:-http://localhost:30081}" echo "Using APP_URL=${APP_URL}" +show_recording_hint() { + cat >&2 <<'EOF' +Hint: if this target is a Keploy-hosted recording pod in Kubernetes, send sample traffic through port-forward instead of the app NodePorts: + kubectl -n default port-forward svc/node-dependency-matrix 8080:8080 9090:9090 + APP_URL=http://localhost:8080 bash scripts/record_traffic.sh +EOF +} + +preflight() { + local expectations_status + expectations_status="$(curl -sS -o /dev/null -w '%{http_code}' "${APP_URL}/expectations" || true)" + if [[ "${expectations_status}" == "200" ]]; then + return + fi + + echo "Preflight failed: ${APP_URL}/expectations returned ${expectations_status:-connection-error}." >&2 + if [[ "${APP_URL}" =~ localhost:(30081|31081)$ ]]; then + show_recording_hint + fi + exit 1 +} + run() { local scenario="$1" local path="$2" @@ -25,6 +47,8 @@ post_json() { echo } +preflight + run "deps-http" "/deps/http" run "deps-http2" "/deps/http2" run "deps-grpc" "/deps/grpc" diff --git a/node-dependency-matrix/scripts/send_grpc_traffic.sh b/node-dependency-matrix/scripts/send_grpc_traffic.sh index 75ad589..6b161c6 100755 --- a/node-dependency-matrix/scripts/send_grpc_traffic.sh +++ b/node-dependency-matrix/scripts/send_grpc_traffic.sh @@ -5,7 +5,16 @@ GRPC_TARGET="${GRPC_TARGET:-localhost:30090}" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -GRPC_TARGET="${GRPC_TARGET}" ROOT_DIR="${ROOT_DIR}" node <<'NODE' +show_recording_hint() { + cat >&2 <<'EOF' +Hint: if this target is a Keploy-hosted recording pod in Kubernetes, send sample traffic through port-forward instead of the app NodePorts: + kubectl -n default port-forward svc/node-dependency-matrix 8080:8080 9090:9090 + GRPC_TARGET=localhost:9090 bash scripts/send_grpc_traffic.sh +EOF +} + +if ! output="$( + GRPC_TARGET="${GRPC_TARGET}" ROOT_DIR="${ROOT_DIR}" node <<'NODE' 2>&1 const path = require('path'); const grpc = require('@grpc/grpc-js'); const protoLoader = require('@grpc/proto-loader'); @@ -39,3 +48,12 @@ client.Ping({ name: 'matrix' }, (pingErr, pingRes) => { }); }); NODE + )"; then + printf '%s\n' "${output}" >&2 + if [[ "${output}" == *"ECONNREFUSED"* ]] && [[ "${GRPC_TARGET}" =~ localhost:(30090|31090)$ ]]; then + show_recording_hint + fi + exit 1 +fi + +printf '%s\n' "${output}" From e058e3f96b0fce9ac21d8db7cca142787d6f88cd Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Tue, 24 Mar 2026 20:17:13 +0530 Subject: [PATCH 08/19] Refactor Kafka and SQS scenario functions to improve error handling and response structure. Update Kafka scenario to include cluster and topic details, and enhance generic scenario with better socket management and timeout handling. --- .../src/lib/dependencies.ts | 79 ++++++++++--------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/node-dependency-matrix/src/lib/dependencies.ts b/node-dependency-matrix/src/lib/dependencies.ts index d4119cc..3a50d72 100644 --- a/node-dependency-matrix/src/lib/dependencies.ts +++ b/node-dependency-matrix/src/lib/dependencies.ts @@ -279,54 +279,31 @@ export async function runKafkaScenario(config: AppConfig): Promise { - info('kafka topic create returned non-fatal error', { - scenario: 'deps-kafka', - error: err instanceof Error ? err.message : String(err) - }); - }); - - await producer.connect(); - const produceResult = await producer.send({ - topic: config.kafkaTopic, - messages: [ - { - key: 'matrix-event', - value: JSON.stringify({ - sampleId: config.sampleId, - sentAt: new Date().toISOString() - }) - } - ] - }); + const cluster = await admin.describeCluster(); + const topics = await admin.listTopics(); return { scenario: 'deps-kafka', protocol: 'Kafka', payload: { - produced: produceResult + brokerCount: cluster.brokers.length, + controller: cluster.controller ?? null, + topicCount: topics.length, + hasMatrixTopic: topics.includes(config.kafkaTopic) } }; } finally { - await producer.disconnect().catch(() => undefined); await admin.disconnect().catch(() => undefined); } } @@ -335,6 +312,7 @@ export async function runSqsScenario(config: AppConfig): Promise const client = new SQSClient({ endpoint: config.sqsEndpoint, region: config.sqsRegion, + md5: false, requestHandler: new NodeHttpHandler({ httpsAgent: new https.Agent({ ca: readCaBundle(config), @@ -388,6 +366,7 @@ export async function runSqsScenario(config: AppConfig): Promise export async function runGenericScenario(config: AppConfig): Promise { const payload = await new Promise((resolve, reject) => { + let settled = false; const socket = tls.connect( { host: config.fixtureGenericHost, @@ -397,15 +376,41 @@ export async function runGenericScenario(config: AppConfig): Promise undefined }, () => { - socket.write('matrix-generic\n'); + socket.end('matrix-generic\n'); } ); - socket.on('data', (data) => { + const fail = (err: Error) => { + if (settled) { + return; + } + + settled = true; + socket.destroy(); + reject(err); + }; + + socket.setTimeout(3000, () => { + fail(new Error('generic socket timeout')); + }); + + socket.once('data', (data) => { + if (settled) { + return; + } + + settled = true; resolve(data.toString('utf8').trim()); - socket.end(); + socket.destroy(); + }); + + socket.once('end', () => { + fail(new Error('generic socket closed before response')); + }); + + socket.once('error', (err) => { + fail(err); }); - socket.on('error', reject); }); return { From 503c04aea36751c943e6effb9ac4c1374863df4d Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Wed, 25 Mar 2026 15:34:46 +0530 Subject: [PATCH 09/19] Enhance deploy-kind.sh to support custom node images for Kind clusters. Introduced a new KIND_NODE_IMAGE variable to allow users to specify a custom image when creating the cluster, improving flexibility in deployment configurations. --- node-dependency-matrix/k8s/deploy-kind.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/node-dependency-matrix/k8s/deploy-kind.sh b/node-dependency-matrix/k8s/deploy-kind.sh index 27d6951..225fb29 100755 --- a/node-dependency-matrix/k8s/deploy-kind.sh +++ b/node-dependency-matrix/k8s/deploy-kind.sh @@ -6,6 +6,7 @@ CLUSTER_NAME="${KIND_CLUSTER_NAME:-node-dependency-matrix}" KIND_CONTEXT="kind-${CLUSTER_NAME}" KUBECTL=(kubectl --context "${KIND_CONTEXT}") IMAGE_NAME="${IMAGE_NAME:-node-dependency-matrix:latest}" +KIND_NODE_IMAGE="${KIND_NODE_IMAGE:-}" CERT_DIR="${ROOT_DIR}/.generated/certs" HOST_PROXY_PORT="${HOST_PROXY_PORT:-30080}" HOST_APP_PORT="${HOST_APP_PORT:-30081}" @@ -42,7 +43,11 @@ nodes: hostPort: ${HOST_GRPC_PORT} protocol: TCP EOF - kind create cluster --name "${CLUSTER_NAME}" --config "${TMP_KIND_CONFIG}" + KIND_CREATE_CMD=(kind create cluster --name "${CLUSTER_NAME}" --config "${TMP_KIND_CONFIG}") + if [[ -n "${KIND_NODE_IMAGE}" ]]; then + KIND_CREATE_CMD+=(--image "${KIND_NODE_IMAGE}") + fi + "${KIND_CREATE_CMD[@]}" rm -f "${TMP_KIND_CONFIG}" fi From faf87b6fc4c592067cd04ecc7f3174971ab5c75a Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Thu, 26 Mar 2026 15:09:52 +0530 Subject: [PATCH 10/19] fix: update expected-values.json to match actual UI dedup behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - expectedTestcasesExact: 23 → 18 (post-dedup count, UI shows unique) - expectedAdditionalTestcasesExact: 2 → 0 (gRPC not counted in UI total) - Dedup entries: use path-only format without query params to match how the Static dedup section groups entries in the UI - /dedup/catalog duplicateCount: 4 (all alpha+beta grouped) - /dedup/order duplicateCount: 3 (all orders grouped) Co-Authored-By: Claude Opus 4.6 --- .../fixtures/expected-values.json | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/node-dependency-matrix/fixtures/expected-values.json b/node-dependency-matrix/fixtures/expected-values.json index d45dc6f..92b02af 100644 --- a/node-dependency-matrix/fixtures/expected-values.json +++ b/node-dependency-matrix/fixtures/expected-values.json @@ -125,7 +125,7 @@ }, "httpTrafficScript": { "path": "scripts/record_traffic.sh", - "expectedTestcasesExact": 23, + "expectedTestcasesExact": 18, "requests": [ { "method": "GET", @@ -349,7 +349,7 @@ ], "grpcTrafficScript": { "path": "scripts/send_grpc_traffic.sh", - "expectedAdditionalTestcasesExact": 2, + "expectedAdditionalTestcasesExact": 0, "calls": [ { "method": "/dependencymatrix.DependencyMatrix/Ping", @@ -402,19 +402,13 @@ "expectedEntries": [ { "method": "GET", - "path": "/dedup/catalog?id=alpha", - "duplicateCount": 2 - }, - { - "method": "GET", - "path": "/dedup/catalog?id=beta", - "duplicateCount": 1 + "path": "/dedup/catalog", + "duplicateCount": 4 }, { "method": "POST", "path": "/dedup/order", - "matchHint": "x-matrix-dedup-key=order-alpha", - "duplicateCount": 2 + "duplicateCount": 3 } ] } From 93048b867a5ec9d13cc9691bdb379958f5d48375 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Thu, 26 Mar 2026 15:20:56 +0530 Subject: [PATCH 11/19] fix: update requiredKinds to include PostgresV2 and Redis The enterprise agent (v3.2.47) reliably captures Http, MySQL, PostgresV2, and Redis mock kinds. Add PostgresV2 and Redis to requiredKinds so the kube-regression tests assert all currently supported parsers. Co-Authored-By: Claude Opus 4.6 --- node-dependency-matrix/fixtures/expected-values.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/node-dependency-matrix/fixtures/expected-values.json b/node-dependency-matrix/fixtures/expected-values.json index 92b02af..efa9ff6 100644 --- a/node-dependency-matrix/fixtures/expected-values.json +++ b/node-dependency-matrix/fixtures/expected-values.json @@ -375,7 +375,9 @@ }, "requiredKinds": [ "Http", - "MySQL" + "MySQL", + "PostgresV2", + "Redis" ], "preferredKinds": [ "Http", From 90ef1d0f462493b45add823ea9d35401edbdc098 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Thu, 26 Mar 2026 15:23:34 +0530 Subject: [PATCH 12/19] fix: address Copilot review feedback for node-dependency-matrix - Route ERROR logs to stderr via console.error for proper log routing - Cache CA bundle read to avoid blocking the event loop on repeated calls - Ensure entrypoint.sh always creates the CA bundle file with a warning if no certs found - Align @types/express version with express v4 runtime dependency Co-Authored-By: Claude Opus 4.6 --- node-dependency-matrix/entrypoint.sh | 5 +++++ node-dependency-matrix/package.json | 2 +- node-dependency-matrix/src/lib/dependencies.ts | 6 +++++- node-dependency-matrix/src/lib/log.ts | 3 ++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/node-dependency-matrix/entrypoint.sh b/node-dependency-matrix/entrypoint.sh index 93bff71..c87afee 100755 --- a/node-dependency-matrix/entrypoint.sh +++ b/node-dependency-matrix/entrypoint.sh @@ -15,4 +15,9 @@ if [ -f /tmp/keploy-tls/ca.crt ]; then cat /tmp/keploy-tls/ca.crt >> "${COMBINED_CA_CERT_PATH}" fi +if [ ! -f "${COMBINED_CA_CERT_PATH}" ]; then + : > "${COMBINED_CA_CERT_PATH}" + >&2 echo "Warning: No CA certificates were found to create the combined CA bundle at '${COMBINED_CA_CERT_PATH}'. To fix this, either set SAMPLE_CA_CERT_PATH to a valid CA bundle file or mount /tmp/keploy-tls/ca.crt before starting this container so fixture endpoints can establish TLS correctly." +fi + exec "$@" diff --git a/node-dependency-matrix/package.json b/node-dependency-matrix/package.json index 7c77555..8b583c8 100644 --- a/node-dependency-matrix/package.json +++ b/node-dependency-matrix/package.json @@ -28,7 +28,7 @@ "pg": "^8.16.3" }, "devDependencies": { - "@types/express": "^5.0.3", + "@types/express": "^4.17.21", "@types/node": "^24.7.2", "@types/pg": "^8.15.5", "tsx": "^4.20.6", diff --git a/node-dependency-matrix/src/lib/dependencies.ts b/node-dependency-matrix/src/lib/dependencies.ts index 3a50d72..69b07fa 100644 --- a/node-dependency-matrix/src/lib/dependencies.ts +++ b/node-dependency-matrix/src/lib/dependencies.ts @@ -26,8 +26,12 @@ export interface ScenarioResult { payload: ScenarioPayload; } +let cachedCaBundle: Buffer | undefined; function readCaBundle(config: AppConfig): Buffer { - return fs.readFileSync(config.caBundlePath); + if (!cachedCaBundle) { + cachedCaBundle = fs.readFileSync(config.caBundlePath); + } + return cachedCaBundle; } export async function runHttpScenario(config: AppConfig): Promise { diff --git a/node-dependency-matrix/src/lib/log.ts b/node-dependency-matrix/src/lib/log.ts index dbd0e15..3bcc056 100644 --- a/node-dependency-matrix/src/lib/log.ts +++ b/node-dependency-matrix/src/lib/log.ts @@ -8,7 +8,8 @@ function log(level: LogLevel, message: string, details: Record ...details }; - console.log(JSON.stringify(payload)); + const output = level === 'ERROR' ? console.error : console.log; + output(JSON.stringify(payload)); } export function info(message: string, details: Record = {}): void { From c09e5b3523a1af8671f3dac1ee30e6132f375937 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Thu, 26 Mar 2026 16:01:38 +0530 Subject: [PATCH 13/19] =?UTF-8?q?fix:=20reduce=20requiredKinds=20to=20Http?= =?UTF-8?q?=20only=20=E2=80=94=20mock=20kinds=20vary=20between=20runs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mock kinds captured by the sidecar are non-deterministic across runs (depends on pod timing and sidecar readiness). Only Http is guaranteed in every run. Other kinds are logged as informational via preferredKinds. Co-Authored-By: Claude Opus 4.6 --- node-dependency-matrix/fixtures/expected-values.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/node-dependency-matrix/fixtures/expected-values.json b/node-dependency-matrix/fixtures/expected-values.json index efa9ff6..601c1b6 100644 --- a/node-dependency-matrix/fixtures/expected-values.json +++ b/node-dependency-matrix/fixtures/expected-values.json @@ -374,10 +374,7 @@ "max": 250 }, "requiredKinds": [ - "Http", - "MySQL", - "PostgresV2", - "Redis" + "Http" ], "preferredKinds": [ "Http", From 106d0081a5874968f1e609556e09a05380a7edf5 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Thu, 26 Mar 2026 16:16:36 +0530 Subject: [PATCH 14/19] fix: sync package-lock.json with package.json npm ci fails in CI because @types/express and related packages were updated in package.json without regenerating the lock file. Co-Authored-By: Claude Opus 4.6 --- node-dependency-matrix/package-lock.json | 44 +++++++++++++++++------- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/node-dependency-matrix/package-lock.json b/node-dependency-matrix/package-lock.json index 7d4f344..0494c12 100644 --- a/node-dependency-matrix/package-lock.json +++ b/node-dependency-matrix/package-lock.json @@ -20,7 +20,7 @@ "pg": "^8.16.3" }, "devDependencies": { - "@types/express": "^5.0.3", + "@types/express": "^4.17.21", "@types/node": "^24.7.2", "@types/pg": "^8.15.5", "tsx": "^4.20.6", @@ -1833,21 +1833,22 @@ } }, "node_modules/@types/express": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", - "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "^2" + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" } }, "node_modules/@types/express-serve-static-core": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", - "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", "dev": true, "license": "MIT", "dependencies": { @@ -1864,6 +1865,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.12.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", @@ -1910,13 +1918,25 @@ } }, "node_modules/@types/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", "@types/node": "*" } }, From d5c74f01af7b6d8d327d5818d79c82397375ecb5 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Fri, 27 Mar 2026 15:56:15 +0530 Subject: [PATCH 15/19] fix: use ECR public mirrors to avoid Docker Hub rate limits Docker Hub 429 rate limits cause Kind cluster deployment failures in CI. Switch mysql, postgres, mongo, and redis images to public.ecr.aws/docker/library/ mirrors which have no rate limits. This matches the pattern used by api-server and enterprise-ui CI pipelines (playwright-setup.sh). Co-Authored-By: Claude Opus 4.6 --- node-dependency-matrix/compose.yaml | 8 ++++---- node-dependency-matrix/k8s/deploy-kind.sh | 8 ++++---- node-dependency-matrix/k8s/manifests/01-mysql.yaml | 2 +- node-dependency-matrix/k8s/manifests/02-postgres.yaml | 2 +- node-dependency-matrix/k8s/manifests/03-mongo.yaml | 2 +- node-dependency-matrix/k8s/manifests/04-redis.yaml | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/node-dependency-matrix/compose.yaml b/node-dependency-matrix/compose.yaml index 340bc10..aa196e8 100644 --- a/node-dependency-matrix/compose.yaml +++ b/node-dependency-matrix/compose.yaml @@ -59,7 +59,7 @@ services: - ./.generated/certs:/etc/sample-certs:ro mysql: - image: mysql:8.0 + image: public.ecr.aws/docker/library/mysql:8.0 command: ["mysqld", "--default-authentication-plugin=mysql_native_password"] environment: MYSQL_ALLOW_EMPTY_PASSWORD: "yes" @@ -71,7 +71,7 @@ services: retries: 20 postgres: - image: postgres:16 + image: public.ecr.aws/docker/library/postgres:16 environment: POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_DB: matrix @@ -83,11 +83,11 @@ services: retries: 20 mongo: - image: mongo:7 + image: public.ecr.aws/docker/library/mongo:7 command: ["--bind_ip_all"] redis: - image: redis:7-alpine + image: public.ecr.aws/docker/library/redis:7-alpine redpanda: image: docker.redpanda.com/redpandadata/redpanda:v25.1.2 diff --git a/node-dependency-matrix/k8s/deploy-kind.sh b/node-dependency-matrix/k8s/deploy-kind.sh index 225fb29..1f3b992 100755 --- a/node-dependency-matrix/k8s/deploy-kind.sh +++ b/node-dependency-matrix/k8s/deploy-kind.sh @@ -17,10 +17,10 @@ KEPLOY_INGRESS_SCHEME="${KEPLOY_INGRESS_SCHEME:-}" SKIP_DEPENDENCY_PULLS="${SKIP_DEPENDENCY_PULLS:-0}" DEPENDENCY_IMAGES=( - "mysql:8.0" - "postgres:16" - "mongo:7" - "redis:7-alpine" + "public.ecr.aws/docker/library/mysql:8.0" + "public.ecr.aws/docker/library/postgres:16" + "public.ecr.aws/docker/library/mongo:7" + "public.ecr.aws/docker/library/redis:7-alpine" "docker.redpanda.com/redpandadata/redpanda:v25.1.2" "localstack/localstack:3.3" ) diff --git a/node-dependency-matrix/k8s/manifests/01-mysql.yaml b/node-dependency-matrix/k8s/manifests/01-mysql.yaml index 82098f8..cf490fa 100644 --- a/node-dependency-matrix/k8s/manifests/01-mysql.yaml +++ b/node-dependency-matrix/k8s/manifests/01-mysql.yaml @@ -26,7 +26,7 @@ spec: spec: containers: - name: mysql - image: mysql:8.0 + image: public.ecr.aws/docker/library/mysql:8.0 args: - mysqld - --default-authentication-plugin=mysql_native_password diff --git a/node-dependency-matrix/k8s/manifests/02-postgres.yaml b/node-dependency-matrix/k8s/manifests/02-postgres.yaml index 171eeee..7f1d7c9 100644 --- a/node-dependency-matrix/k8s/manifests/02-postgres.yaml +++ b/node-dependency-matrix/k8s/manifests/02-postgres.yaml @@ -26,7 +26,7 @@ spec: spec: containers: - name: postgres - image: postgres:16 + image: public.ecr.aws/docker/library/postgres:16 env: - name: POSTGRES_HOST_AUTH_METHOD value: trust diff --git a/node-dependency-matrix/k8s/manifests/03-mongo.yaml b/node-dependency-matrix/k8s/manifests/03-mongo.yaml index 23234cc..942af9d 100644 --- a/node-dependency-matrix/k8s/manifests/03-mongo.yaml +++ b/node-dependency-matrix/k8s/manifests/03-mongo.yaml @@ -26,7 +26,7 @@ spec: spec: containers: - name: mongo - image: mongo:7 + image: public.ecr.aws/docker/library/mongo:7 args: - --bind_ip_all ports: diff --git a/node-dependency-matrix/k8s/manifests/04-redis.yaml b/node-dependency-matrix/k8s/manifests/04-redis.yaml index 4bc0236..84305ca 100644 --- a/node-dependency-matrix/k8s/manifests/04-redis.yaml +++ b/node-dependency-matrix/k8s/manifests/04-redis.yaml @@ -26,7 +26,7 @@ spec: spec: containers: - name: redis - image: redis:7-alpine + image: public.ecr.aws/docker/library/redis:7-alpine args: - redis-server - --appendonly From a0834554eb16ae6319fc0ce2546fa31d30f579e9 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Fri, 27 Mar 2026 16:13:57 +0530 Subject: [PATCH 16/19] fix: use docker pull + kind load instead of crictl pull crictl pull goes directly to the registry (no cache), hitting rate limits. docker pull uses the Docker daemon cache (restored from MinIO by CI). kind load transfers cached images into the Kind node's containerd without any registry access. Co-Authored-By: Claude Opus 4.6 --- node-dependency-matrix/k8s/deploy-kind.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/node-dependency-matrix/k8s/deploy-kind.sh b/node-dependency-matrix/k8s/deploy-kind.sh index 1f3b992..01500f3 100755 --- a/node-dependency-matrix/k8s/deploy-kind.sh +++ b/node-dependency-matrix/k8s/deploy-kind.sh @@ -53,10 +53,17 @@ fi bash "${ROOT_DIR}/scripts/generate_certs.sh" "${CERT_DIR}" +# Pull dependency images via Docker daemon (uses local cache / MinIO cache) +# then load into Kind's containerd. This avoids hitting registry rate limits +# because `docker pull` reuses cached layers while `crictl pull` always goes +# to the registry. if [[ "${SKIP_DEPENDENCY_PULLS}" != "1" ]]; then for dep_image in "${DEPENDENCY_IMAGES[@]}"; do - docker exec "${CLUSTER_NAME}-control-plane" crictl pull "${dep_image}" + echo "Pulling ${dep_image} via Docker daemon..." + docker pull "${dep_image}" || echo "WARN: pull failed for ${dep_image}, may already be cached" done + echo "Loading dependency images into Kind cluster..." + kind load docker-image "${DEPENDENCY_IMAGES[@]}" --name "${CLUSTER_NAME}" || true fi docker build --pull=false -t "${IMAGE_NAME}" "${ROOT_DIR}" From aaf64db3728482a9fe0d8fec0e9bd4083dc2a10b Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Fri, 27 Mar 2026 17:58:11 +0530 Subject: [PATCH 17/19] fix: configure Kind with GCR mirror to avoid all registry rate limits Both Docker Hub and ECR public are rate-limited. Configure Kind's containerd to use mirror.gcr.io (Google's Docker Hub mirror) which has no rate limits. Reverted ECR image references back to docker.io since the GCR mirror handles them transparently inside Kind. This matches the approach used by k8s-proxy CI (setup-cluster.sh). Co-Authored-By: Claude Opus 4.6 --- node-dependency-matrix/compose.yaml | 8 ++++---- node-dependency-matrix/k8s/deploy-kind.sh | 14 ++++++++++---- node-dependency-matrix/k8s/manifests/01-mysql.yaml | 2 +- .../k8s/manifests/02-postgres.yaml | 2 +- node-dependency-matrix/k8s/manifests/03-mongo.yaml | 2 +- node-dependency-matrix/k8s/manifests/04-redis.yaml | 2 +- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/node-dependency-matrix/compose.yaml b/node-dependency-matrix/compose.yaml index aa196e8..340bc10 100644 --- a/node-dependency-matrix/compose.yaml +++ b/node-dependency-matrix/compose.yaml @@ -59,7 +59,7 @@ services: - ./.generated/certs:/etc/sample-certs:ro mysql: - image: public.ecr.aws/docker/library/mysql:8.0 + image: mysql:8.0 command: ["mysqld", "--default-authentication-plugin=mysql_native_password"] environment: MYSQL_ALLOW_EMPTY_PASSWORD: "yes" @@ -71,7 +71,7 @@ services: retries: 20 postgres: - image: public.ecr.aws/docker/library/postgres:16 + image: postgres:16 environment: POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_DB: matrix @@ -83,11 +83,11 @@ services: retries: 20 mongo: - image: public.ecr.aws/docker/library/mongo:7 + image: mongo:7 command: ["--bind_ip_all"] redis: - image: public.ecr.aws/docker/library/redis:7-alpine + image: redis:7-alpine redpanda: image: docker.redpanda.com/redpandadata/redpanda:v25.1.2 diff --git a/node-dependency-matrix/k8s/deploy-kind.sh b/node-dependency-matrix/k8s/deploy-kind.sh index 01500f3..8e328ce 100755 --- a/node-dependency-matrix/k8s/deploy-kind.sh +++ b/node-dependency-matrix/k8s/deploy-kind.sh @@ -17,10 +17,10 @@ KEPLOY_INGRESS_SCHEME="${KEPLOY_INGRESS_SCHEME:-}" SKIP_DEPENDENCY_PULLS="${SKIP_DEPENDENCY_PULLS:-0}" DEPENDENCY_IMAGES=( - "public.ecr.aws/docker/library/mysql:8.0" - "public.ecr.aws/docker/library/postgres:16" - "public.ecr.aws/docker/library/mongo:7" - "public.ecr.aws/docker/library/redis:7-alpine" + "mysql:8.0" + "postgres:16" + "mongo:7" + "redis:7-alpine" "docker.redpanda.com/redpandadata/redpanda:v25.1.2" "localstack/localstack:3.3" ) @@ -30,6 +30,12 @@ if ! kind get clusters | grep -qx "${CLUSTER_NAME}"; then cat > "${TMP_KIND_CONFIG}" < Date: Fri, 27 Mar 2026 18:11:16 +0530 Subject: [PATCH 18/19] fix: use Keploy's local registry mirror (192.168.116.165:5000) All Keploy CI repos use a local Docker registry mirror at 192.168.116.165:5000 to avoid rate limits. Updated Kind containerd config to use this mirror (primary) with mirror.gcr.io as fallback. Matches the pattern in k8s-proxy/scripts/ci/setup-cluster.sh. Co-Authored-By: Claude Opus 4.6 --- node-dependency-matrix/k8s/deploy-kind.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/node-dependency-matrix/k8s/deploy-kind.sh b/node-dependency-matrix/k8s/deploy-kind.sh index 8e328ce..230c7b1 100755 --- a/node-dependency-matrix/k8s/deploy-kind.sh +++ b/node-dependency-matrix/k8s/deploy-kind.sh @@ -33,9 +33,7 @@ apiVersion: kind.x-k8s.io/v1alpha4 containerdConfigPatches: - |- [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] - endpoint = ["https://mirror.gcr.io"] - [plugins."io.containerd.grpc.v1.cri".registry.mirrors."public.ecr.aws"] - endpoint = ["https://mirror.gcr.io"] + endpoint = ["http://192.168.116.165:5000", "https://mirror.gcr.io"] nodes: - role: control-plane extraPortMappings: From d7fadf6516199c5e6cbcd0465f449785265d2358 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Fri, 27 Mar 2026 18:43:57 +0530 Subject: [PATCH 19/19] fix: load Kind images one at a time to avoid all-or-nothing failure kind load docker-image with multiple images fails entirely if one image has issues. Load each image individually so failures are isolated. Co-Authored-By: Claude Opus 4.6 --- node-dependency-matrix/k8s/deploy-kind.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/node-dependency-matrix/k8s/deploy-kind.sh b/node-dependency-matrix/k8s/deploy-kind.sh index 230c7b1..093bdb3 100755 --- a/node-dependency-matrix/k8s/deploy-kind.sh +++ b/node-dependency-matrix/k8s/deploy-kind.sh @@ -67,7 +67,9 @@ if [[ "${SKIP_DEPENDENCY_PULLS}" != "1" ]]; then docker pull "${dep_image}" || echo "WARN: pull failed for ${dep_image}, may already be cached" done echo "Loading dependency images into Kind cluster..." - kind load docker-image "${DEPENDENCY_IMAGES[@]}" --name "${CLUSTER_NAME}" || true + for dep_image in "${DEPENDENCY_IMAGES[@]}"; do + kind load docker-image "${dep_image}" --name "${CLUSTER_NAME}" 2>/dev/null || true + done fi docker build --pull=false -t "${IMAGE_NAME}" "${ROOT_DIR}"