diff --git a/.circleci/config.yml b/.circleci/config.yml index 712804a..cf20c95 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,107 +1,143 @@ version: 2.1 - -jobs: - quality: +executors: + reviewed: docker: - - image: node:26.8.1-bookworm - - image: postgres:16-alpine - environment: - POSTGRES_PASSWORD: forms_local - POSTGRES_DB: forms_processor_test - environment: - TEST_DATABASE_URL: postgresql://postgres:forms_local@localhost:5432/forms_processor_test + - image: cimg/aws@sha256:4cf3768db00ddf9bc439b8c84e8a60c11a14edf81c8baf76f74132144f8340d5 +commands: + checkout-source: steps: - - checkout - - run: npm ci - - run: - name: Lint, type-check, test routing and retries, and compile - command: | - npm run lint - npm run typecheck - npm test - npm run test:integration - npm run build - deployment-checks: - docker: - - image: cimg/python:3.13.2 + - run: + name: Check out the exact public source commit over HTTPS + command: | + set -euo pipefail + [[ "$CIRCLE_SHA1" =~ ^[0-9a-f]{40}$ ]] + export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null GIT_TERMINAL_PROMPT=0 + git init --quiet . + git remote add origin https://github.com/topcoder-platform/forms-processor-v6.git + git -c credential.helper= fetch --quiet --depth 1 origin "$CIRCLE_SHA1" + git checkout --quiet --detach FETCH_HEAD + test "$(git rev-parse HEAD)" = "$CIRCLE_SHA1" + checkout-suite: + steps: + - run: + name: Verify deployment suite v2.0.5 + command: | + set -euo pipefail + git init --quiet /tmp/tc-deploy-scripts + git -C /tmp/tc-deploy-scripts remote add origin https://github.com/topcoder-platform/tc-deploy-scripts.git + git -C /tmp/tc-deploy-scripts fetch --quiet --depth 1 origin refs/tags/v2.0.5 + git -C /tmp/tc-deploy-scripts checkout --quiet --detach FETCH_HEAD + test "$(git -C /tmp/tc-deploy-scripts rev-parse HEAD)" = bff574ab5c0b46773e25d2af1eff76675fba2ba5 +jobs: + build: + executor: reviewed steps: - - checkout - - run: pip install boto3 - - run: python3 -m unittest discover -s deploy/tests + - checkout-source + - setup_remote_docker + - checkout-suite + - run: + name: Build committed source without AWS or broker credentials + command: | + set -euo pipefail + python3 -I /tmp/tc-deploy-scripts/build_image.py --dockerfile Dockerfile --image forms-processor-v6:candidate + mkdir -p /tmp/release + docker save --output /tmp/release/image.tar forms-processor-v6:candidate + no_output_timeout: 30m + - persist_to_workspace: + root: /tmp/release + paths: + - image.tar deploy: - docker: - - image: cimg/python:3.13.2-browsers + executor: reviewed parameters: - deploy_env: - type: enum - enum: [DEV, PROD] environment: type: enum - enum: [dev, production] + enum: + - dev + - prod environment: - AWS_REGION: us-east-1 - AWS_DEFAULT_REGION: us-east-1 - AWS_STS_REGIONAL_ENDPOINTS: regional - AWS_RETRY_MODE: standard - AWS_MAX_ATTEMPTS: "10" - APPNAME: forms-processor-v6 - DEPLOY_ENV: << parameters.deploy_env >> - DEPLOYMENT_ENVIRONMENT: << parameters.environment >> + DEPLOY_ENV: << parameters.environment >> + steps: + - checkout-source + - setup_remote_docker + - checkout-suite + - attach_workspace: + at: /tmp/release + - run: + name: Promote a scanned digest with repository-bound OIDC + command: | + set +x + set -euo pipefail + test -f "deploy/${DEPLOY_ENV}.json" + docker load --input /tmp/release/image.tar + python3 -I deploy/release.py "deploy/${DEPLOY_ENV}.json" + no_output_timeout: 45m + quality: + docker: + - image: node@sha256:9f94d34c787165dca03b74e5bf9c3bf90e8de79b19aa3d87fe1fa1694bf75c89 + - image: postgres@sha256:0cefaabd882e42bcb2a2f3dc56d3ce4770560ba62fd7f197f1a2e008fd5731ab + environment: + POSTGRES_PASSWORD: forms_local + POSTGRES_DB: forms_processor_test + environment: + TEST_DATABASE_URL: postgresql://postgres:forms_local@localhost:5432/forms_processor_test + steps: + - checkout-source + - run: npm ci --install-links=true + - run: + name: Lint, type-check, test routing and retries, and compile + command: | + npm run lint + npm run typecheck + npm test + npm run test:integration + npm run build + deployment-checks: + executor: reviewed steps: - - checkout - - setup_remote_docker - - run: - name: Install deployment dependencies - command: | - sudo apt-get update -qq - sudo apt-get install -y -qq jq python3-pip - pip install boto3 awscli - - run: - name: Install the pinned Topcoder AWS credential helper - command: | - git -c advice.detachedHead=false clone --quiet --depth 1 --branch v1.4.20 https://github.com/topcoder-platform/tc-deploy-scripts ../buildscript - test "$(git -C ../buildscript rev-parse HEAD)" = 5f3745c35463ce0e2475c12269aa7a2b446456e5 - cp ../buildscript/awsconfiguration.sh . - - run: - name: Build tested runtime image - command: docker buildx build --load --tag "${APPNAME}:candidate" . - - run: - name: Deploy a new task revision to the existing ECS service - no_output_timeout: 35m - command: | - set -euo pipefail - set +x - trap 'rm -f awsenvconf awsconfiguration.sh' EXIT - ./awsconfiguration.sh "$DEPLOY_ENV" "$AWS_REGION" - source awsenvconf - python3 -u deploy/release.py "$DEPLOYMENT_ENVIRONMENT" "${DEPLOYMENT_ENVIRONMENT}-${CIRCLE_SHA1}-${CIRCLE_BUILD_NUM}" - - store_artifacts: - path: deploy/release-<< parameters.environment >>.json - destination: release + - checkout-suite + - run: + name: Test the pinned deployment security and rollout checks + command: python3 -m unittest discover -s /tmp/tc-deploy-scripts/tests -q workflows: - build-and-deploy: + release: jobs: - - quality - - deployment-checks - - deploy: - name: deploy-dev - context: org-global - requires: [quality, deployment-checks] - deploy_env: DEV - environment: dev - serial-group: << pipeline.project.slug >>/deploy-dev - filters: - branches: - only: develop - tags: - only: /^dev-.*/ - - deploy: - name: deploy-production - context: org-global - requires: [quality, deployment-checks] - deploy_env: PROD - environment: production - serial-group: << pipeline.project.slug >>/deploy-production - filters: - branches: - only: master + - quality + - deployment-checks + - build: + requires: + - quality + - deployment-checks + - deploy: + name: deploy-dev + environment: dev + requires: + - build + context: forms-processor-v6-dev-deploy + serial-group: << pipeline.project.slug >>/deploy-dev + filters: + branches: + only: develop + tags: + ignore: /.*/ + - approve-prod: + type: approval + requires: + - build + filters: + branches: + only: master + tags: + ignore: /.*/ + - deploy: + name: deploy-prod + environment: prod + requires: + - approve-prod + context: forms-processor-v6-prod-deploy + serial-group: << pipeline.project.slug >>/deploy-prod + filters: + branches: + only: master + tags: + ignore: /.*/ diff --git a/.gitignore b/.gitignore index ded37c1..3532471 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ awsenvconf awsconfiguration.sh __pycache__/ deploy/release-*.json + +# Infrastructure definitions and detailed AWS inventories are maintained privately. +/deploy/iam/ +/deploy/*-baseline.json diff --git a/.nvmrc b/.nvmrc index 60bb1e6..63f87c2 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -26.8.1 +26.12.1 diff --git a/Dockerfile b/Dockerfile index 533b11e..4bea4b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,29 @@ -FROM node:26.8.1-bookworm-slim AS build +ARG NODE_VERSION=26.12.1 +FROM node:${NODE_VERSION}-alpine AS build WORKDIR /app -COPY package*.json ./ -RUN npm ci +COPY package.json package-lock.json ./ +COPY tools ./tools +RUN npm ci --install-links=true COPY . . -RUN npm run lint && npm run typecheck && npm test && npm run build +RUN npm run lint && npm run typecheck && npm test && npm run test:dependency-compat && npm run build -FROM node:26.8.1-bookworm-slim AS runtime +FROM build AS production-dependencies +RUN npm prune --install-links=true --omit=dev + +FROM alpine:3.24 AS runtime +ARG NODE_VERSION WORKDIR /app ENV NODE_ENV=production -COPY package*.json ./ -RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force -COPY --from=build --chown=node:node /app/dist ./dist +RUN apk upgrade --no-cache \ + && apk add --no-cache ca-certificates "nodejs-current~${NODE_VERSION%%.*}" \ + && addgroup -S -g 10001 app \ + && adduser -S -D -u 10001 -G app app +COPY --from=production-dependencies --chown=app:app /app/package.json /app/package-lock.json ./ +COPY --from=production-dependencies --chown=app:app /app/node_modules ./node_modules +COPY --from=build --chown=app:app /app/dist ./dist COPY deploy/certs/us-east-1-bundle.pem ./deploy/certs/us-east-1-bundle.pem ENV NODE_EXTRA_CA_CERTS=/app/deploy/certs/us-east-1-bundle.pem -USER node +USER app EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=5s --start-period=30s CMD ["node", "-e", "fetch('http://127.0.0.1:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] CMD ["node", "dist/main.js"] diff --git a/README.md b/README.md index 83b7688..3d134d1 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Submissions that do not opt in stay in the API and do not reach this processor. ```sh nvm use -npm ci +npm ci --install-links=true cp .env.example .env # Apply forms-api-v6 migrations to your local forms database first. npm run dev @@ -264,3 +264,32 @@ See [deploy/README.md](deploy/README.md). CircleCI checks every branch and deplo pinned Topcoder AWS credential helper as forms-api-v6. Each environment needs its existing ECS service/ECR repository, required SSM references and API migrations before deployment. The pipeline registers a new task revision and updates the existing service; it does not apply CloudFormation. + +## Shared authentication and bus client (v6 refresh) + +This branch uses `tc-core-library-js` **4.0.0-rc.1**. Use its named +`createJwtVerifier` / `createJwtAuthenticator`, `createM2MClient`, and +`createBusApiClient` exports for shared authentication and bus API publishing. +Service-specific roles, scopes, sessions and ownership checks remain in this service. +The former bus wrapper dependency and local JWT cryptography must not be reintroduced. + +RSA verification requires an exact configured issuer and audience (`JWT_AUDIENCE` +for client tokens, `AUTH0_AUDIENCE` for machine/API tokens, or this service's explicit +issuer-policy settings). HMAC secrets must be at least 32 bytes; no default secret +is supplied. Expiry is enforced. Machine identity comes from the shared verifier, +not the presence of scopes. Supply real development credentials through the service's +existing environment configuration before deployment. + +Bus publishers use `createBusApiClient(config).postEvent(event)` with the existing +event envelope and optional key. Configure `BUSAPI_URL` for v6 and the existing +OAuth credentials/token provider. The client uses bounded HTTPS requests and does +not retry POSTs automatically. Application retry/outbox behavior remains local. + +See the [core migration guide](https://github.com/topcoder-platform/tc-core-library-js/blob/v4.0.0-rc.1/MIGRATION.md) +for the API mapping, compatibility requirements, and rollout scope. + +## Security maintenance + +The production image uses Node.js 26.12.1 on Alpine 3.24 and runs as UID 10001. It contains only compiled application code, production dependencies, and the RDS trust bundle. npm, development dependencies, and environment files are excluded. Dependencies install from the lockfile, with the shared core pinned to an immutable patched revision. + +Run `npm run lint`, `npm run typecheck`, `npm test`, `npm run test:dependency-compat`, and `npm audit`. The dependency compatibility tests preserve the legacy matcher API while using its patched engine. Database integration tests continue to require a disposable database named `forms_processor_test`. diff --git a/deploy/MIGRATION.md b/deploy/MIGRATION.md new file mode 100644 index 0000000..08cffb1 --- /dev/null +++ b/deploy/MIGRATION.md @@ -0,0 +1,42 @@ +# OIDC deployment migration + +This change replaces the Auth0 credential broker and SSM-to-shell deployment path with [tc-deploy-scripts v2.0.5](https://github.com/topcoder-platform/tc-deploy-scripts/releases/tag/v2.0.5), fetched by tag and verified against commit `bff574ab5c0b46773e25d2af1eff76675fba2ba5`. Build and deployment run in separate jobs. The deployer cannot read Parameter Store, Secrets Manager or decrypt KMS data, including through another assumed role. ECS continues resolving the existing runtime secret references using the execution role. + +## Production target + +The read-only inventory on 2026-11-03 verified account `409275337247`, cluster `topcoder-infrastructure`, service `forms-processor-v6`, family `forms-processor-v6`, and container `forms-processor-v6`. `prod.json` preserves the observed task/execution roles and names the proposed service-specific deployment role. [prod-baseline.json](https://github.com/topcoder-platform/v6-services-iac/blob/main/services/forms-processor-v6/deploy/prod-baseline.json) records references only; it contains no parameter values. Do not recreate the service or replace its complete task definition from this baseline. + +## Infrastructure prerequisites, applied separately by an administrator + +1. In each account, register the CircleCI OIDC issuer `https://oidc.circleci.com/org/947bef5e-1669-415d-b83c-4fc85155b7df` with client ID `947bef5e-1669-415d-b83c-4fc85155b7df`. The current production account has only the legacy Auth0 provider. Do not remove that provider until all its consumers have migrated. +2. Review and install [iam/prod-role.cloudformation.json](https://github.com/topcoder-platform/v6-services-iac/blob/main/services/forms-processor-v6/deploy/iam/prod-role.cloudformation.json) with an administrator/IaC identity. It creates the proposed deployment role and applies the same least-privilege policy as a permissions boundary. The trust binds this CircleCI project, repository, checkout branch and configuration branch. It does not grant infrastructure creation to CircleCI. The role and matching boundary include `inspector2:ListFindings` and `inspector2:ListCoverage` restricted to the deployment region; these scan-metadata APIs require `Resource: "*"`. Secret-access denies remain explicit. +3. Configure ECR repository `forms-processor-v6` with unconditional `IMMUTABLE` tags and supported scanning. Preserve existing enhanced scanning where enabled. Existing tags are not retagged. The release fails for MEDIUM/HIGH/CRITICAL/UNDEFINED findings, failed scans, or a mismatched digest. +4. In the infrastructure that owns the task definition, add the reviewed container check from `prod-healthcheck.json`, keeping all roles, runtime secret references, resources, mounts and sidecars. Verify the health endpoint against the running container in dev first. Preserve circuit-breaker rollback and positive desired capacity. Remove any `RESET_DB` secret binding and plaintext secret environment entries before adoption. Keep the execution role's runtime read permissions scoped to the references that service needs. +5. Create restricted `forms-processor-v6-dev-deploy` and `forms-processor-v6-prod-deploy` CircleCI contexts without Auth0/static AWS credentials. Restrict production to the reviewed `master` checkout and configuration ref, authorized operators, and approval job. Remove legacy credentials from project settings too: removing `org-global` alone does not remove project-level variables. Confirm `CIRCLE_OIDC_TOKEN_V3` is issued. Disable secret access for forks and SSH debugging in deployment jobs. + +## Development acceptance before merge/release + +Read-only inventory on 2026-11-04 verified `dev.json` against the actual development service, task/execution roles and image repositories. This PR targets `develop`. Matching IAM templates and detailed baselines remain private. **Keep this PR in draft until the service-specific prerequisites below are complete.** No AWS or CircleCI resources were provisioned by this refresh. + +- Install the service deployment role and matching permissions boundary. +- Create the restricted empty CircleCI deployment context. + +Run the shared offline suite, validate the CircleCI config and IAM policies, and test a dev release plus an unhealthy-image rollback. Verify CloudTrail contains no SSM/Secrets Manager calls from the deployment principal. Remove its old broker grants only after migration. Check the runtime secret references and roles stayed unchanged. Production requires the manual approval job and a passing digest/scan/health verification. + +The pinned executor is `cimg/aws@sha256:4cf3768db00ddf9bc439b8c84e8a60c11a14edf81c8baf76f74132144f8340d5`. Python/AWS CLI/Buildx/Git compatibility was tested with the shared offline suite. No package installation or service build runs in the deployment job. The Dockerfile retains the existing in-image quality gates. This PR does not deploy, change IAM, rotate credentials, or claim a successful live rollout. + +## Processor release behavior + +Application lint/type checks, routing/retry unit tests, PostgreSQL integration tests and compilation remain prerequisites. Deployment tests now run the pinned shared security/rollout suite. The old SSM-discovery release implementation and its discovery-specific tests are replaced. Existing ECS runtime references are preserved byte-for-byte instead of discovering optional settings during a release; additions/removals belong in the infrastructure that owns the task. Forms API schema migration must still precede a processor release. Zero-capacity activation/bootstrap is an administrator operation: the deployer refuses it. + +## Infrastructure source + +IAM templates and detailed AWS baselines are maintained in the private [v6-services-iac service directory](https://github.com/topcoder-platform/v6-services-iac/tree/main/services/forms-processor-v6). Provision infrastructure separately with an administrator identity; application CI does not fetch that repository or install IAM. Required application manifests and health-check proposals remain here. + +## Dev branch refresh + +This PR targets `develop` and preserves its current application/dependency fixes. The shared suite is v2.0.5 (82 offline tests); service-specific adapters and their tests remain part of review. Keep this PR in draft until its documented infrastructure and integration prerequisites are met. Updating these files does not provision AWS or CircleCI resources. + +Source checkout uses the public HTTPS repository and verifies the exact CircleCI commit, avoiding dependency on failing SSH checkout keys. No GitHub token is added to build or deployment contexts. + +The credential-free builder rejects committed `.npmrc` files. Preserve the existing local compatibility dependencies with explicit `--install-links=true` npm options instead; do not reintroduce credential-capable npm configuration files. diff --git a/deploy/dev-healthcheck.json b/deploy/dev-healthcheck.json new file mode 100644 index 0000000..d23559b --- /dev/null +++ b/deploy/dev-healthcheck.json @@ -0,0 +1,14 @@ +{ + "forms-processor-v6": { + "command": [ + "CMD", + "node", + "-e", + "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" + ], + "interval": 30, + "timeout": 5, + "retries": 3, + "startPeriod": 60 + } +} diff --git a/deploy/dev.json b/deploy/dev.json new file mode 100644 index 0000000..a36697a --- /dev/null +++ b/deploy/dev.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": 2, + "environment": "dev", + "accountId": "811668436784", + "region": "us-east-1", + "deployRoleArn": "arn:aws:iam::811668436784:role/forms-processor-v6-dev-deploy", + "taskRoleArn": "arn:aws:iam::811668436784:role/forms-processor-v6-dev-TaskRole-zeTo8YcPLgMU", + "executionRoleArn": "arn:aws:iam::811668436784:role/forms-processor-v6-dev-ExecutionRole-jvv8jQ05uWJh", + "cluster": "topcoder-infrastructure", + "service": "forms-processor-v6", + "taskFamily": "forms-processor-v6", + "circleciOrgId": "947bef5e-1669-415d-b83c-4fc85155b7df", + "circleciProjectId": "9abc16b0-0d52-490f-a4c0-6e3db3ede656", + "repository": "github.com/topcoder-platform/forms-processor-v6", + "branch": "develop", + "images": { + "forms-processor-v6": "forms-processor-v6" + } +} diff --git a/deploy/prod-healthcheck.json b/deploy/prod-healthcheck.json new file mode 100644 index 0000000..d23559b --- /dev/null +++ b/deploy/prod-healthcheck.json @@ -0,0 +1,14 @@ +{ + "forms-processor-v6": { + "command": [ + "CMD", + "node", + "-e", + "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" + ], + "interval": 30, + "timeout": 5, + "retries": 3, + "startPeriod": 60 + } +} diff --git a/deploy/prod.json b/deploy/prod.json new file mode 100644 index 0000000..6fcfac1 --- /dev/null +++ b/deploy/prod.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": 2, + "environment": "prod", + "accountId": "409275337247", + "region": "us-east-1", + "deployRoleArn": "arn:aws:iam::409275337247:role/forms-processor-v6-prod-deploy", + "taskRoleArn": "arn:aws:iam::409275337247:role/forms-processor-v6-production-TaskRole-F7Uvj6UKRNRm", + "executionRoleArn": "arn:aws:iam::409275337247:role/forms-processor-v6-production-ExecutionRole-M3KAOLfyCIxx", + "cluster": "topcoder-infrastructure", + "service": "forms-processor-v6", + "taskFamily": "forms-processor-v6", + "circleciOrgId": "947bef5e-1669-415d-b83c-4fc85155b7df", + "circleciProjectId": "9abc16b0-0d52-490f-a4c0-6e3db3ede656", + "repository": "github.com/topcoder-platform/forms-processor-v6", + "branch": "master", + "images": { + "forms-processor-v6": "forms-processor-v6" + } +} diff --git a/deploy/release.py b/deploy/release.py index ade1a8d..de70d94 100644 --- a/deploy/release.py +++ b/deploy/release.py @@ -1,210 +1,13 @@ #!/usr/bin/env python3 -"""Release a prebuilt image to an existing ECS service, without provisioning infrastructure. - -Usage: release.py dev|production IMAGE_TAG [LOCAL_IMAGE]. Uses inherited temporary -AWS credentials, boto3 and Docker. API migrations must precede this release. -""" -import base64 -from copy import deepcopy +"""Pass the reviewed manifest and this job's prebuilt images to the pinned suite.""" import json -import os from pathlib import Path -import re import subprocess import sys -import time - -import boto3 -from botocore.config import Config - -APP = 'forms-processor-v6' -CLUSTER = 'topcoder-infrastructure' -ACCOUNTS = {'dev': '811668436784', 'production': '409275337247'} -SSM_CLIENT_CONFIG = Config(retries={'mode': 'standard', 'total_max_attempts': 10}) - - -def read_service(ecs): - """Read the fixed ECS service using ecs; return active service or raise on missing/invalid state.""" - result = ecs.describe_services(cluster=CLUSTER, services=[APP]) - if result.get('failures') or len(result.get('services', [])) != 1: - raise RuntimeError('Existing forms-processor-v6 ECS service is required') - service = result['services'][0] - if service['status'] != 'ACTIVE': - raise RuntimeError('Existing ECS service must be active') - return service - - -# Optional processor-owned settings for AI classification and label actions (docs/adr/0001), -# and for the SpaceTech challenge notification. Absent values defer the matching flows; -# the code defaults apply where documented. -LABEL_ACTION_SETTINGS = [ - 'AI_API_URL', 'AI_AUTH0_AUDIENCE', - 'LETS_TALK_CLASSIFY_MAX_ATTEMPTS', 'LETS_TALK_CLASSIFY_MAX_AGE_MINUTES', - 'LETS_TALK_OPPORTUNITY_TEMPLATE_ID', 'LETS_TALK_SKILLS_TEMPLATE_ID', - 'LETS_TALK_GENERAL_TEMPLATE_ID', 'LETS_TALK_WIPRO_TEMPLATE_ID', - 'LETS_TALK_SALES_REPLY_TEMPLATE_ID', 'LETS_TALK_SUPPORT_TEMPLATE_ID', - 'LETS_TALK_SUPPORT_RECIPIENTS', 'LETS_TALK_SALES_REPLY_TO', 'LETS_TALK_REPLY_CAP_PER_DAY', - 'SPACE_TECH_CHALLENGE_RECIPIENTS', 'SPACE_TECH_CHALLENGE_TEMPLATE_ID', -] - - -def secret_references(ssm): - """Resolve named SSM references in batches of ten, without decrypting SecureStrings. - - Takes an SSM client; returns name/valueFrom pairs. API Auth0 settings take - precedence over shared defaults. Kafka uses globals. Missing optional sales - settings permit a disabled-flow deployment. Raises for absent required values - or sales/Bus parameters that are not SecureStrings; AWS errors propagate after - client retries. GetParameters values are discarded; only names/types are used. - Avoids the low-throughput DescribeParameters scan of whole parameter prefixes. - """ - prefixes = {'api': '/config/forms-api-v6/appvar', - 'app': f'/config/{APP}/appvar', - 'common': '/config/common/global-appvar'} - sources = {'DATABASE_URL': ['api'], 'BUSAPI_URL': ['app'], - 'AUTH0_CLIENT_ID': ['api'], 'AUTH0_CLIENT_SECRET': ['api'], - 'AUTH0_URL': ['api', 'common'], 'AUTH0_AUDIENCE': ['api', 'common'], - 'AUTH0_PROXY_SERVER_URL': ['api', 'common'], - 'KAFKA_URL': ['common'], 'KAFKA_SSL_ENABLED': ['common'], - 'LETS_TALK_RECIPIENTS': ['app'], 'LETS_TALK_SENDGRID_TEMPLATE_ID': ['app'], - **{name: ['app'] for name in LABEL_ACTION_SETTINGS}} - names = [prefixes[source] + '/' + name - for name, candidates in sources.items() for source in candidates] - available = {} - for start in range(0, len(names), 10): - response = ssm.get_parameters(Names=names[start:start + 10], WithDecryption=False) - available.update({p['Name']: p['Type'] for p in response['Parameters']}) - optional = {'AUTH0_PROXY_SERVER_URL', 'KAFKA_SSL_ENABLED', - 'LETS_TALK_RECIPIENTS', 'LETS_TALK_SENDGRID_TEMPLATE_ID', *LABEL_ACTION_SETTINGS} - references = [] - for name, candidates in sources.items(): - path = next((prefixes[source] + '/' + name for source in candidates - if prefixes[source] + '/' + name in available), None) - if path is None: - if name not in optional: - raise RuntimeError('Missing required SSM parameter: ' + name) - continue - # Every processor-owned parameter (Bus URL, recipients, templates, AI settings) is a SecureString. - if path.startswith(prefixes['app'] + '/'): - if available[path] != 'SecureString': - raise RuntimeError('SecureString required: ' + path) - references.append({'name': name, 'valueFrom': path}) - return references - - -def task_request(ecs, current, image, references): - """Clone the live task's registerable fields and replace the processor image/managed secrets. - - Inputs are the ECS client, described task, ECR image URI and SSM references. - Returns a register-task-definition request, preserving roles/resources/sidecars. - Raises if family or application container identity differs from the fixed service. - """ - if current['family'] != APP: - raise RuntimeError('Unexpected task family') - allowed = ecs.meta.service_model.operation_model('RegisterTaskDefinition').input_shape.members - request = {key: deepcopy(value) for key, value in current.items() if key in allowed} - containers = [c for c in request['containerDefinitions'] if c['name'] == APP] - if len(containers) != 1: - raise RuntimeError('Expected one forms-processor-v6 container') - container = containers[0] - managed = {'DATABASE_URL', 'BUSAPI_URL', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', - 'AUTH0_URL', 'AUTH0_AUDIENCE', 'AUTH0_PROXY_SERVER_URL', 'KAFKA_URL', - 'LETS_TALK_RECIPIENTS', 'LETS_TALK_SENDGRID_TEMPLATE_ID', *LABEL_ACTION_SETTINGS} - # Preserve an existing TLS setting if the global store does not define it. - managed.update(ref['name'] for ref in references) - container['environment'] = [e for e in container.get('environment', []) if e['name'] not in managed] - container['secrets'] = [s for s in container.get('secrets', []) if s['name'] not in managed] + references - container['image'] = image - container['healthCheck'] = { - 'command': ['CMD', 'node', '-e', - "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"], - 'interval': 30, 'timeout': 5, 'retries': 3, 'startPeriod': 60, - } - return request - - -def verify_service(ecs, definition_arn): - """Observe one rollout using ecs and the requested ARN; return service when tasks are healthy. - - Returns None while converging; raises on rollback/replacement/failed rollout or - AWS errors. Used by the bounded release waiter, including activation from zero. - """ - service = read_service(ecs) - if service['taskDefinition'] != definition_arn: - raise RuntimeError('ECS rolled back or replaced the requested task definition') - deployments = service['deployments'] - if any(d.get('rolloutState') == 'FAILED' for d in deployments): - raise RuntimeError('ECS deployment failed') - if (len(deployments) != 1 or deployments[0].get('rolloutState') != 'COMPLETED' - or service['runningCount'] != service['desiredCount'] or service['desiredCount'] < 1 - or service['pendingCount']): - return None - arns = [] - for page in ecs.get_paginator('list_tasks').paginate( - cluster=CLUSTER, serviceName=APP, desiredStatus='RUNNING'): - arns.extend(page['taskArns']) - tasks = [] - for start in range(0, len(arns), 100): - tasks.extend(ecs.describe_tasks(cluster=CLUSTER, tasks=arns[start:start + 100])['tasks']) - healthy = [t for t in tasks if t['taskDefinitionArn'] == definition_arn - and t.get('healthStatus') == 'HEALTHY' and t['lastStatus'] == 'RUNNING'] - return service if len(healthy) >= service['desiredCount'] else None - - -def main(): - """Read CLI arguments, verify account/service, push image, register a revision and update ECS. - - Returns None after verified deployment and a non-secret release artifact. Raises - on invalid arguments/account, missing existing infrastructure, AWS/Docker errors, - concurrent service change, rollback or a 30-minute timeout. Never updates stacks, - creates services, modifies networking/IAM, runs migrations, or enables email flows. - """ - if len(sys.argv) not in (3, 4) or sys.argv[1] not in ACCOUNTS: - raise RuntimeError(__doc__) - environment, tag = sys.argv[1:3] - if not re.fullmatch(r'[A-Za-z0-9_][A-Za-z0-9_.-]{0,110}', tag): - raise RuntimeError('Invalid image tag') - session = boto3.Session(region_name=os.environ.get('AWS_REGION', 'us-east-1')) - account = session.client('sts').get_caller_identity()['Account'] - if account != ACCOUNTS[environment]: - raise RuntimeError('AWS account does not match requested environment') - ecs, ecr = [session.client(name) for name in ('ecs', 'ecr')] - ssm = session.client('ssm', config=SSM_CLIENT_CONFIG) - service = read_service(ecs) - if len(service['deployments']) != 1 or service['deployments'][0].get('rolloutState') != 'COMPLETED': - raise RuntimeError('Existing ECS deployment must be stable before releasing') - current = ecs.describe_task_definition(taskDefinition=service['taskDefinition'])['taskDefinition'] - references = secret_references(ssm) - repository = ecr.describe_repositories(repositoryNames=[APP])['repositories'][0]['repositoryUri'] - image = f'{repository}:{tag}' - request = task_request(ecs, current, image, references) - authorization = ecr.get_authorization_token()['authorizationData'][0] - user, password = base64.b64decode(authorization['authorizationToken']).decode().split(':', 1) - subprocess.run(['docker', 'login', '--username', user, '--password-stdin', authorization['proxyEndpoint']], - input=password, text=True, check=True, stdout=subprocess.DEVNULL) - local_image = sys.argv[3] if len(sys.argv) == 4 else APP + ':candidate' - subprocess.run(['docker', 'tag', local_image, image], check=True) - subprocess.run(['docker', 'push', image], check=True) - if read_service(ecs)['taskDefinition'] != service['taskDefinition']: - raise RuntimeError('Service changed during image push; retry after inspecting concurrent release') - definition_arn = ecs.register_task_definition(**request)['taskDefinition']['taskDefinitionArn'] - ecs.update_service(cluster=CLUSTER, service=APP, taskDefinition=definition_arn, - desiredCount=max(1, service['desiredCount'])) - deadline = time.monotonic() + 1800 - while time.monotonic() < deadline: - retained = verify_service(ecs, definition_arn) - if retained: - break - print('Waiting for healthy requested ECS revision', flush=True) - time.sleep(15) - else: - raise RuntimeError('ECS deployment timed out; inspect before retrying') - evidence = {'environment': environment, 'cluster': CLUSTER, 'service': APP, 'image': image, - 'taskDefinition': definition_arn, 'previousTaskDefinition': service['taskDefinition'], - 'runningCount': retained['runningCount']} - Path(f'deploy/release-{environment}.json').write_text(json.dumps(evidence, indent=2) + '\n') - print(json.dumps(evidence, indent=2)) - -if __name__ == '__main__': - main() +manifest = Path(sys.argv[1]) +data = json.loads(manifest.read_text()) +command = [sys.executable, '-I', '/tmp/tc-deploy-scripts/tcdeploy.py', 'deploy', '--manifest', str(manifest)] +for container in data['images']: + command.extend(['--image', container + '=forms-processor-v6:candidate']) +subprocess.run(command, check=True) diff --git a/deploy/tests/test_release.py b/deploy/tests/test_release.py deleted file mode 100644 index c83e9a1..0000000 --- a/deploy/tests/test_release.py +++ /dev/null @@ -1,220 +0,0 @@ -"""Exercise rollout guards and secret-reference selection without contacting AWS.""" -import importlib.util -import json -from pathlib import Path -from types import SimpleNamespace -import unittest -from unittest.mock import Mock, patch - -import boto3 -from botocore.awsrequest import AWSResponse -from botocore.exceptions import ClientError - -spec = importlib.util.spec_from_file_location('release', Path(__file__).parents[1] / 'release.py') -release = importlib.util.module_from_spec(spec) -spec.loader.exec_module(release) - - -class ReleaseTests(unittest.TestCase): - """Verify that releases update existing ECS tasks and cannot cross environment boundaries.""" - - def setUp(self): - """Build named-parameter fixtures for each test; takes no arguments and returns nothing.""" - entries = { - '/config/forms-api-v6/appvar': ['DATABASE_URL', 'AUTH0_CLIENT_ID', 'AUTH0_CLIENT_SECRET', 'AUTH0_URL'], - '/config/forms-processor-v6/appvar': ['BUSAPI_URL', 'LETS_TALK_RECIPIENTS', 'LETS_TALK_SENDGRID_TEMPLATE_ID', - *release.LABEL_ACTION_SETTINGS], - '/config/common/global-appvar': ['AUTH0_URL', 'AUTH0_AUDIENCE', 'AUTH0_PROXY_SERVER_URL', 'KAFKA_URL', 'KAFKA_SSL_ENABLED'], - } - self.parameters = { - path + '/' + name: {'Name': path + '/' + name, 'Type': 'SecureString', 'Value': 'unused-ciphertext'} - for path, names in entries.items() for name in names - } - self.ssm = Mock() - self.ssm.get_parameters.side_effect = self.parameter_response - - def parameter_response(self, *, Names, WithDecryption): - """Return the requested fixture parameters and absent names; rejects oversized/decrypting reads. - - Called by the SSM mock with AWS request arguments. Reverses result order to - verify lookup by name, and raises AssertionError for unsafe request options. - """ - self.assertFalse(WithDecryption) - self.assertGreater(len(Names), 0) - self.assertLessEqual(len(Names), 10) - return {'Parameters': [self.parameters[name] for name in reversed(Names) if name in self.parameters], - 'InvalidParameters': [name for name in Names if name not in self.parameters]} - - def test_missing_service_is_rejected(self): - """A missing service raises before release can provision anything; no inputs or return value.""" - ecs = Mock() - ecs.describe_services.return_value = {'services': [], 'failures': [{'reason': 'MISSING'}]} - with self.assertRaisesRegex(RuntimeError, 'Existing'): - release.read_service(ecs) - ecs.update_service.assert_not_called() - - def test_secret_sources_and_types(self): - """Two bounded non-decrypting reads return only named references with the intended precedence.""" - refs = {r['name']: r['valueFrom'] for r in release.secret_references(self.ssm)} - self.assertEqual(refs['AUTH0_URL'], '/config/forms-api-v6/appvar/AUTH0_URL') - self.assertEqual(refs['AUTH0_AUDIENCE'], '/config/common/global-appvar/AUTH0_AUDIENCE') - self.assertEqual(refs['DATABASE_URL'], '/config/forms-api-v6/appvar/DATABASE_URL') - self.assertEqual(refs['KAFKA_URL'], '/config/common/global-appvar/KAFKA_URL') - self.assertEqual(refs['BUSAPI_URL'], '/config/forms-processor-v6/appvar/BUSAPI_URL') - self.assertEqual(len(refs), 11 + len(release.LABEL_ACTION_SETTINGS)) - self.assertNotIn('unused-ciphertext', json.dumps(refs)) - requested = [name for call in self.ssm.get_parameters.call_args_list for name in call.kwargs['Names']] - expected = 14 + len(release.LABEL_ACTION_SETTINGS) - self.assertEqual(self.ssm.get_parameters.call_count, -(-expected // 10)) - self.assertEqual(len(requested), expected) - self.assertEqual(len(set(requested)), expected) - self.assertEqual(refs['AI_API_URL'], '/config/forms-processor-v6/appvar/AI_API_URL') - self.assertNotIn('/config/forms-api-v6/appvar/MIGRATION_DATABASE_URL', requested) - self.ssm.get_paginator.assert_not_called() - self.ssm.describe_parameters.assert_not_called() - - def test_optional_parameters_and_auth_fallback(self): - """Missing optional settings are omitted and absent API Auth0 settings use shared defaults.""" - del self.parameters['/config/forms-api-v6/appvar/AUTH0_URL'] - for name in ['LETS_TALK_RECIPIENTS', 'LETS_TALK_SENDGRID_TEMPLATE_ID', *release.LABEL_ACTION_SETTINGS]: - del self.parameters['/config/forms-processor-v6/appvar/' + name] - for name in ['KAFKA_SSL_ENABLED', 'AUTH0_PROXY_SERVER_URL']: - del self.parameters['/config/common/global-appvar/' + name] - refs = {r['name']: r['valueFrom'] for r in release.secret_references(self.ssm)} - self.assertEqual(refs['AUTH0_URL'], '/config/common/global-appvar/AUTH0_URL') - self.assertEqual(len(refs), 7) - - def test_required_parameter_missing_fails(self): - """A missing required database reference raises instead of publishing a broken task.""" - del self.parameters['/config/forms-api-v6/appvar/DATABASE_URL'] - with self.assertRaisesRegex(RuntimeError, 'Missing required'): - release.secret_references(self.ssm) - - def test_sales_parameters_must_remain_secure_strings(self): - """Wrong types for each required Bus or optional sales value raise without exposing its value.""" - for name in ['BUSAPI_URL', 'LETS_TALK_RECIPIENTS', 'LETS_TALK_SENDGRID_TEMPLATE_ID', - 'AI_API_URL', 'LETS_TALK_SALES_REPLY_TO']: - parameter = self.parameters['/config/forms-processor-v6/appvar/' + name] - with self.subTest(name=name): - parameter['Type'] = 'String' - with self.assertRaisesRegex(RuntimeError, 'SecureString') as raised: - release.secret_references(self.ssm) - self.assertNotIn('unused-ciphertext', str(raised.exception)) - parameter['Type'] = 'SecureString' - - def test_ssm_failures_stop_before_deployment(self): - """Access denial and exhausted throttling propagate before any ECR, Docker or ECS mutation.""" - for code in ['AccessDeniedException', 'ThrottlingException']: - with self.subTest(code=code): - session, ecs, ecr = Mock(), Mock(), Mock() - sts = Mock() - sts.get_caller_identity.return_value = {'Account': release.ACCOUNTS['production']} - session.client.side_effect = lambda name, **kwargs: { - 'sts': sts, 'ecs': ecs, 'ecr': ecr, 'ssm': self.ssm}[name] - ecs.describe_services.return_value = {'services': [{ - 'status': 'ACTIVE', 'taskDefinition': 'current', - 'deployments': [{'rolloutState': 'COMPLETED'}]}]} - ecs.describe_task_definition.return_value = {'taskDefinition': {'family': release.APP}} - self.ssm.get_parameters.side_effect = ClientError({'Error': { - 'Code': code, 'Message': 'Parameter read failed'}}, 'GetParameters') - with patch.object(release.boto3, 'Session', return_value=session), \ - patch.object(release.sys, 'argv', ['release.py', 'production', 'candidate']), \ - patch.object(release.subprocess, 'run') as run: - with self.assertRaises(ClientError): - release.main() - session.client.assert_any_call('ssm', config=release.SSM_CLIENT_CONFIG) - ecr.describe_repositories.assert_not_called() - ecs.register_task_definition.assert_not_called() - ecs.update_service.assert_not_called() - run.assert_not_called() - - def test_sdk_retries_throttling_and_has_a_finite_budget(self): - """Exercise real SDK retry handling offline: recover after five throttles and stop after ten attempts.""" - # GetParameters reads ten names per request. - batches = -(-(14 + len(release.LABEL_ACTION_SETTINGS)) // 10) - for throttle_count, expected_requests in [(5, 5 + batches), (100, 10)]: - with self.subTest(throttle_count=throttle_count): - ssm = boto3.client('ssm', region_name='us-east-1', aws_access_key_id='test', - aws_secret_access_key='test', config=release.SSM_CLIENT_CONFIG) - requests = [] - - def respond(request): - """Return an AWSResponse for a prepared HTTP request; fake throttles precede fixture reads.""" - requests.append(json.loads(request.body)) - self.assertEqual(request.headers['X-Amz-Target'], b'AmazonSSM.GetParameters') - throttled = len(requests) <= throttle_count - body = {'__type': 'ThrottlingException', 'message': 'Rate exceeded'} if throttled \ - else self.parameter_response(**requests[-1]) - raw = SimpleNamespace(stream=lambda: iter([json.dumps(body).encode()])) - return AWSResponse(request.url, 400 if throttled else 200, - {'content-type': 'application/x-amz-json-1.1'}, raw) - - with patch.object(ssm._endpoint.http_session, 'send', side_effect=respond), \ - patch('botocore.endpoint.time.sleep'): - if throttle_count == 5: - self.assertEqual(len(release.secret_references(ssm)), 11 + len(release.LABEL_ACTION_SETTINGS)) - else: - with self.assertRaises(ClientError): - release.secret_references(ssm) - self.assertEqual(len(requests), expected_requests) - self.assertTrue(all(not request['WithDecryption'] for request in requests)) - ssm.close() - - def test_clone_preserves_roles_resources_and_removes_stale_literals(self): - """Cloning returns a registerable copy with exact secret overrides and unchanged sidecars/roles.""" - ecs = boto3.client('ecs', region_name='us-east-1', aws_access_key_id='test', aws_secret_access_key='test') - current = {'family': release.APP, 'taskDefinitionArn': 'old', 'revision': 7, - 'executionRoleArn': 'execution', 'taskRoleArn': 'task', 'cpu': '256', 'memory': '512', - 'networkMode': 'awsvpc', 'requiresCompatibilities': ['FARGATE'], - 'containerDefinitions': [{'name': release.APP, 'image': 'old', 'readonlyRootFilesystem': True, - 'environment': [{'name': 'BUSAPI_URL', 'value': 'old'}, {'name': 'KAFKA_SSL_ENABLED', 'value': 'true'}], - 'secrets': [{'name': 'LETS_TALK_RECIPIENTS', 'valueFrom': 'old'}, {'name': 'OTHER', 'valueFrom': '/other'}]}, - {'name': 'sidecar', 'image': 'sidecar'}]} - refs = [{'name': 'BUSAPI_URL', 'valueFrom': '/config/forms-processor-v6/appvar/BUSAPI_URL'}] - result = release.task_request(ecs, current, 'new', refs) - self.assertNotIn('revision', result) - self.assertNotIn('taskDefinitionArn', result) - for key in ['executionRoleArn', 'taskRoleArn', 'cpu', 'memory', 'networkMode', 'requiresCompatibilities']: - self.assertEqual(result[key], current[key]) - container = result['containerDefinitions'][0] - self.assertEqual(container['environment'], [{'name': 'KAFKA_SSL_ENABLED', 'value': 'true'}]) - self.assertEqual(container['secrets'], [{'name': 'OTHER', 'valueFrom': '/other'}] + refs) - self.assertEqual(container['image'], 'new') - self.assertEqual(current['containerDefinitions'][0]['image'], 'old') - self.assertEqual(result['containerDefinitions'][1], current['containerDefinitions'][1]) - self.assertIn('healthCheck', container) - - def test_rollback_and_unhealthy_tasks_are_not_success(self): - """Only healthy tasks of the requested revision finish verification; rollback raises.""" - ecs = Mock() - service = {'status': 'ACTIVE', 'taskDefinition': 'new', 'desiredCount': 1, 'runningCount': 1, - 'pendingCount': 0, 'deployments': [{'rolloutState': 'COMPLETED'}]} - ecs.describe_services.return_value = {'services': [service]} - ecs.get_paginator.return_value.paginate.return_value = [{'taskArns': ['task']}] - task = {'taskDefinitionArn': 'new', 'healthStatus': 'UNHEALTHY', 'lastStatus': 'RUNNING'} - ecs.describe_tasks.return_value = {'tasks': [task]} - self.assertIsNone(release.verify_service(ecs, 'new')) - task['healthStatus'] = 'HEALTHY' - self.assertEqual(release.verify_service(ecs, 'new'), service) - service['deployments'][0]['rolloutState'] = 'FAILED' - with self.assertRaisesRegex(RuntimeError, 'failed'): - release.verify_service(ecs, 'new') - service['taskDefinition'] = 'old' - with self.assertRaisesRegex(RuntimeError, 'rolled back'): - release.verify_service(ecs, 'new') - - def test_wrong_account_stops_before_mutation(self): - """Production with dev credentials raises before ECR, ECS, SSM or Docker are called.""" - session = Mock(region_name='us-east-1') - session.client.return_value.get_caller_identity.return_value = {'Account': release.ACCOUNTS['dev']} - with patch.object(release.boto3, 'Session', return_value=session), \ - patch.object(release.sys, 'argv', ['release.py', 'production', 'candidate']), \ - patch.object(release.subprocess, 'run') as run: - with self.assertRaisesRegex(RuntimeError, 'account'): - release.main() - session.client.assert_called_once_with('sts') - run.assert_not_called() - - -if __name__ == '__main__': - unittest.main() diff --git a/eslint.config.mjs b/eslint.config.mjs index c05d584..1c86c43 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,4 +4,5 @@ export default tseslint.config( { ignores: ["dist/**", "node_modules/**"] }, js.configs.recommended, ...tseslint.configs.recommended, + { files: ["tools/**/*.cjs"], languageOptions: { sourceType: "commonjs", globals: { require: "readonly", module: "readonly", process: "readonly" } }, rules: { "@typescript-eslint/no-require-imports": "off" } }, ); diff --git a/package-lock.json b/package-lock.json index 72d0804..e47b492 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "@platformatic/kafka": "2.8.0", "pg": "8.23.0", - "zod": "4.3.6" + "tc-core-library-js": "https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/58de37670180e183e8378c3d203df0112a0414f6", + "zod": "4.6.7" }, "devDependencies": { "@eslint/js": "9.39.2", @@ -21,7 +22,7 @@ "prettier": "3.8.1", "tsx": "4.22.4", "typescript": "5.9.3", - "typescript-eslint": "8.54.0" + "typescript-eslint": "8.72.1" }, "engines": { "node": ">=26.8.1 <27" @@ -703,6 +704,13 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/nonrecursive-glob-match": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@isaacs/nonrecursive-glob-match/-/nonrecursive-glob-match-1.0.3.tgz", + "integrity": "sha512-SI7kyKHq4XXDRozw+r9GlRZkia1ZkFrpLW98r2IC2Bpr8YkFGE3fvjzmxCG0x78A++69HmDNrDvSMYBZmHGpKA==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/@node-rs/crc32": { "version": "1.10.8", "resolved": "https://registry.npmjs.org/@node-rs/crc32/-/crc32-1.10.8.tgz", @@ -835,9 +843,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -854,9 +859,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -873,9 +875,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -892,9 +891,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -998,9 +994,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz", + "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==", "dev": true, "license": "MIT" }, @@ -1011,11 +1007,26 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "26.0.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~8.3.0" @@ -1034,20 +1045,20 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", - "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.1.tgz", + "integrity": "sha512-5CokTWXpmbylfFabsbvHC0CAw3XLaGnoa+nFJX5FR592h/LoZsENUE6p/rg5xvkKrFEXz6pYmgI29AgXc2N3BQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/type-utils": "8.54.0", - "@typescript-eslint/utils": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/type-utils": "8.72.1", + "@typescript-eslint/utils": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1057,15 +1068,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.54.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "@typescript-eslint/parser": "^8.72.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.10.tgz", - "integrity": "sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz", + "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", "dev": true, "license": "MIT", "engines": { @@ -1073,16 +1084,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", - "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.1.tgz", + "integrity": "sha512-ZGwZP4OCfyA0AQTpEAMWz+rtX01DRhQWW5hB0ucKDEIX5U7jdmPSaMx94rQfaPAQzIYz2BimOgKOJtjfpRFQ9g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", "debug": "^4.4.3" }, "engines": { @@ -1093,19 +1104,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", - "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.1.tgz", + "integrity": "sha512-fAuVMkwdO/huI8nyo/WKnNKf6Gag93S56Rh3Ts5mGcavwSdaN8tRg1gCeIGX8PF+nzp1BjGfO4gHhGzW1J6jTg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.54.0", - "@typescript-eslint/types": "^8.54.0", + "@typescript-eslint/tsconfig-utils": "^8.72.1", + "@typescript-eslint/types": "^8.72.1", "debug": "^4.4.3" }, "engines": { @@ -1116,18 +1127,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", - "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.1.tgz", + "integrity": "sha512-lC/t6eMCZZQAAKZEYGL6RDVSuFehpdmMT6h+nJIjIa6oylN/PwDgDVtEIbB7O9xFfZYtZAYLoLlDNWKJhtF+uQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0" + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1138,9 +1149,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", - "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.1.tgz", + "integrity": "sha512-e4D5R5vpeAEPBxT2BAzsFQL3sbwyeeeo5+aczgNSRK1V734jE7pTqaGFU+fXapWEQwnZbpFaTn32+Z8DwS6OTg==", "dev": true, "license": "MIT", "engines": { @@ -1151,21 +1162,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", - "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.1.tgz", + "integrity": "sha512-iY2wlmyrXJmx4yxUGP3aYCFF3cA/6ShddOKmLU2dx7LnEykbAM8WThlxZEfd1NddqceF6eJmZUayxx9oKIYNlg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/utils": "8.72.1", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1175,14 +1186,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", - "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.1.tgz", + "integrity": "sha512-LmE5IhQet1BWOWEXqBrVtBPsMAOQ6Nzk35d+WLh3mk4EUF33sqlfxjio1MdYeSvKSG5skactKKuixBIKXhL4Rw==", "dev": true, "license": "MIT", "engines": { @@ -1194,21 +1205,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", - "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.1.tgz", + "integrity": "sha512-DAlUoUV93hIvrtIQLwCbmZQYVZwOMhiHJrOVUUIWicFZaIYjTRqlY3I/NZevfkLmSqkNHRu2qYDcq9lrn16oOQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.54.0", - "@typescript-eslint/tsconfig-utils": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/project-service": "8.72.1", + "@typescript-eslint/tsconfig-utils": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/visitor-keys": "8.72.1", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1218,46 +1229,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", - "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", - "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.1.tgz", + "integrity": "sha512-5v5liypurJBFHCBNqh/RjsymAD+VWcE0djuGY/BUqp5fC3Qn+luWDEksjuTtWjPn6i5zQlUtkZCRG/CYG0yCkA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0" + "@typescript-eslint/scope-manager": "8.72.1", + "@typescript-eslint/types": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1267,19 +1252,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", - "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.1.tgz", + "integrity": "sha512-SB8xqipSzZHUePCCLNeYxqnxq83X61/qzNhjQGbTfjxGZIeOEP7tBgWYX05Tcd6CgE02WdTJnh5b8kMZJ/+bnQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.72.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1289,10 +1274,23 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/acorn": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", - "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", "dev": true, "license": "MIT", "bin": { @@ -1375,23 +1373,34 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/brace-expansion": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", - "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1439,13 +1448,6 @@ "dev": true, "license": "MIT" }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1485,6 +1487,15 @@ "dev": true, "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/esbuild": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", @@ -1740,9 +1751,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", - "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", "funding": [ { "type": "github", @@ -1827,9 +1838,9 @@ } }, "node_modules/flatted": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", - "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -1951,6 +1962,15 @@ "dev": true, "license": "ISC" }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-yaml": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", @@ -1994,6 +2014,65 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.2.tgz", + "integrity": "sha512-BqTyEDV+lS8F2trk3A+qJnxV5Q9EqKCBJOPti3W97r7qTympCZjb7h2X6f2kc+0K3rsSTY1/6YG2eaXKoj497w==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9.0.4", + "debug": "^4.3.4", + "jose": "^4.15.4", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2018,6 +2097,11 @@ "node": ">= 0.8.0" } }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2034,6 +2118,48 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2041,6 +2167,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -2048,23 +2180,59 @@ "license": "Apache-2.0", "optional": true }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "license": "MIT", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "name": "@topcoder/minimatch-compat", + "version": "1.0.0", + "resolved": "file:tools/minimatch-compat", "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "minimatch-engine": "npm:minimatch@11.0.1" + } + }, + "node_modules/minimatch-engine": { + "name": "minimatch", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-11.0.1.tgz", + "integrity": "sha512-t+Vn6DX4wPCxS23y03C0uhF7S1jpi1GfqPrnlEipCJaA2ykiNbMkVlDutZtmvLfRcUN6R8AYVlzTsQrT4RZoLQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/nonrecursive-glob-match": "^1.0.1", + "brace-expansion": "^5.0.12" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mnemonist": { - "version": "0.40.5", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.5.tgz", - "integrity": "sha512-egXDkYJsKZCizjk8FydZ3g8TOigBwt+dIaehgKcQSAE3DCkCTWG4N94trvwR1ZYeAHo1exxE+ZhZOUuj01HdNw==", + "version": "0.40.6", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.6.tgz", + "integrity": "sha512-c4mbin5HCz77Srw1L3nwThCH/0hLVYuY3oMF2plP2tKfp/gqzSgHP63qRUOpL8jutYWNNCr49LUlKSt7exkaww==", "license": "MIT", "dependencies": { "obliterator": "^2.0.4" @@ -2200,16 +2368,16 @@ } }, "node_modules/pg-cloudflare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", - "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.1.tgz", + "integrity": "sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==", "license": "MIT", "optional": true }, "node_modules/pg-connection-string": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", - "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.1.tgz", + "integrity": "sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==", "license": "MIT" }, "node_modules/pg-int8": { @@ -2231,9 +2399,9 @@ } }, "node_modules/pg-protocol": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", - "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.1.tgz", + "integrity": "sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==", "license": "MIT" }, "node_modules/pg-types": { @@ -2340,9 +2508,9 @@ } }, "node_modules/protobufjs": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.8.0.tgz", - "integrity": "sha512-N3xhQ5yyBx3vQq4gubBfASzYhJGNzeDbjqBpu61g7UVylsN/qyffU96TKWD3GbbLOKF82VGNRNvv1+BFgE31Eg==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.8.1.tgz", + "integrity": "sha512-d9e9179bjXSJdpJ3+R9fP8hzXsnKLyxTjHLG1dUzje8a27pPyXCe6PrcRtgx2rjoM3IZzHd2vAs+6J/BsO1k7Q==", "license": "BSD-3-Clause", "optional": true, "dependencies": { @@ -2391,6 +2559,26 @@ "node": ">=0.10.0" } }, + "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/scule": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", @@ -2401,7 +2589,6 @@ "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2468,6 +2655,20 @@ "node": ">=8" } }, + "node_modules/tc-core-library-js": { + "version": "4.0.0-rc.2", + "resolved": "https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/58de37670180e183e8378c3d203df0112a0414f6", + "integrity": "sha512-sCM3ieXGZtHGJw5OKeqOC9q1iMlj6faEmwnJU7rQ56aiiIFsjN9IfG3GVRBLEeUsAMJH52DIK1/l1NUzV6n+gQ==", + "license": "ISC", + "dependencies": { + "@types/jsonwebtoken": "^9.0.10", + "jsonwebtoken": "^9.0.3", + "jwks-rsa": "^3.2.0" + }, + "engines": { + "node": ">=22.13" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -2545,16 +2746,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", - "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", + "version": "8.72.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.1.tgz", + "integrity": "sha512-3Li9FfPmQp45nxEtFD1ZCrYFI3cYK5kpyNtmm1H7lMagm8u9sy6PTjy3ytwQZs7KNukOyKFHAZ6z6GjYFeaeXw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.54.0", - "@typescript-eslint/parser": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/utils": "8.54.0" + "@typescript-eslint/eslint-plugin": "8.72.1", + "@typescript-eslint/parser": "8.72.1", + "@typescript-eslint/typescript-estree": "8.72.1", + "@typescript-eslint/utils": "8.72.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2564,15 +2765,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/undici-types": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", - "dev": true, "license": "MIT" }, "node_modules/uri-js": { @@ -2620,6 +2820,12 @@ "node": ">=0.4" } }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -2634,9 +2840,9 @@ } }, "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 8b4820f..37d85b9 100644 --- a/package.json +++ b/package.json @@ -17,12 +17,14 @@ "start": "node dist/main.js", "dev": "tsx watch --env-file=.env src/main.ts", "deploy:dev": "BRANCH=$(git rev-parse --abbrev-ref HEAD) && TAG=\"dev-${BRANCH}\" && git tag -d \"$TAG\" 2>/dev/null; git push origin \":refs/tags/$TAG\" 2>/dev/null; git tag \"$TAG\" && git push origin \"$TAG\"", - "format": "prettier --write src test *.json *.mjs .circleci/config.yml deploy/*.yaml" + "format": "prettier --write src test *.json *.mjs .circleci/config.yml deploy/*.yaml", + "test:dependency-compat": "node --test tools/minimatch-compat/index.test.cjs" }, "dependencies": { "@platformatic/kafka": "2.8.0", "pg": "8.23.0", - "zod": "4.3.6" + "zod": "4.6.7", + "tc-core-library-js": "https://codeload.github.com/topcoder-platform/tc-core-library-js/tar.gz/58de37670180e183e8378c3d203df0112a0414f6" }, "devDependencies": { "@eslint/js": "9.39.2", @@ -32,6 +34,17 @@ "prettier": "3.8.1", "tsx": "4.22.4", "typescript": "5.9.3", - "typescript-eslint": "8.54.0" + "typescript-eslint": "8.72.1" + }, + "overrides": { + "minimatch": "file:tools/minimatch-compat", + "brace-expansion@^1": "1.1.21", + "brace-expansion@^2": "2.1.7", + "brace-expansion@>=3": "5.0.12", + "js-yaml@^4": "4.3.2", + "qs": "6.16.1", + "ajv@^6": "6.15.0", + "flatted": "3.4.2", + "fast-uri": "3.1.9" } } diff --git a/src/bus.ts b/src/bus.ts index 83543f1..ac38e50 100644 --- a/src/bus.ts +++ b/src/bus.ts @@ -1,4 +1,9 @@ -import { AuthError, M2MTokenProvider, type M2MConfig } from "./auth.js"; +import { + createBusApiClient, + HttpError, + type BusApiClient as SharedBusClient, +} from "tc-core-library-js"; +import type { M2MConfig } from "./auth.js"; /** Settings for the standard Topcoder OAuth client-credentials/Bus API endpoints. */ export interface BusConfig extends M2MConfig { @@ -20,48 +25,30 @@ export interface EventPublisher { /** Contains only a fixed operational code, never credentials or response bodies. */ export class BusError extends Error {} -/** Publishes authenticated Bus API events with bounded, abortable requests and cached M2M tokens. - * Uses the same client-credentials and auth0_url proxy contract as tc-core-library-js. - */ +/** Adapts the shared bus publisher to the processor's durable-delivery error contract. */ export class BusApiClient implements EventPublisher { - private readonly tokens: M2MTokenProvider; - /** Accepts validated settings and optional HTTP/clock dependencies; performs no network requests. */ + private readonly client: SharedBusClient; + + /** Creates the shared publisher from config and an optional test transport/clock; throws for invalid config. */ constructor( - private readonly config: BusConfig, - private readonly request: typeof fetch = fetch, + config: BusConfig, + request: typeof fetch = fetch, now: () => number = Date.now, ) { - this.tokens = new M2MTokenProvider(config, request, now); + void now; + this.client = createBusApiClient({ ...config, fetch: request, timeoutMs: 7000 }); } - /** Publish an envelope to /bus/events; resolves on HTTP acceptance or throws a sanitized BusError. - * Token acquisition and publication share a 15-second deadline, below the database lock timeout. - */ + /** Publishes an envelope and resolves on acceptance; throws sanitized BusError on token or bus failure. */ async postEvent(event: BusEvent): Promise { - const signal = AbortSignal.timeout(15_000); try { - const token = await this.tokens.get(signal); - const response = await this.request( - `${this.config.BUSAPI_URL}/bus/events`, - { - method: "POST", - redirect: "error", - signal, - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(event), - }, - ); - await response.body?.cancel().catch(() => {}); - if (response.status === 401) this.tokens.invalidate(); - if (!response.ok) throw new BusError(`bus_http_${response.status}`); + await this.client.postEvent(event); } catch (error) { - if (error instanceof BusError) throw error; - if (error instanceof AuthError) - throw new BusError(`bus_${error.message}`); - throw new BusError("bus_network_or_timeout"); + throw new BusError( + error instanceof HttpError && error.status + ? `bus_http_${error.status}` + : "bus_network_or_timeout", + ); } } } diff --git a/test/bus.test.ts b/test/bus.test.ts index e2a7c17..d358547 100644 --- a/test/bus.test.ts +++ b/test/bus.test.ts @@ -21,7 +21,6 @@ const event: BusEvent = { test("requests/caches M2M tokens and posts the envelope to the v6 Bus endpoint", async () => { let tokenRequests = 0; let posts = 0; - let now = 100000; const request: typeof fetch = async (url, init) => { assert.ok(init?.signal); assert.equal(init.redirect, "error"); @@ -32,7 +31,6 @@ test("requests/caches M2M tokens and posts the envelope to the v6 Bus endpoint", client_id: "client", client_secret: "secret", audience: options.AUTH0_AUDIENCE, - auth0_url: options.AUTH0_URL, }); return Response.json({ access_token: "token", expires_in: 120 }); } @@ -45,14 +43,13 @@ test("requests/caches M2M tokens and posts the envelope to the v6 Bus endpoint", assert.deepEqual(JSON.parse(String(init.body)), event); return new Response(null, { status: 202 }); }; - const client = new BusApiClient(options, request, () => now); + const client = new BusApiClient(options, request); await client.postEvent(event); await client.postEvent(event); assert.equal(tokenRequests, 1); assert.equal(posts, 2); - now += 61000; await client.postEvent(event); - assert.equal(tokenRequests, 2); + assert.equal(tokenRequests, 1); }); test("supports the standard Auth0 proxy endpoint and auth0_url field", async () => { const proxy = "https://proxy.example.com/token"; @@ -76,7 +73,7 @@ test("authentication failure prevents Bus publication and never exposes response calls++; return new Response("secret", { status: 403 }); }).postEvent(event), - /^Error: bus_auth_http_403$/, + /^Error: bus_http_403$/, ); assert.equal(calls, 1); }); @@ -85,7 +82,7 @@ test("invalid token responses do not publish", async () => { new BusApiClient(options, async () => Response.json({ error: "secret" }), ).postEvent(event), - /bus_auth_invalid_response/, + /bus_network_or_timeout/, ); }); test("401 clears cached token; publication failure remains retryable", async () => { diff --git a/tools/minimatch-compat/index.cjs b/tools/minimatch-compat/index.cjs new file mode 100644 index 0000000..c48079e --- /dev/null +++ b/tools/minimatch-compat/index.cjs @@ -0,0 +1,38 @@ +const modern = require('minimatch-engine') + +// Older build tools expect a callable CommonJS export. glob 7 additionally +// reads _glob and uses String.match on compiled segments. Delegate matching +// to minimatch 11's bounded matcher, without restoring the old regex engine. +function compatible (defaultOptions = {}) { + const options = (value) => ({ ...defaultOptions, ...value }) + const match = (path, pattern, value) => + modern.minimatch(path, pattern, options(value)) + Object.assign(match, modern) + match.Minimatch = class extends modern.Minimatch { + constructor (pattern, value) { + super(pattern, options(value)) + for (const row of this.set) { + for (const segment of row) { + if (typeof segment === 'object' && segment !== null) { + segment._glob = segment.pattern + segment[Symbol.match] = (input) => + segment.test(input) ? [input] : null + } + } + } + } + + static defaults (value) { + return compatible(options(value)).Minimatch + } + } + match.defaults = (value) => compatible(options(value)) + match.filter = (pattern, value) => modern.filter(pattern, options(value)) + match.match = (paths, pattern, value) => + modern.match(paths, pattern, options(value)) + match.braceExpand = (pattern, value) => + modern.braceExpand(pattern, options(value)) + return match +} + +module.exports = compatible() diff --git a/tools/minimatch-compat/index.test.cjs b/tools/minimatch-compat/index.test.cjs new file mode 100644 index 0000000..c1d71e8 --- /dev/null +++ b/tools/minimatch-compat/index.test.cjs @@ -0,0 +1,30 @@ +const assert = require('node:assert/strict'); +const { createRequire } = require('node:module'); +const { spawnSync } = require('node:child_process'); +const { test } = require('node:test'); + +const legacyRequire = createRequire(require.resolve('eslint')); +const match = legacyRequire('minimatch'); + +test('legacy consumers retain the callable minimatch API', () => { + assert.equal(typeof match, 'function'); + assert.equal(match('src/main.ts', '**/*.ts'), true); + assert.equal(match('src/main.js', '**/*.ts'), false); + assert.equal(match('.env', '*'), false); + assert.equal(match.defaults({ dot: true })('.env', '*'), true); + assert.deepEqual(match.match(['a.ts', 'b.js'], '*.ts'), ['a.ts']); + assert.deepEqual(['a.ts', 'b.js'].filter(match.filter('*.ts')), ['a.ts']); + assert.deepEqual(match.braceExpand('*.{js,ts}'), ['*.js', '*.ts']); + assert.equal(new match.Minimatch('**/*.ts').match('src/main.ts'), true); + assert.equal(match.Minimatch.defaults({ dot: true }) !== undefined, true); + assert.equal(typeof match.GLOBSTAR, 'symbol'); +}); + +test('amplifying glob patterns terminate within a bounded time', () => { + const result = spawnSync(process.execPath, ['-e', ` + const match = require(${JSON.stringify(legacyRequire.resolve('minimatch'))}); + new match.Minimatch('**/../'.repeat(30) + '*.ts', { optimizationLevel: 2 }); + `], { timeout: 5000, encoding: 'utf8' }); + assert.equal(result.error, undefined); + assert.equal(result.status, 0, result.stderr); +}); diff --git a/tools/minimatch-compat/package.json b/tools/minimatch-compat/package.json new file mode 100644 index 0000000..3f163b2 --- /dev/null +++ b/tools/minimatch-compat/package.json @@ -0,0 +1,10 @@ +{ + "name": "@topcoder/minimatch-compat", + "version": "1.0.0", + "private": true, + "description": "Legacy CommonJS entry point backed by patched minimatch", + "main": "index.cjs", + "dependencies": { + "minimatch-engine": "npm:minimatch@11.0.1" + } +}