diff --git a/.dockerignore b/.dockerignore index abb7a78..49a56f7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,3 +5,5 @@ esling.config.js README.md license.txt +dist +node_modules diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..579920e --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text eolf=lf diff --git a/.github/workflows/generate_ts_client.yml b/.github/workflows/generate_ts_client.yml new file mode 100644 index 0000000..8cda8d3 --- /dev/null +++ b/.github/workflows/generate_ts_client.yml @@ -0,0 +1,47 @@ +name: Generate TypeScript client for the frontend + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + generate_ts_client: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 25 + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Generate Prisma client + run: npx prisma generate + + - name: Generate Swagger JSON + run: npm run generate:swagger + + - name: Generate TypeScript client + run: npm run generate:client + + - name: Zip API client + run: | + mkdir -p artifacts + zip -r artifacts/api_ts_client_$(date +"%Y%m%d_%H%M%S").zip ./generated_client + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: api-client-artifacts + path: artifacts/api_ts_client_*.zip + diff --git a/.github/workflows/jest.yml b/.github/workflows/jest.yml new file mode 100644 index 0000000..7f36c16 --- /dev/null +++ b/.github/workflows/jest.yml @@ -0,0 +1,55 @@ +name: Run Jest Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: testdb + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + DATABASE_URL: postgres://postgres:postgres@localhost:5432/testdb + NODE_ENV: test + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm ci + + - name: Generate Prisma client + run: npx prisma generate + + - name: Run Prisma migrations + run: npx prisma migrate deploy + + - name: Build project + run: npm run build + + - name: Run Jest tests + run: npm run test -- --runInBand diff --git a/.gitignore b/.gitignore index bf7179e..2a4bb0e 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,4 @@ yarn.lock # Project specific config.json +config/webrtc.json diff --git a/.prettierrc b/.prettierrc index c2eb5d2..3db5dbc 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,6 @@ { "singleQuote": false, - "trailingComma": "none", - "endOfLine": "crlf" + "endOfLine": "lf", + "tabWidth": 2, + "trailingComma": "none" } diff --git a/Dockerfile b/Dockerfile index 65bc7f9..78789a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine +FROM node:20-alpine AS base ARG BACKEND_PORT=3000 ARG POSTGRES_HOST @@ -9,16 +9,26 @@ ARG POSTGRES_DB WORKDIR /app -RUN apk add --no-cache python3 make g++ +# If we need some dependencies that require native compilation (unlikely), +# decomment this out: +# RUN apk add --no-cache python3 make g++ ENV DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" COPY package.json ./ +COPY prisma.config.ts ./ COPY prisma ./prisma RUN npm install RUN npx prisma generate + +FROM base AS dev +ENV PORT=${BACKEND_PORT} COPY . . +CMD ["sh", "-c", "npx prisma migrate deploy && npm run start:dev"] +FROM base AS prod +COPY . . +RUN npm run build ENV PORT=${BACKEND_PORT} -CMD ["sh", "-c", "npx prisma migrate deploy && npm run start"] +CMD ["sh", "-c", "npx prisma migrate deploy && npm run start:prod"] diff --git a/dev.sh b/dev.sh new file mode 100755 index 0000000..4c1c4cb --- /dev/null +++ b/dev.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +docker compose -f docker-compose.yml -f docker-compose.dev.yml watch diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..748cbc1 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,33 @@ +networks: + naucto-dev: + name: naucto-dev + driver: bridge + +services: + db: + networks: + - naucto-dev + + backend: + build: + target: dev + networks: + - naucto-dev + develop: + watch: + - action: sync + path: ./src + target: /app/src + + - action: sync + path: ./prisma + target: /app/prisma + ignore: + - prisma/migrations + + - action: rebuild + path: package.json + + - action: rebuild + path: prisma/schema.prisma + diff --git a/docker-compose.yml b/docker-compose.yml index c5e0c71..6f7f2e7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,8 @@ +networks: + naucto: + name: naucto + driver: bridge + services: db: image: postgres @@ -13,14 +18,16 @@ services: interval: 10s timeout: 5s retries: 5 + networks: + - naucto backend: build: context: . dockerfile: Dockerfile + target: prod args: - BACKEND_PORT=${BACKEND_PORT} - - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_DB=${POSTGRES_DB} @@ -33,9 +40,10 @@ services: - .env ports: - "${BACKEND_PORT}:3000" - volumes: - - .:/app restart: on-failure + networks: + - naucto volumes: db-data: + diff --git a/jest.config.ts b/jest.config.ts index 2f6a70b..2a17816 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -13,7 +13,7 @@ const config: Config = { testRegex: ".*\\.spec\\.ts$", moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths || {}, { prefix: "/", - }), + }) || {}, transform: { "^.+\\.(t|j)s$": [ "ts-jest", diff --git a/openapi-ts.config.ts b/openapi-ts.config.ts new file mode 100644 index 0000000..58af843 --- /dev/null +++ b/openapi-ts.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from "@hey-api/openapi-ts"; + +export default defineConfig({ + input: "swagger.json", + output: { + path: "generated_client", + postProcess: ["prettier"], + }, + plugins: [ + { + name: "@hey-api/client-axios", + // Bundle the client runtime inside generated_client so the frontend + // can drop it in as-is without an extra npm dependency. + bundle: true, + }, + { + name: "@hey-api/sdk", + operations: { nesting: "operationId" }, + }, + { + name: "@hey-api/typescript", + // Emit runtime enum objects (e.g. ProjectResponseDtoStatus.COMPLETED) + // alongside the union types, giving callers a type-safe constant to + // reference instead of raw string literals. + enums: "javascript", + }, + ], +}); diff --git a/package-lock.json b/package-lock.json index 3e26063..327407d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,8 @@ "dependencies": { "@aws-sdk/client-s3": "^3.975.0", "@aws-sdk/cloudfront-signer": "^3.975.0", + "@aws-sdk/lib-storage": "^3.986.0", + "@aws-sdk/s3-request-presigner": "^3.986.0", "@nestjs/common": "^11.1.12", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.1.12", @@ -19,6 +21,7 @@ "@nestjs/platform-express": "^11.1.12", "@nestjs/schedule": "^6.1.0", "@nestjs/swagger": "^11.2.5", + "@prisma/adapter-pg": "^7.4.1", "@prisma/client": "^7.3.0", "bcryptjs": "^3.0.3", "class-transformer": "^0.5.1", @@ -33,12 +36,14 @@ "multer": "^2.0.2", "passport": "^0.7.0", "passport-jwt": "^4.0.1", + "pg": "^8.18.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2", "ws": "^8.19.0" }, "devDependencies": { "@eslint/js": "^9.39.2", + "@hey-api/openapi-ts": "latest", "@nestjs/cli": "^11.0.16", "@nestjs/testing": "^11.1.12", "@swc/cli": "^0.7.10", @@ -46,8 +51,10 @@ "@types/cookie-parser": "^1.4.10", "@types/express": "^5.0.6", "@types/jest": "^30.0.0", + "@types/multer": "^2.0.0", "@types/node": "^25.0.10", "@types/passport-jwt": "^4.0.1", + "@types/pg": "^8.16.0", "@types/supertest": "^6.0.3", "@types/ws": "^8", "depcheck": "^1.4.7", @@ -62,6 +69,7 @@ "supertest": "^7.2.2", "ts-jest": "^29.4.6", "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", "tsx": "^4.21.0", "typescript": "^5.9.3", "typescript-eslint": "^8.53.1" @@ -390,114 +398,65 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.975.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.975.0.tgz", - "integrity": "sha512-aF1M/iMD29BPcpxjqoym0YFa4WR9Xie1/IhVumwOGH6TB45DaqYO7vLwantDBcYNRn/cZH6DFHksO7RmwTFBhw==", + "version": "3.1004.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1004.0.tgz", + "integrity": "sha512-m0zNfpsona9jQdX1cHtHArOiuvSGZPsgp/KRZS2YjJhKah96G2UN3UNGZQ6aVjXIQjCY6UanCJo0uW9Xf2U41w==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.1", - "@aws-sdk/credential-provider-node": "^3.972.1", - "@aws-sdk/middleware-bucket-endpoint": "^3.972.1", - "@aws-sdk/middleware-expect-continue": "^3.972.1", - "@aws-sdk/middleware-flexible-checksums": "^3.972.1", - "@aws-sdk/middleware-host-header": "^3.972.1", - "@aws-sdk/middleware-location-constraint": "^3.972.1", - "@aws-sdk/middleware-logger": "^3.972.1", - "@aws-sdk/middleware-recursion-detection": "^3.972.1", - "@aws-sdk/middleware-sdk-s3": "^3.972.2", - "@aws-sdk/middleware-ssec": "^3.972.1", - "@aws-sdk/middleware-user-agent": "^3.972.2", - "@aws-sdk/region-config-resolver": "^3.972.1", - "@aws-sdk/signature-v4-multi-region": "3.972.0", - "@aws-sdk/types": "^3.973.0", - "@aws-sdk/util-endpoints": "3.972.0", - "@aws-sdk/util-user-agent-browser": "^3.972.1", - "@aws-sdk/util-user-agent-node": "^3.972.1", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.21.1", - "@smithy/eventstream-serde-browser": "^4.2.8", - "@smithy/eventstream-serde-config-resolver": "^4.3.8", - "@smithy/eventstream-serde-node": "^4.2.8", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-blob-browser": "^4.2.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/hash-stream-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/md5-js": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.11", - "@smithy/middleware-retry": "^4.4.27", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.10.12", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.26", - "@smithy/util-defaults-mode-node": "^4.2.29", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-stream": "^4.5.10", - "@smithy/util-utf8": "^4.2.0", - "@smithy/util-waiter": "^4.2.8", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.974.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.974.0.tgz", - "integrity": "sha512-ci+GiM0c4ULo4D79UMcY06LcOLcfvUfiyt8PzNY0vbt5O8BfCPYf4QomwVgkNcLLCYmroO4ge2Yy1EsLUlcD6g==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.0", - "@aws-sdk/middleware-host-header": "^3.972.1", - "@aws-sdk/middleware-logger": "^3.972.1", - "@aws-sdk/middleware-recursion-detection": "^3.972.1", - "@aws-sdk/middleware-user-agent": "^3.972.1", - "@aws-sdk/region-config-resolver": "^3.972.1", - "@aws-sdk/types": "^3.973.0", - "@aws-sdk/util-endpoints": "3.972.0", - "@aws-sdk/util-user-agent-browser": "^3.972.1", - "@aws-sdk/util-user-agent-node": "^3.972.1", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.21.0", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.10", - "@smithy/middleware-retry": "^4.4.26", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.10.11", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.25", - "@smithy/util-defaults-mode-node": "^4.2.28", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/credential-provider-node": "^3.972.18", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.7", + "@aws-sdk/middleware-expect-continue": "^3.972.7", + "@aws-sdk/middleware-flexible-checksums": "^3.973.4", + "@aws-sdk/middleware-host-header": "^3.972.7", + "@aws-sdk/middleware-location-constraint": "^3.972.7", + "@aws-sdk/middleware-logger": "^3.972.7", + "@aws-sdk/middleware-recursion-detection": "^3.972.7", + "@aws-sdk/middleware-sdk-s3": "^3.972.18", + "@aws-sdk/middleware-ssec": "^3.972.7", + "@aws-sdk/middleware-user-agent": "^3.972.19", + "@aws-sdk/region-config-resolver": "^3.972.7", + "@aws-sdk/signature-v4-multi-region": "^3.996.6", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-endpoints": "^3.996.4", + "@aws-sdk/util-user-agent-browser": "^3.972.7", + "@aws-sdk/util-user-agent-node": "^3.973.4", + "@smithy/config-resolver": "^4.4.10", + "@smithy/core": "^3.23.8", + "@smithy/eventstream-serde-browser": "^4.2.11", + "@smithy/eventstream-serde-config-resolver": "^4.3.11", + "@smithy/eventstream-serde-node": "^4.2.11", + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/hash-blob-browser": "^4.2.12", + "@smithy/hash-node": "^4.2.11", + "@smithy/hash-stream-node": "^4.2.11", + "@smithy/invalid-dependency": "^4.2.11", + "@smithy/md5-js": "^4.2.11", + "@smithy/middleware-content-length": "^4.2.11", + "@smithy/middleware-endpoint": "^4.4.22", + "@smithy/middleware-retry": "^4.4.39", + "@smithy/middleware-serde": "^4.2.12", + "@smithy/middleware-stack": "^4.2.11", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/protocol-http": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@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.38", + "@smithy/util-defaults-mode-node": "^4.2.41", + "@smithy/util-endpoints": "^3.3.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-retry": "^4.2.11", + "@smithy/util-stream": "^4.5.17", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.2.11", "tslib": "^2.6.2" }, "engines": { @@ -505,13 +464,13 @@ } }, "node_modules/@aws-sdk/cloudfront-signer": { - "version": "3.975.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/cloudfront-signer/-/cloudfront-signer-3.975.0.tgz", - "integrity": "sha512-hXZVREp1SmZfsFZectGDFHkz44tcG+LtgErpeBbtVSFLf+hp3D9eD3JnEyTMrR85iIpMX9mF1LeGFnSMenrwqw==", + "version": "3.1003.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/cloudfront-signer/-/cloudfront-signer-3.1003.0.tgz", + "integrity": "sha512-0LxfvSR9fgTgffgC4Tv1wXs/8H9tqZfz6zb8uFzLrmD1xKeHWH1Acq6duA1nasWB8DW6FMNRPndbPvDnJc7oEg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.21.1", - "@smithy/url-parser": "^4.2.8", + "@smithy/core": "^3.23.8", + "@smithy/url-parser": "^4.2.11", "tslib": "^2.6.2" }, "engines": { @@ -519,23 +478,23 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.1.tgz", - "integrity": "sha512-Ocubx42QsMyVs9ANSmFpRm0S+hubWljpPLjOi9UFrtcnVJjrVJTzQ51sN0e5g4e8i8QZ7uY73zosLmgYL7kZTQ==", + "version": "3.973.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.18.tgz", + "integrity": "sha512-GUIlegfcK2LO1J2Y98sCJy63rQSiLiDOgVw7HiHPRqfI2vb3XozTVqemwO0VSGXp54ngCnAQz0Lf0YPCBINNxA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.0", - "@aws-sdk/xml-builder": "^3.972.1", - "@smithy/core": "^3.21.1", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/signature-v4": "^5.3.8", - "@smithy/smithy-client": "^4.10.12", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/xml-builder": "^3.972.10", + "@smithy/core": "^3.23.8", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/signature-v4": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -543,12 +502,12 @@ } }, "node_modules/@aws-sdk/crc64-nvme": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.0.tgz", - "integrity": "sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw==", + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.4.tgz", + "integrity": "sha512-HKZIZLbRyvzo/bXZU7Zmk6XqU+1C9DjI56xd02vwuDIxedxBEqP17t9ExhbP9QFeNq/a3l9GOcyirFXxmbDhmw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -556,15 +515,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.1.tgz", - "integrity": "sha512-/etNHqnx96phy/SjI0HRC588o4vKH5F0xfkZ13yAATV7aNrb+5gYGNE6ePWafP+FuZ3HkULSSlJFj0AxgrAqYw==", + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.16.tgz", + "integrity": "sha512-HrdtnadvTGAQUr18sPzGlE5El3ICphnH6SU7UQOMOWFgRKbTRNN8msTxM4emzguUso9CzaHU2xy5ctSrmK5YNA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.0", - "@aws-sdk/types": "^3.973.0", - "@smithy/property-provider": "^4.2.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -572,20 +531,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.2", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.2.tgz", - "integrity": "sha512-mXgdaUfe5oM+tWKyeZ7Vh/iQ94FrkMky1uuzwTOmFADiRcSk5uHy/e3boEFedXiT/PRGzgBmqvJVK4F6lUISCg==", + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.18.tgz", + "integrity": "sha512-NyB6smuZAixND5jZumkpkunQ0voc4Mwgkd+SZ6cvAzIB7gK8HV8Zd4rS8Kn5MmoGgusyNfVGG+RLoYc4yFiw+A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.1", - "@aws-sdk/types": "^3.973.0", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.10.12", - "@smithy/types": "^4.12.0", - "@smithy/util-stream": "^4.5.10", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/property-provider": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" }, "engines": { @@ -593,24 +552,24 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.1.tgz", - "integrity": "sha512-OdbJA3v+XlNDsrYzNPRUwr8l7gw1r/nR8l4r96MDzSBDU8WEo8T6C06SvwaXR8SpzsjO3sq5KMP86wXWg7Rj4g==", + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.17.tgz", + "integrity": "sha512-dFqh7nfX43B8dO1aPQHOcjC0SnCJ83H3F+1LoCh3X1P7E7N09I+0/taID0asU6GCddfDExqnEvQtDdkuMe5tKQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.0", - "@aws-sdk/credential-provider-env": "^3.972.1", - "@aws-sdk/credential-provider-http": "^3.972.1", - "@aws-sdk/credential-provider-login": "^3.972.1", - "@aws-sdk/credential-provider-process": "^3.972.1", - "@aws-sdk/credential-provider-sso": "^3.972.1", - "@aws-sdk/credential-provider-web-identity": "^3.972.1", - "@aws-sdk/nested-clients": "3.974.0", - "@aws-sdk/types": "^3.973.0", - "@smithy/credential-provider-imds": "^4.2.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/credential-provider-env": "^3.972.16", + "@aws-sdk/credential-provider-http": "^3.972.18", + "@aws-sdk/credential-provider-login": "^3.972.17", + "@aws-sdk/credential-provider-process": "^3.972.16", + "@aws-sdk/credential-provider-sso": "^3.972.17", + "@aws-sdk/credential-provider-web-identity": "^3.972.17", + "@aws-sdk/nested-clients": "^3.996.7", + "@aws-sdk/types": "^3.973.5", + "@smithy/credential-provider-imds": "^4.2.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -618,18 +577,18 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.1.tgz", - "integrity": "sha512-CccqDGL6ZrF3/EFWZefvKW7QwwRdxlHUO8NVBKNVcNq6womrPDvqB6xc9icACtE0XB0a7PLoSTkAg8bQVkTO2w==", + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.17.tgz", + "integrity": "sha512-gf2E5b7LpKb+JX2oQsRIDxdRZjBFZt2olCGlWCdb3vBERbXIPgm2t1R5mEnwd4j0UEO/Tbg5zN2KJbHXttJqwA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.0", - "@aws-sdk/nested-clients": "3.974.0", - "@aws-sdk/types": "^3.973.0", - "@smithy/property-provider": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/nested-clients": "^3.996.7", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -637,22 +596,22 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.1.tgz", - "integrity": "sha512-DwXPk9GfuU/xG9tmCyXFVkCr6X3W8ZCoL5Ptb0pbltEx1/LCcg7T+PBqDlPiiinNCD6ilIoMJDWsnJ8ikzZA7Q==", + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.18.tgz", + "integrity": "sha512-ZDJa2gd1xiPg/nBDGhUlat02O8obaDEnICBAVS8qieZ0+nDfaB0Z3ec6gjZj27OqFTjnB/Q5a0GwQwb7rMVViw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.1", - "@aws-sdk/credential-provider-http": "^3.972.1", - "@aws-sdk/credential-provider-ini": "^3.972.1", - "@aws-sdk/credential-provider-process": "^3.972.1", - "@aws-sdk/credential-provider-sso": "^3.972.1", - "@aws-sdk/credential-provider-web-identity": "^3.972.1", - "@aws-sdk/types": "^3.973.0", - "@smithy/credential-provider-imds": "^4.2.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/credential-provider-env": "^3.972.16", + "@aws-sdk/credential-provider-http": "^3.972.18", + "@aws-sdk/credential-provider-ini": "^3.972.17", + "@aws-sdk/credential-provider-process": "^3.972.16", + "@aws-sdk/credential-provider-sso": "^3.972.17", + "@aws-sdk/credential-provider-web-identity": "^3.972.17", + "@aws-sdk/types": "^3.973.5", + "@smithy/credential-provider-imds": "^4.2.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -660,16 +619,16 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.1.tgz", - "integrity": "sha512-bi47Zigu3692SJwdBvo8y1dEwE6B61stCwCFnuRWJVTfiM84B+VTSCV661CSWJmIZzmcy7J5J3kWyxL02iHj0w==", + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.16.tgz", + "integrity": "sha512-n89ibATwnLEg0ZdZmUds5bq8AfBAdoYEDpqP3uzPLaRuGelsKlIvCYSNNvfgGLi8NaHPNNhs1HjJZYbqkW9b+g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.0", - "@aws-sdk/types": "^3.973.0", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -677,18 +636,18 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.1.tgz", - "integrity": "sha512-dLZVNhM7wSgVUFsgVYgI5hb5Z/9PUkT46pk/SHrSmUqfx6YDvoV4YcPtaiRqviPpEGGiRtdQMEadyOKIRqulUQ==", + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.17.tgz", + "integrity": "sha512-wGtte+48xnhnhHMl/MsxzacBPs5A+7JJedjiP452IkHY7vsbYKcvQBqFye8LwdTJVeHtBHv+JFeTscnwepoWGg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.974.0", - "@aws-sdk/core": "^3.973.0", - "@aws-sdk/token-providers": "3.974.0", - "@aws-sdk/types": "^3.973.0", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/nested-clients": "^3.996.7", + "@aws-sdk/token-providers": "3.1004.0", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -696,35 +655,56 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.1.tgz", - "integrity": "sha512-YMDeYgi0u687Ay0dAq/pFPKuijrlKTgsaB/UATbxCs/FzZfMiG4If5ksywHmmW7MiYUF8VVv+uou3TczvLrN4w==", + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.17.tgz", + "integrity": "sha512-8aiVJh6fTdl8gcyL+sVNcNwTtWpmoFa1Sh7xlj6Z7L/cZ/tYMEBHq44wTYG8Kt0z/PpGNopD89nbj3FHl9QmTA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/nested-clients": "^3.996.7", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/lib-storage": { + "version": "3.1004.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1004.0.tgz", + "integrity": "sha512-4W6UkeLVd/1FyXFvD9PHMw5FSOY7tsf6+I52jmgdZwDZ9gJcJBx6wF9IhaVp1AXhScZGY9HqHiqYt0qlrSHrGw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.0", - "@aws-sdk/nested-clients": "3.974.0", - "@aws-sdk/types": "^3.973.0", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@smithy/abort-controller": "^4.2.11", + "@smithy/middleware-endpoint": "^4.4.22", + "@smithy/smithy-client": "^4.12.2", + "buffer": "5.6.0", + "events": "3.3.0", + "stream-browserify": "3.0.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-s3": "^3.1004.0" } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.1.tgz", - "integrity": "sha512-YVvoitBdE8WOpHqIXvv49efT73F4bJ99XH2bi3Dn3mx7WngI4RwHwn/zF5i0q1Wdi5frGSCNF3vuh+pY817//w==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.7.tgz", + "integrity": "sha512-goX+axlJ6PQlRnzE2bQisZ8wVrlm6dXJfBzMJhd8LhAIBan/w1Kl73fJnalM/S+18VnpzIHumyV6DtgmvqG5IA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.0", - "@aws-sdk/util-arn-parser": "^3.972.1", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-config-provider": "^4.2.0", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -732,14 +712,14 @@ } }, "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.1.tgz", - "integrity": "sha512-6lfl2/J/kutzw/RLu1kjbahsz4vrGPysrdxWaw8fkjLYG+6M6AswocIAZFS/LgAVi/IWRwPTx9YC0/NH2wDrSw==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.7.tgz", + "integrity": "sha512-mvWqvm61bmZUKmmrtl2uWbokqpenY3Mc3Jf4nXB/Hse6gWxLPaCQThmhPBDzsPSV8/Odn8V6ovWt3pZ7vy4BFQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.0", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.5", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -747,24 +727,24 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.972.1.tgz", - "integrity": "sha512-kjVVREpqeUkYQsXr78AcsJbEUlxGH7+H6yS7zkjrnu6HyEVxbdSndkKX6VpKneFOihjCAhIXlk4wf3butDHkNQ==", + "version": "3.973.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.973.4.tgz", + "integrity": "sha512-7CH2jcGmkvkHc5Buz9IGbdjq1729AAlgYJiAvGq7qhCHqYleCsriWdSnmsqWTwdAfXHMT+pkxX3w6v5tJNcSug==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "^3.973.0", - "@aws-sdk/crc64-nvme": "3.972.0", - "@aws-sdk/types": "^3.973.0", - "@smithy/is-array-buffer": "^4.2.0", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-stream": "^4.5.10", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/crc64-nvme": "^3.972.4", + "@aws-sdk/types": "^3.973.5", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-stream": "^4.5.17", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -772,14 +752,14 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.1.tgz", - "integrity": "sha512-/R82lXLPmZ9JaUGSUdKtBp2k/5xQxvBT3zZWyKiBOhyulFotlfvdlrO8TnqstBimsl4lYEYySDL+W6ldFh6ALg==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.7.tgz", + "integrity": "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.0", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.5", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -787,13 +767,13 @@ } }, "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.1.tgz", - "integrity": "sha512-YisPaCbvBk9gY5aUI8jDMDKXsLZ9Fet0WYj1MviK8tZYMgxBIYHM6l3O/OHaAIujojZvamd9F3haYYYWp5/V3w==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.7.tgz", + "integrity": "sha512-vdK1LJfffBp87Lj0Bw3WdK1rJk9OLDYdQpqoKgmpIZPe+4+HawZ6THTbvjhJt4C4MNnRrHTKHQjkwBiIpDBoig==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.0", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -801,13 +781,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.1.tgz", - "integrity": "sha512-JGgFl6cHg9G2FHu4lyFIzmFN8KESBiRr84gLC3Aeni0Gt1nKm+KxWLBuha/RPcXxJygGXCcMM4AykkIwxor8RA==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.7.tgz", + "integrity": "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.0", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -815,15 +795,15 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.1.tgz", - "integrity": "sha512-taGzNRe8vPHjnliqXIHp9kBgIemLE/xCaRTMH1NH0cncHeaPcjxtnCroAAM9aOlPuKvBe2CpZESyvM1+D8oI7Q==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.7.tgz", + "integrity": "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.0", + "@aws-sdk/types": "^3.973.5", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -831,24 +811,24 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.2", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.2.tgz", - "integrity": "sha512-5f9x9/G+StE8+7wd9EVDF3d+J74xK+WBA3FhZwLSkf3pHFGLKzlmUfxJJE1kkXkbj/j/H+Dh3zL/hrtQE9hNsg==", + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.18.tgz", + "integrity": "sha512-5E3XxaElrdyk6ZJ0TjH7Qm6ios4b/qQCiLr6oQ8NK7e4Kn6JBTJCaYioQCQ65BpZ1+l1mK5wTAac2+pEz0Smpw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.1", - "@aws-sdk/types": "^3.973.0", - "@aws-sdk/util-arn-parser": "^3.972.1", - "@smithy/core": "^3.21.1", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/signature-v4": "^5.3.8", - "@smithy/smithy-client": "^4.10.12", - "@smithy/types": "^4.12.0", - "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-stream": "^4.5.10", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/core": "^3.23.8", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/signature-v4": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-stream": "^4.5.17", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -856,13 +836,13 @@ } }, "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.1.tgz", - "integrity": "sha512-fLtRTPd/MxJT2drJKft2GVGKm35PiNEeQ1Dvz1vc/WhhgAteYrp4f1SfSgjgLaYWGMExESJL4bt8Dxqp6tVsog==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.7.tgz", + "integrity": "sha512-G9clGVuAml7d8DYzY6DnRi7TIIDRvZ3YpqJPz/8wnWS5fYx/FNWNmkO6iJVlVkQg9BfeMzd+bVPtPJOvC4B+nQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.0", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.5", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -870,17 +850,18 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.2", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.2.tgz", - "integrity": "sha512-d+Exq074wy0X6wvShg/kmZVtkah+28vMuqCtuY3cydg8LUZOJBtbAolCpEJizSyb8mJJZF9BjWaTANXL4OYnkg==", + "version": "3.972.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.19.tgz", + "integrity": "sha512-Km90fcXt3W/iqujHzuM6IaDkYCj73gsYufcuWXApWdzoTy6KGk8fnchAjePMARU0xegIR3K4N3yIo1vy7OVe8A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.1", - "@aws-sdk/types": "^3.973.0", - "@aws-sdk/util-endpoints": "3.972.0", - "@smithy/core": "^3.21.1", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-endpoints": "^3.996.4", + "@smithy/core": "^3.23.8", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-retry": "^4.2.11", "tslib": "^2.6.2" }, "engines": { @@ -888,48 +869,48 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.974.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.974.0.tgz", - "integrity": "sha512-k3dwdo/vOiHMJc9gMnkPl1BA5aQfTrZbz+8fiDkWrPagqAioZgmo5oiaOaeX0grObfJQKDtcpPFR4iWf8cgl8Q==", + "version": "3.996.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.7.tgz", + "integrity": "sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.0", - "@aws-sdk/middleware-host-header": "^3.972.1", - "@aws-sdk/middleware-logger": "^3.972.1", - "@aws-sdk/middleware-recursion-detection": "^3.972.1", - "@aws-sdk/middleware-user-agent": "^3.972.1", - "@aws-sdk/region-config-resolver": "^3.972.1", - "@aws-sdk/types": "^3.973.0", - "@aws-sdk/util-endpoints": "3.972.0", - "@aws-sdk/util-user-agent-browser": "^3.972.1", - "@aws-sdk/util-user-agent-node": "^3.972.1", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.21.0", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.10", - "@smithy/middleware-retry": "^4.4.26", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.10.11", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.25", - "@smithy/util-defaults-mode-node": "^4.2.28", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/middleware-host-header": "^3.972.7", + "@aws-sdk/middleware-logger": "^3.972.7", + "@aws-sdk/middleware-recursion-detection": "^3.972.7", + "@aws-sdk/middleware-user-agent": "^3.972.19", + "@aws-sdk/region-config-resolver": "^3.972.7", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-endpoints": "^3.996.4", + "@aws-sdk/util-user-agent-browser": "^3.972.7", + "@aws-sdk/util-user-agent-node": "^3.973.4", + "@smithy/config-resolver": "^4.4.10", + "@smithy/core": "^3.23.8", + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/hash-node": "^4.2.11", + "@smithy/invalid-dependency": "^4.2.11", + "@smithy/middleware-content-length": "^4.2.11", + "@smithy/middleware-endpoint": "^4.4.22", + "@smithy/middleware-retry": "^4.4.39", + "@smithy/middleware-serde": "^4.2.12", + "@smithy/middleware-stack": "^4.2.11", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/protocol-http": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@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.38", + "@smithy/util-defaults-mode-node": "^4.2.41", + "@smithy/util-endpoints": "^3.3.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-retry": "^4.2.11", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -937,120 +918,51 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.1.tgz", - "integrity": "sha512-voIY8RORpxLAEgEkYaTFnkaIuRwVBEc+RjVZYcSSllPV+ZEKAacai6kNhJeE3D70Le+JCfvRb52tng/AVHY+jQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.0", - "@smithy/config-resolver": "^4.4.6", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.972.0.tgz", - "integrity": "sha512-2udiRijmjpN81Pvajje4TsjbXDZNP6K9bYUanBYH8hXa/tZG5qfGCySD+TyX0sgDxCQmEDMg3LaQdfjNHBDEgQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.972.0", - "@aws-sdk/types": "3.972.0", - "@smithy/protocol-http": "^5.3.8", - "@smithy/signature-v4": "^5.3.8", - "@smithy/types": "^4.12.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/signature-v4-multi-region/node_modules/@aws-sdk/core": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.972.0.tgz", - "integrity": "sha512-nEeUW2M9F+xdIaD98F5MBcQ4ITtykj3yKbgFZ6J0JtL3bq+Z90szQ6Yy8H/BLPYXTs3V4n9ifnBo8cprRDiE6A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.972.0", - "@aws-sdk/xml-builder": "3.972.0", - "@smithy/core": "^3.20.6", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/signature-v4": "^5.3.8", - "@smithy/smithy-client": "^4.10.8", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/signature-v4-multi-region/node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.0.tgz", - "integrity": "sha512-0bcKFXWx+NZ7tIlOo7KjQ+O2rydiHdIQahrq+fN6k9Osky29v17guy68urUKfhTobR6iY6KvxkroFWaFtTgS5w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.972.0", - "@aws-sdk/types": "3.972.0", - "@aws-sdk/util-arn-parser": "3.972.0", - "@smithy/core": "^3.20.6", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/signature-v4": "^5.3.8", - "@smithy/smithy-client": "^4.10.8", - "@smithy/types": "^4.12.0", - "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-stream": "^4.5.10", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/signature-v4-multi-region/node_modules/@aws-sdk/types": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.972.0.tgz", - "integrity": "sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.7.tgz", + "integrity": "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.5", + "@smithy/config-resolver": "^4.4.10", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/signature-v4-multi-region/node_modules/@aws-sdk/util-arn-parser": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.0.tgz", - "integrity": "sha512-RM5Mmo/KJ593iMSrALlHEOcc9YOIyOsDmS5x2NLOMdEmzv1o00fcpAkCQ02IGu1eFneBFT7uX0Mpag0HI+Cz2g==", + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.1004.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1004.0.tgz", + "integrity": "sha512-JlYj2tDr14czOkXtsK8x27xein8MgHtYzI1d2V3uswlh/ngZncrRhbWXnqW35DsRUSVj8cmvabKxilVitRJ0rw==", "license": "Apache-2.0", "dependencies": { + "@aws-sdk/signature-v4-multi-region": "^3.996.6", + "@aws-sdk/types": "^3.973.5", + "@aws-sdk/util-format-url": "^3.972.7", + "@smithy/middleware-endpoint": "^4.4.22", + "@smithy/protocol-http": "^5.3.11", + "@smithy/smithy-client": "^4.12.2", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/signature-v4-multi-region/node_modules/@aws-sdk/xml-builder": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.0.tgz", - "integrity": "sha512-POaGMcXnozzqBUyJM3HLUZ9GR6OKJWPGJEmhtTnxZXt8B6JcJ/6K3xRJ5H/j8oovVLz8Wg6vFxAHv8lvuASxMg==", + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.6.tgz", + "integrity": "sha512-NnsOQsVmJXy4+IdPFUjRCWPn9qNH1TzS/f7MiWgXeoHs903tJpAWQWQtoFvLccyPoBgomKP9L89RRr2YsT/L0g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", - "fast-xml-parser": "5.2.5", + "@aws-sdk/middleware-sdk-s3": "^3.972.18", + "@aws-sdk/types": "^3.973.5", + "@smithy/protocol-http": "^5.3.11", + "@smithy/signature-v4": "^5.3.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1058,17 +970,17 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.974.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.974.0.tgz", - "integrity": "sha512-cBykL0LiccKIgNhGWvQRTPvsBLPZxnmJU3pYxG538jpFX8lQtrCy1L7mmIHNEdxIdIGEPgAEHF8/JQxgBToqUQ==", + "version": "3.1004.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1004.0.tgz", + "integrity": "sha512-j9BwZZId9sFp+4GPhf6KrwO8Tben2sXibZA8D1vv2I1zBdvkUHcBA2g4pkqIpTRalMTLC0NPkBPX0gERxfy/iA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.0", - "@aws-sdk/nested-clients": "3.974.0", - "@aws-sdk/types": "^3.973.0", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.18", + "@aws-sdk/nested-clients": "^3.996.7", + "@aws-sdk/types": "^3.973.5", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1076,12 +988,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.0.tgz", - "integrity": "sha512-jYIdB7a7jhRTvyb378nsjyvJh1Si+zVduJ6urMNGpz8RjkmHZ+9vM2H07XaIB2Cfq0GhJRZYOfUCH8uqQhqBkQ==", + "version": "3.973.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz", + "integrity": "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1089,9 +1001,9 @@ } }, "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.1.tgz", - "integrity": "sha512-XnNit6H9PPHhqUXW/usjX6JeJ6Pm8ZNqivTjmNjgWHeOfVpblUc/MTic02UmCNR0jJLPjQ3mBKiMen0tnkNQjQ==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1101,28 +1013,30 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.972.0.tgz", - "integrity": "sha512-6JHsl1V/a1ZW8D8AFfd4R52fwZPnZ5H4U6DS8m/bWT8qad72NvbOFAC7U2cDtFs2TShqUO3TEiX/EJibtY3ijg==", + "version": "3.996.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.4.tgz", + "integrity": "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.972.0", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-endpoints": "^3.2.8", + "@aws-sdk/types": "^3.973.5", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@smithy/util-endpoints": "^3.3.2", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/util-endpoints/node_modules/@aws-sdk/types": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.972.0.tgz", - "integrity": "sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug==", + "node_modules/@aws-sdk/util-format-url": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.7.tgz", + "integrity": "sha512-V+PbnWfUl93GuFwsOHsAq7hY/fnm9kElRqR8IexIJr5Rvif9e614X5sGSyz3mVSf1YAZ+VTy63W1/pGdA55zyA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.5", + "@smithy/querystring-builder": "^4.2.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1130,9 +1044,9 @@ } }, "node_modules/@aws-sdk/util-locate-window": { - "version": "3.965.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.3.tgz", - "integrity": "sha512-FNUqAjlKAGA7GM05kywE99q8wiPHPZqrzhq3wXRga6PRD6A0kzT85Pb0AzYBVTBRpSrKyyr6M92Y6bnSBVp2BA==", + "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" @@ -1142,27 +1056,27 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.1.tgz", - "integrity": "sha512-IgF55NFmJX8d9Wql9M0nEpk2eYbuD8G4781FN4/fFgwTXBn86DvlZJuRWDCMcMqZymnBVX7HW9r+3r9ylqfW0w==", + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.7.tgz", + "integrity": "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.0", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.5", + "@smithy/types": "^4.13.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.1.tgz", - "integrity": "sha512-oIs4JFcADzoZ0c915R83XvK2HltWupxNsXUIuZse2rgk7b97zTpkxaqXiH0h9ylh31qtgo/t8hp4tIqcsMrEbQ==", + "version": "3.973.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.4.tgz", + "integrity": "sha512-uqKeLqZ9D3nQjH7HGIERNXK9qnSpUK08l4MlJ5/NZqSSdeJsVANYp437EM9sEzwU28c2xfj2V6qlkqzsgtKs6Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.1", - "@aws-sdk/types": "^3.973.0", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/middleware-user-agent": "^3.972.19", + "@aws-sdk/types": "^3.973.5", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -1178,13 +1092,13 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.1.tgz", - "integrity": "sha512-6zZGlPOqn7Xb+25MAXGb1JhgvaC5HjZj6GzszuVrnEgbhvzBRFGKYemuHBV4bho+dtqeYKPgaZUv7/e80hIGNg==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.10.tgz", + "integrity": "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", - "fast-xml-parser": "5.2.5", + "@smithy/types": "^4.13.0", + "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" }, "engines": { @@ -1201,9 +1115,9 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", - "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { @@ -1216,9 +1130,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", - "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, "license": "MIT", "engines": { @@ -1226,21 +1140,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", - "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.28.6", + "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -1267,14 +1181,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", - "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -1407,13 +1321,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1677,18 +1591,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", - "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.6", + "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -1696,9 +1610,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { @@ -1738,6 +1652,13 @@ "lodash": "4.17.21" } }, + "node_modules/@chevrotain/cst-dts-gen/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@chevrotain/gast": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", @@ -1749,6 +1670,13 @@ "lodash": "4.17.21" } }, + "node_modules/@chevrotain/gast/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@chevrotain/types": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", @@ -1863,9 +1791,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", "cpu": [ "ppc64" ], @@ -1880,9 +1808,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", "cpu": [ "arm" ], @@ -1897,9 +1825,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", "cpu": [ "arm64" ], @@ -1914,9 +1842,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", "cpu": [ "x64" ], @@ -1931,9 +1859,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ "arm64" ], @@ -1948,9 +1876,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", "cpu": [ "x64" ], @@ -1965,9 +1893,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], @@ -1982,9 +1910,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], @@ -1999,9 +1927,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", "cpu": [ "arm" ], @@ -2016,9 +1944,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", "cpu": [ "arm64" ], @@ -2033,9 +1961,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", "cpu": [ "ia32" ], @@ -2050,9 +1978,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", "cpu": [ "loong64" ], @@ -2067,9 +1995,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", "cpu": [ "mips64el" ], @@ -2084,9 +2012,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", "cpu": [ "ppc64" ], @@ -2101,9 +2029,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], @@ -2118,9 +2046,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], @@ -2135,9 +2063,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], @@ -2152,9 +2080,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", "cpu": [ "arm64" ], @@ -2169,9 +2097,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ "x64" ], @@ -2186,9 +2114,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", "cpu": [ "arm64" ], @@ -2203,9 +2131,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], @@ -2220,9 +2148,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], @@ -2237,9 +2165,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], @@ -2254,9 +2182,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], @@ -2271,9 +2199,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ "ia32" ], @@ -2288,9 +2216,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ "x64" ], @@ -2347,15 +2275,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2373,9 +2301,9 @@ } }, "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -2412,20 +2340,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -2436,9 +2364,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -2484,9 +2412,9 @@ "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -2497,9 +2425,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -2533,6 +2461,109 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hey-api/codegen-core": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.7.1.tgz", + "integrity": "sha512-X5qG+rr/BJvr+pEGcoW6l2azoZGrVuxsviEIhuf+3VwL9bk0atfubT65Xwo+4jDxXvjbhZvlwS0Ty3I7mLE2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hey-api/types": "0.1.3", + "ansi-colors": "4.1.3", + "c12": "3.3.3", + "color-support": "1.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + }, + "peerDependencies": { + "typescript": ">=5.5.3" + } + }, + "node_modules/@hey-api/json-schema-ref-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.3.1.tgz", + "integrity": "sha512-7atnpUkT8TyUPHYPLk91j/GyaqMuwTEHanLOe50Dlx0EEvNuQqFD52Yjg8x4KU0UFL1mWlyhE+sUE/wAtQ1N2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "7.1.3", + "@types/json-schema": "7.0.15", + "js-yaml": "4.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + } + }, + "node_modules/@hey-api/openapi-ts": { + "version": "0.94.0", + "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.94.0.tgz", + "integrity": "sha512-dbg3GG+v7sg9/Ahb7yFzwzQIJwm151JAtsnh9KtFyqiN0rGkMGA3/VqogEUq1kJB9XWrlMQwigwzhiEQ33VCSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hey-api/codegen-core": "0.7.1", + "@hey-api/json-schema-ref-parser": "1.3.1", + "@hey-api/shared": "0.2.2", + "@hey-api/types": "0.1.3", + "ansi-colors": "4.1.3", + "color-support": "1.1.3", + "commander": "14.0.3" + }, + "bin": { + "openapi-ts": "bin/run.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + }, + "peerDependencies": { + "typescript": ">=5.5.3" + } + }, + "node_modules/@hey-api/shared": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@hey-api/shared/-/shared-0.2.2.tgz", + "integrity": "sha512-vMqCS+j7F9xpWoXC7TBbqZkaelwrdeuSB+s/3elu54V5iq++S59xhkSq5rOgDIpI1trpE59zZQa6dpyUxItOgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hey-api/codegen-core": "0.7.1", + "@hey-api/json-schema-ref-parser": "1.3.1", + "@hey-api/types": "0.1.3", + "ansi-colors": "4.1.3", + "cross-spawn": "7.0.6", + "open": "11.0.0", + "semver": "7.7.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + }, + "peerDependencies": { + "typescript": ">=5.5.3" + } + }, + "node_modules/@hey-api/types": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@hey-api/types/-/types-0.1.3.tgz", + "integrity": "sha512-mZaiPOWH761yD4GjDQvtjS2ZYLu5o5pI1TVSvV/u7cmbybv51/FVtinFBeaE1kFQCKZ8OQpn2ezjLBJrKsGATw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5.5.3" + } + }, "node_modules/@hono/node-server": { "version": "1.19.9", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", @@ -2948,29 +2979,6 @@ } } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3036,12 +3044,12 @@ } }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -3094,6 +3102,16 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -3164,16 +3182,6 @@ "node": ">=8" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -3408,6 +3416,7 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -3632,6 +3641,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "dev": true, + "license": "MIT" + }, "node_modules/@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -4042,10 +4058,20 @@ } } }, + "node_modules/@nestjs/cli/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/@nestjs/common": { - "version": "11.1.12", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.12.tgz", - "integrity": "sha512-v6U3O01YohHO+IE3EIFXuRuu3VJILWzyMmSYZXpyBbnp0hk0mFyHxK2w3dF4I5WnbwiRbWlEXdeXFvPQ7qaZzw==", + "version": "11.1.16", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.16.tgz", + "integrity": "sha512-JSIeW+USuMJkkcNbiOdcPkVCeI3TSnXstIVEPpp3HiaKnPRuSbUUKm9TY9o/XpIcPHWUOQItAtC5BiAwFdVITQ==", "license": "MIT", "dependencies": { "file-type": "21.3.0", @@ -4074,14 +4100,14 @@ } }, "node_modules/@nestjs/config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.2.tgz", - "integrity": "sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.3.tgz", + "integrity": "sha512-FQ3M3Ohqfl+nHAn5tp7++wUQw0f2nAk+SFKe8EpNRnIifPqvfJP6JQxPKtFLMOHbyer4X646prFG4zSRYEssQQ==", "license": "MIT", "dependencies": { - "dotenv": "16.4.7", - "dotenv-expand": "12.0.1", - "lodash": "4.17.21" + "dotenv": "17.2.3", + "dotenv-expand": "12.0.3", + "lodash": "4.17.23" }, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", @@ -4089,9 +4115,9 @@ } }, "node_modules/@nestjs/config/node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -4101,9 +4127,9 @@ } }, "node_modules/@nestjs/core": { - "version": "11.1.12", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.12.tgz", - "integrity": "sha512-97DzTYMf5RtGAVvX1cjwpKRiCUpkeQ9CCzSAenqkAhOmNVVFaApbhuw+xrDt13rsCa2hHVOYPrV4dBgOYMJjsA==", + "version": "11.1.16", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.16.tgz", + "integrity": "sha512-tXWXyCiqWthelJjrE0KLFjf0O98VEt+WPVx5CrqCf+059kIxJ8y1Vw7Cy7N4fwQafWNrmFL2AfN87DDMbVAY0w==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -4185,14 +4211,14 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "11.1.12", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.12.tgz", - "integrity": "sha512-GYK/vHI0SGz5m8mxr7v3Urx8b9t78Cf/dj5aJMZlGd9/1D9OI1hAl00BaphjEXINUJ/BQLxIlF2zUjrYsd6enQ==", + "version": "11.1.16", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.16.tgz", + "integrity": "sha512-IOegr5+ZfUiMKgk+garsSU4MOkPRhm46e6w8Bp1GcO4vCdl9Piz6FlWAzKVfa/U3Hn/DdzSVJOW3TWcQQFdBDw==", "license": "MIT", "dependencies": { - "cors": "2.8.5", + "cors": "2.8.6", "express": "5.2.1", - "multer": "2.0.2", + "multer": "2.1.1", "path-to-regexp": "8.3.0", "tslib": "2.8.1" }, @@ -4206,12 +4232,12 @@ } }, "node_modules/@nestjs/schedule": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.1.0.tgz", - "integrity": "sha512-W25Ydc933Gzb1/oo7+bWzzDiOissE+h/dhIAPugA39b9MuIzBbLybuXpc1AjoQLczO3v0ldmxaffVl87W0uqoQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.1.1.tgz", + "integrity": "sha512-kQl1RRgi02GJ0uaUGCrXHCcwISsCsJDciCKe38ykJZgnAeeoeVWs8luWtBo4AqAAXm4nS5K8RlV0smHUJ4+2FA==", "license": "MIT", "dependencies": { - "cron": "4.3.5" + "cron": "4.4.0" }, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", @@ -4293,15 +4319,15 @@ } }, "node_modules/@nestjs/swagger": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.5.tgz", - "integrity": "sha512-wCykbEybMqiYcvkyzPW4SbXKcwra9AGdajm0MvFgKR3W+gd1hfeKlo67g/s9QCRc/mqUU4KOE5Qtk7asMeFuiA==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.6.tgz", + "integrity": "sha512-oiXOxMQqDFyv1AKAqFzSo6JPvMEs4uA36Eyz/s2aloZLxUjcLfUMELSLSNQunr61xCPTpwEOShfmO7NIufKXdA==", "license": "MIT", "dependencies": { "@microsoft/tsdoc": "0.16.0", "@nestjs/mapped-types": "2.1.0", "js-yaml": "4.1.1", - "lodash": "4.17.21", + "lodash": "4.17.23", "path-to-regexp": "8.3.0", "swagger-ui-dist": "5.31.0" }, @@ -4326,9 +4352,9 @@ } }, "node_modules/@nestjs/testing": { - "version": "11.1.12", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.12.tgz", - "integrity": "sha512-W0M/i5nb9qRQpTQfJm+1mGT/+y4YezwwdcD7mxFG8JEZ5fz/ZEAk1Ayri2VBJKJUdo20B1ggnvqew4dlTMrSNg==", + "version": "11.1.16", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.16.tgz", + "integrity": "sha512-E7/aUCxzeMSJV80L5GWGIuiMyR/1ncS7uOIetAImfbS4ATE1/h2GBafk0qpk+vjFtPIbtoh9BWDGICzUEU5jDA==", "dev": true, "license": "MIT", "dependencies": { @@ -4415,13 +4441,24 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@prisma/adapter-pg": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.4.2.tgz", + "integrity": "sha512-oUo2Zhe9Tf6YwVL8kLPuOLTK1Z2pwi/Ua77t2PuGyBan2w7shRKqHvYK+3XXmRH9RWhPJ4SMtHZKpNo6Ax/4bQ==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.4.2", + "pg": "^8.16.3", + "postgres-array": "3.0.4" + } + }, "node_modules/@prisma/client": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.3.0.tgz", - "integrity": "sha512-FXBIxirqQfdC6b6HnNgxGmU7ydCPEPk7maHMOduJJfnTP+MuOGa15X4omjR/zpPUUpm8ef/mEFQjJudOGkXFcQ==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.4.2.tgz", + "integrity": "sha512-ts2mu+cQHriAhSxngO3StcYubBGTWDtu/4juZhXCUKOwgh26l+s4KD3vT2kMUzFyrYnll9u/3qWrtzRv9CGWzA==", "license": "Apache-2.0", "dependencies": { - "@prisma/client-runtime-utils": "7.3.0" + "@prisma/client-runtime-utils": "7.4.2" }, "engines": { "node": "^20.19 || ^22.12 || >=24.0" @@ -4440,15 +4477,15 @@ } }, "node_modules/@prisma/client-runtime-utils": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.3.0.tgz", - "integrity": "sha512-dG/ceD9c+tnXATPk8G+USxxYM9E6UdMTnQeQ+1SZUDxTz7SgQcfxEqafqIQHcjdlcNK/pvmmLfSwAs3s2gYwUw==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.4.2.tgz", + "integrity": "sha512-cID+rzOEb38VyMsx5LwJMEY4NGIrWCNpKu/0ImbeooQ2Px7TI+kOt7cm0NelxUzF2V41UVVXAmYjANZQtCu1/Q==", "license": "Apache-2.0" }, "node_modules/@prisma/config": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.3.0.tgz", - "integrity": "sha512-QyMV67+eXF7uMtKxTEeQqNu/Be7iH+3iDZOQZW5ttfbSwBamCSdwPszA0dum+Wx27I7anYTPLmRmMORKViSW1A==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.4.2.tgz", + "integrity": "sha512-CftBjWxav99lzY1Z4oDgomdb1gh9BJFAOmWF6P2v1xRfXqQb56DfBub+QKcERRdNoAzCb3HXy3Zii8Vb4AsXhg==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -4458,11 +4495,59 @@ "empathic": "2.0.0" } }, - "node_modules/@prisma/debug": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.3.0.tgz", - "integrity": "sha512-yh/tHhraCzYkffsI1/3a7SHX8tpgbJu1NPnuxS4rEpJdWAUDHUH25F1EDo6PPzirpyLNkgPPZdhojQK804BGtg==", + "node_modules/@prisma/config/node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/@prisma/config/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@prisma/config/node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", "devOptional": true, + "license": "MIT" + }, + "node_modules/@prisma/debug": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.4.2.tgz", + "integrity": "sha512-aP7qzu+g/JnbF6U69LMwHoUkELiserKmWsE2shYuEpNUJ4GrtxBCvZwCyCBHFSH2kLTF2l1goBlBh4wuvRq62w==", "license": "Apache-2.0" }, "node_modules/@prisma/dev": { @@ -4491,57 +4576,66 @@ "zeptomatch": "2.1.0" } }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.4.2.tgz", + "integrity": "sha512-REdjFpT/ye9KdDs+CXAXPIbMQkVLhne9G5Pe97sNY4Ovx4r2DAbWM9hOFvvB1Oq8H8bOCdu0Ri3AoGALquQqVw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.4.2" + } + }, "node_modules/@prisma/engines": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.3.0.tgz", - "integrity": "sha512-cWRQoPDXPtR6stOWuWFZf9pHdQ/o8/QNWn0m0zByxf5Kd946Q875XdEJ52pEsX88vOiXUmjuPG3euw82mwQNMg==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.4.2.tgz", + "integrity": "sha512-B+ZZhI4rXlzjVqRw/93AothEKOU5/x4oVyJFGo9RpHPnBwaPwk4Pi0Q4iGXipKxeXPs/dqljgNBjK0m8nocOJA==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.3.0", - "@prisma/engines-version": "7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735", - "@prisma/fetch-engine": "7.3.0", - "@prisma/get-platform": "7.3.0" + "@prisma/debug": "7.4.2", + "@prisma/engines-version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", + "@prisma/fetch-engine": "7.4.2", + "@prisma/get-platform": "7.4.2" } }, "node_modules/@prisma/engines-version": { - "version": "7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735.tgz", - "integrity": "sha512-IH2va2ouUHihyiTTRW889LjKAl1CusZOvFfZxCDNpjSENt7g2ndFsK0vdIw/72v7+jCN6YgkHmdAP/BI7SDgyg==", + "version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919.tgz", + "integrity": "sha512-5FIKY3KoYQlBuZC2yc16EXfVRQ8HY+fLqgxkYfWCtKhRb3ajCRzP/rPeoSx11+NueJDANdh4hjY36mdmrTcGSg==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.3.0.tgz", - "integrity": "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.4.2.tgz", + "integrity": "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.3.0" + "@prisma/debug": "7.4.2" } }, "node_modules/@prisma/fetch-engine": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.3.0.tgz", - "integrity": "sha512-Mm0F84JMqM9Vxk70pzfNpGJ1lE4hYjOeLMu7nOOD1i83nvp8MSAcFYBnHqLvEZiA6onUR+m8iYogtOY4oPO5lQ==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.4.2.tgz", + "integrity": "sha512-f/c/MwYpdJO7taLETU8rahEstLeXfYgQGlz5fycG7Fbmva3iPdzGmjiSWHeSWIgNnlXnelUdCJqyZnFocurZuA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.3.0", - "@prisma/engines-version": "7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735", - "@prisma/get-platform": "7.3.0" + "@prisma/debug": "7.4.2", + "@prisma/engines-version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", + "@prisma/get-platform": "7.4.2" } }, "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.3.0.tgz", - "integrity": "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.4.2.tgz", + "integrity": "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.3.0" + "@prisma/debug": "7.4.2" } }, "node_modules/@prisma/get-platform": { @@ -4628,12 +4722,12 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.8.tgz", - "integrity": "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.11.tgz", + "integrity": "sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -4641,9 +4735,9 @@ } }, "node_modules/@smithy/chunked-blob-reader": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", - "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz", + "integrity": "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -4653,12 +4747,12 @@ } }, "node_modules/@smithy/chunked-blob-reader-native": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.1.tgz", - "integrity": "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz", + "integrity": "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-base64": "^4.3.0", + "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -4666,16 +4760,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.6.tgz", - "integrity": "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==", + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.10.tgz", + "integrity": "sha512-IRTkd6ps0ru+lTWnfnsbXzW80A8Od8p3pYiZnW98K2Hb20rqfsX7VTlfUwhrcOeSSy68Gn9WBofwPuw3e5CCsg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.3.2", + "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" }, "engines": { @@ -4683,20 +4777,20 @@ } }, "node_modules/@smithy/core": { - "version": "3.21.1", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.21.1.tgz", - "integrity": "sha512-NUH8R4O6FkN8HKMojzbGg/5pNjsfTjlMmeFclyPfPaXXUrbr5TzhWgbf7t92wfrpCHRgpjyz7ffASIS3wX28aA==", + "version": "3.23.9", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.9.tgz", + "integrity": "sha512-1Vcut4LEL9HZsdpI0vFiRYIsaoPwZLjAxnVQDUMQK8beMS+EYPLDQCXtbzfxmM5GzSgjfe2Q9M7WaXwIMQllyQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.9", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-stream": "^4.5.10", - "@smithy/util-utf8": "^4.2.0", - "@smithy/uuid": "^1.1.0", + "@smithy/middleware-serde": "^4.2.12", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-stream": "^4.5.17", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, "engines": { @@ -4704,15 +4798,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.8.tgz", - "integrity": "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.11.tgz", + "integrity": "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", "tslib": "^2.6.2" }, "engines": { @@ -4720,14 +4814,14 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.8.tgz", - "integrity": "sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.11.tgz", + "integrity": "sha512-Sf39Ml0iVX+ba/bgMPxaXWAAFmHqYLTmbjAPfLPLY8CrYkRDEqZdUsKC1OwVMCdJXfAt0v4j49GIJ8DoSYAe6w==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.12.0", - "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/types": "^4.13.0", + "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -4735,13 +4829,13 @@ } }, "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.8.tgz", - "integrity": "sha512-MTfQT/CRQz5g24ayXdjg53V0mhucZth4PESoA5IhvaWVDTOQLfo8qI9vzqHcPsdd2v6sqfTYqF5L/l+pea5Uyw==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.11.tgz", + "integrity": "sha512-3rEpo3G6f/nRS7fQDsZmxw/ius6rnlIpz4UX6FlALEzz8JoSxFmdBt0SZnthis+km7sQo6q5/3e+UJcuQivoXA==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/eventstream-serde-universal": "^4.2.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -4749,12 +4843,12 @@ } }, "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.8.tgz", - "integrity": "sha512-ah12+luBiDGzBruhu3efNy1IlbwSEdNiw8fOZksoKoWW1ZHvO/04MQsdnws/9Aj+5b0YXSSN2JXKy/ClIsW8MQ==", + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.11.tgz", + "integrity": "sha512-XeNIA8tcP/GDWnnKkO7qEm/bg0B/bP9lvIXZBXcGZwZ+VYM8h8k9wuDvUODtdQ2Wcp2RcBkPTCSMmaniVHrMlA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -4762,13 +4856,13 @@ } }, "node_modules/@smithy/eventstream-serde-node": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.8.tgz", - "integrity": "sha512-cYpCpp29z6EJHa5T9WL0KAlq3SOKUQkcgSoeRfRVwjGgSFl7Uh32eYGt7IDYCX20skiEdRffyDpvF2efEZPC0A==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.11.tgz", + "integrity": "sha512-fzbCh18rscBDTQSCrsp1fGcclLNF//nJyhjldsEl/5wCYmgpHblv5JSppQAyQI24lClsFT0wV06N1Porn0IsEw==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/eventstream-serde-universal": "^4.2.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -4776,13 +4870,13 @@ } }, "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.8.tgz", - "integrity": "sha512-iJ6YNJd0bntJYnX6s52NC4WFYcZeKrPUr1Kmmr5AwZcwCSzVpS7oavAmxMR7pMq7V+D1G4s9F5NJK0xwOsKAlQ==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.11.tgz", + "integrity": "sha512-MJ7HcI+jEkqoWT5vp+uoVaAjBrmxBtKhZTeynDRG/seEjJfqyg3SiqMMqyPnAMzmIfLaeJ/uiuSDP/l9AnMy/Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-codec": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/eventstream-codec": "^4.2.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -4790,15 +4884,15 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.9.tgz", - "integrity": "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==", + "version": "5.3.13", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.13.tgz", + "integrity": "sha512-U2Hcfl2s3XaYjikN9cT4mPu8ybDbImV3baXR0PkVlC0TTx808bRP3FaPGAzPtB8OByI+JqJ1kyS+7GEgae7+qQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.8", - "@smithy/querystring-builder": "^4.2.8", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", + "@smithy/protocol-http": "^5.3.11", + "@smithy/querystring-builder": "^4.2.11", + "@smithy/types": "^4.13.0", + "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -4806,14 +4900,14 @@ } }, "node_modules/@smithy/hash-blob-browser": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.9.tgz", - "integrity": "sha512-m80d/iicI7DlBDxyQP6Th7BW/ejDGiF0bgI754+tiwK0lgMkcaIBgvwwVc7OFbY4eUzpGtnig52MhPAEJ7iNYg==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.12.tgz", + "integrity": "sha512-1wQE33DsxkM/waftAhCH9VtJbUGyt1PJ9YRDpOu+q9FUi73LLFUZ2fD8A61g2mT1UY9k7b99+V1xZ41Rz4SHRQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/chunked-blob-reader": "^5.2.0", - "@smithy/chunked-blob-reader-native": "^4.2.1", - "@smithy/types": "^4.12.0", + "@smithy/chunked-blob-reader": "^5.2.2", + "@smithy/chunked-blob-reader-native": "^4.2.3", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -4821,14 +4915,14 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.8.tgz", - "integrity": "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.11.tgz", + "integrity": "sha512-T+p1pNynRkydpdL015ruIoyPSRw9e/SQOWmSAMmmprfswMrd5Ow5igOWNVlvyVFZlxXqGmyH3NQwfwy8r5Jx0A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/types": "^4.13.0", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -4836,13 +4930,13 @@ } }, "node_modules/@smithy/hash-stream-node": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.8.tgz", - "integrity": "sha512-v0FLTXgHrTeheYZFGhR+ehX5qUm4IQsjAiL9qehad2cyjMWcN2QG6/4mSwbSgEQzI7jwfoXj7z4fxZUx/Mhj2w==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.11.tgz", + "integrity": "sha512-hQsTjwPCRY8w9GK07w1RqJi3e+myh0UaOWBBhZ1UMSDgofH/Q1fEYzU1teaX6HkpX/eWDdm7tAGR0jBPlz9QEQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/types": "^4.13.0", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -4850,12 +4944,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.8.tgz", - "integrity": "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.11.tgz", + "integrity": "sha512-cGNMrgykRmddrNhYy1yBdrp5GwIgEkniS7k9O1VLB38yxQtlvrxpZtUVvo6T4cKpeZsriukBuuxfJcdZQc/f/g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -4863,9 +4957,9 @@ } }, "node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "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" @@ -4875,13 +4969,13 @@ } }, "node_modules/@smithy/md5-js": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.8.tgz", - "integrity": "sha512-oGMaLj4tVZzLi3itBa9TCswgMBr7k9b+qKYowQ6x1rTyTuO1IU2YHdHUa+891OsOH+wCsH7aTPRsTJO3RMQmjQ==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.11.tgz", + "integrity": "sha512-350X4kGIrty0Snx2OWv7rPM6p6vM7RzryvFs6B/56Cux3w3sChOb3bymo5oidXJlPcP9fIRxGUCk7GqpiSOtng==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/types": "^4.13.0", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -4889,13 +4983,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz", - "integrity": "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.11.tgz", + "integrity": "sha512-UvIfKYAKhCzr4p6jFevPlKhQwyQwlJ6IeKLDhmV1PlYfcW3RL4ROjNEDtSik4NYMi9kDkH7eSwyTP3vNJ/u/Dw==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -4903,18 +4997,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.11.tgz", - "integrity": "sha512-/WqsrycweGGfb9sSzME4CrsuayjJF6BueBmkKlcbeU5q18OhxRrvvKlmfw3tpDsK5ilx2XUJvoukwxHB0nHs/Q==", + "version": "4.4.23", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.23.tgz", + "integrity": "sha512-UEFIejZy54T1EJn2aWJ45voB7RP2T+IRzUqocIdM6GFFa5ClZncakYJfcYnoXt3UsQrZZ9ZRauGm77l9UCbBLw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.21.1", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-middleware": "^4.2.8", + "@smithy/core": "^3.23.9", + "@smithy/middleware-serde": "^4.2.12", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", + "@smithy/url-parser": "^4.2.11", + "@smithy/util-middleware": "^4.2.11", "tslib": "^2.6.2" }, "engines": { @@ -4922,19 +5016,19 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.27", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.27.tgz", - "integrity": "sha512-xFUYCGRVsfgiN5EjsJJSzih9+yjStgMTCLANPlf0LVQkPDYCe0hz97qbdTZosFOiYlGBlHYityGRxrQ/hxhfVQ==", + "version": "4.4.40", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.40.tgz", + "integrity": "sha512-YhEMakG1Ae57FajERdHNZ4ShOPIY7DsgV+ZoAxo/5BT0KIe+f6DDU2rtIymNNFIj22NJfeeI6LWIifrwM0f+rA==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/service-error-classification": "^4.2.8", - "@smithy/smithy-client": "^4.10.12", - "@smithy/types": "^4.12.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/uuid": "^1.1.0", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/service-error-classification": "^4.2.11", + "@smithy/smithy-client": "^4.12.3", + "@smithy/types": "^4.13.0", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-retry": "^4.2.11", + "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, "engines": { @@ -4942,13 +5036,13 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.9.tgz", - "integrity": "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.12.tgz", + "integrity": "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -4956,12 +5050,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.8.tgz", - "integrity": "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.11.tgz", + "integrity": "sha512-s+eenEPW6RgliDk2IhjD2hWOxIx1NKrOHxEwNUaUXxYBxIyCcDfNULZ2Mu15E3kwcJWBedTET/kEASPV1A1Akg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -4969,14 +5063,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.8.tgz", - "integrity": "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==", + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.11.tgz", + "integrity": "sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@smithy/property-provider": "^4.2.11", + "@smithy/shared-ini-file-loader": "^4.4.6", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -4984,15 +5078,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.8.tgz", - "integrity": "sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==", + "version": "4.4.14", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.14.tgz", + "integrity": "sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/querystring-builder": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/abort-controller": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/querystring-builder": "^4.2.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -5000,12 +5094,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.8.tgz", - "integrity": "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.11.tgz", + "integrity": "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -5013,12 +5107,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.8.tgz", - "integrity": "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==", + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.11.tgz", + "integrity": "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -5026,13 +5120,13 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.8.tgz", - "integrity": "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.11.tgz", + "integrity": "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", - "@smithy/util-uri-escape": "^4.2.0", + "@smithy/types": "^4.13.0", + "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -5040,12 +5134,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.8.tgz", - "integrity": "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.11.tgz", + "integrity": "sha512-nE3IRNjDltvGcoThD2abTozI1dkSy8aX+a2N1Rs55en5UsdyyIXgGEmevUL3okZFoJC77JgRGe99xYohhsjivQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -5053,24 +5147,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.8.tgz", - "integrity": "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.11.tgz", + "integrity": "sha512-HkMFJZJUhzU3HvND1+Yw/kYWXp4RPDLBWLcK1n+Vqw8xn4y2YiBhdww8IxhkQjP/QlZun5bwm3vcHc8AqIU3zw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0" + "@smithy/types": "^4.13.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.3.tgz", - "integrity": "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==", + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.6.tgz", + "integrity": "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -5078,18 +5172,18 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz", - "integrity": "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==", + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.11.tgz", + "integrity": "sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-uri-escape": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.11", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -5097,17 +5191,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.10.12", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.12.tgz", - "integrity": "sha512-VKO/HKoQ5OrSHW6AJUmEnUKeXI1/5LfCwO9cwyao7CmLvGnZeM1i36Lyful3LK1XU7HwTVieTqO1y2C/6t3qtA==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.3.tgz", + "integrity": "sha512-7k4UxjSpHmPN2AxVhvIazRSzFQjWnud3sOsXcFStzagww17j1cFQYqTSiQ8xuYK3vKLR1Ni8FzuT3VlKr3xCNw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.21.1", - "@smithy/middleware-endpoint": "^4.4.11", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-stream": "^4.5.10", + "@smithy/core": "^3.23.9", + "@smithy/middleware-endpoint": "^4.4.23", + "@smithy/middleware-stack": "^4.2.11", + "@smithy/protocol-http": "^5.3.11", + "@smithy/types": "^4.13.0", + "@smithy/util-stream": "^4.5.17", "tslib": "^2.6.2" }, "engines": { @@ -5115,9 +5209,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz", - "integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.0.tgz", + "integrity": "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -5127,13 +5221,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.8.tgz", - "integrity": "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.11.tgz", + "integrity": "sha512-oTAGGHo8ZYc5VZsBREzuf5lf2pAurJQsccMusVZ85wDkX66ojEc/XauiGjzCj50A61ObFTPe6d7Pyt6UBYaing==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/querystring-parser": "^4.2.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -5141,13 +5235,13 @@ } }, "node_modules/@smithy/util-base64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", - "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "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.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -5155,9 +5249,9 @@ } }, "node_modules/@smithy/util-body-length-browser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", - "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "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" @@ -5167,9 +5261,9 @@ } }, "node_modules/@smithy/util-body-length-node": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", - "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "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" @@ -5179,12 +5273,12 @@ } }, "node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "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.0", + "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -5192,9 +5286,9 @@ } }, "node_modules/@smithy/util-config-provider": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", - "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "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" @@ -5204,14 +5298,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.26", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.26.tgz", - "integrity": "sha512-vva0dzYUTgn7DdE0uaha10uEdAgmdLnNFowKFjpMm6p2R0XDk5FHPX3CBJLzWQkQXuEprsb0hGz9YwbicNWhjw==", + "version": "4.3.39", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.39.tgz", + "integrity": "sha512-ui7/Ho/+VHqS7Km2wBw4/Ab4RktoiSshgcgpJzC4keFPs6tLJS4IQwbeahxQS3E/w98uq6E1mirCH/id9xIXeQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.10.12", - "@smithy/types": "^4.12.0", + "@smithy/property-provider": "^4.2.11", + "@smithy/smithy-client": "^4.12.3", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -5219,17 +5313,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.29", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.29.tgz", - "integrity": "sha512-c6D7IUBsZt/aNnTBHMTf+OVh+h/JcxUUgfTcIJaWRe6zhOum1X+pNKSZtZ+7fbOn5I99XVFtmrnXKv8yHHErTQ==", + "version": "4.2.42", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.42.tgz", + "integrity": "sha512-QDA84CWNe8Akpj15ofLO+1N3Rfg8qa2K5uX0y6HnOp4AnRYRgWrKx/xzbYNbVF9ZsyJUYOfcoaN3y93wA/QJ2A==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.6", - "@smithy/credential-provider-imds": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.10.12", - "@smithy/types": "^4.12.0", + "@smithy/config-resolver": "^4.4.10", + "@smithy/credential-provider-imds": "^4.2.11", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/property-provider": "^4.2.11", + "@smithy/smithy-client": "^4.12.3", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -5237,13 +5331,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.8.tgz", - "integrity": "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.2.tgz", + "integrity": "sha512-+4HFLpE5u29AbFlTdlKIT7jfOzZ8PDYZKTb3e+AgLz986OYwqTourQ5H+jg79/66DB69Un1+qKecLnkZdAsYcA==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", + "@smithy/node-config-provider": "^4.3.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -5251,9 +5345,9 @@ } }, "node_modules/@smithy/util-hex-encoding": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", - "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "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" @@ -5263,12 +5357,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.8.tgz", - "integrity": "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.11.tgz", + "integrity": "sha512-r3dtF9F+TpSZUxpOVVtPfk09Rlo4lT6ORBqEvX3IBT6SkQAdDSVKR5GcfmZbtl7WKhKnmb3wbDTQ6ibR2XHClw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -5276,13 +5370,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.8.tgz", - "integrity": "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.11.tgz", + "integrity": "sha512-XSZULmL5x6aCTTii59wJqKsY1l3eMIAomRAccW7Tzh9r8s7T/7rdo03oektuH5jeYRlJMPcNP92EuRDvk9aXbw==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/service-error-classification": "^4.2.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -5290,18 +5384,18 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.10", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.10.tgz", - "integrity": "sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==", + "version": "4.5.17", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.17.tgz", + "integrity": "sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/node-http-handler": "^4.4.8", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/fetch-http-handler": "^5.3.13", + "@smithy/node-http-handler": "^4.4.14", + "@smithy/types": "^4.13.0", + "@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": { @@ -5309,9 +5403,9 @@ } }, "node_modules/@smithy/util-uri-escape": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", - "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "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" @@ -5321,12 +5415,12 @@ } }, "node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "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.0", + "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -5334,13 +5428,13 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.8.tgz", - "integrity": "sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.11.tgz", + "integrity": "sha512-x7Rh2azQPs3XxbvCzcttRErKKvLnbZfqRf/gOjw2pb+ZscX88e5UkRPCB67bVnsFHxayvMvmePfKTqsRb+is1A==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/abort-controller": "^4.2.11", + "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "engines": { @@ -5348,9 +5442,9 @@ } }, "node_modules/@smithy/uuid": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", - "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "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" @@ -5412,9 +5506,9 @@ } }, "node_modules/@swc/core": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.10.tgz", - "integrity": "sha512-udNofxftduMUEv7nqahl2nvodCiCDQ4Ge0ebzsEm6P8s0RC2tBM0Hqx0nNF5J/6t9uagFJyWIDjXy3IIWMHDJw==", + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.18.tgz", + "integrity": "sha512-z87aF9GphWp//fnkRsqvtY+inMVPgYW3zSlXH1kJFvRT5H/wiAn+G32qW5l3oEk63KSF1x3Ov0BfHCObAmT8RA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -5430,16 +5524,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.10", - "@swc/core-darwin-x64": "1.15.10", - "@swc/core-linux-arm-gnueabihf": "1.15.10", - "@swc/core-linux-arm64-gnu": "1.15.10", - "@swc/core-linux-arm64-musl": "1.15.10", - "@swc/core-linux-x64-gnu": "1.15.10", - "@swc/core-linux-x64-musl": "1.15.10", - "@swc/core-win32-arm64-msvc": "1.15.10", - "@swc/core-win32-ia32-msvc": "1.15.10", - "@swc/core-win32-x64-msvc": "1.15.10" + "@swc/core-darwin-arm64": "1.15.18", + "@swc/core-darwin-x64": "1.15.18", + "@swc/core-linux-arm-gnueabihf": "1.15.18", + "@swc/core-linux-arm64-gnu": "1.15.18", + "@swc/core-linux-arm64-musl": "1.15.18", + "@swc/core-linux-x64-gnu": "1.15.18", + "@swc/core-linux-x64-musl": "1.15.18", + "@swc/core-win32-arm64-msvc": "1.15.18", + "@swc/core-win32-ia32-msvc": "1.15.18", + "@swc/core-win32-x64-msvc": "1.15.18" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -5451,9 +5545,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.10.tgz", - "integrity": "sha512-U72pGqmJYbjrLhMndIemZ7u9Q9owcJczGxwtfJlz/WwMaGYAV/g4nkGiUVk/+QSX8sFCAjanovcU1IUsP2YulA==", + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.18.tgz", + "integrity": "sha512-+mIv7uBuSaywN3C9LNuWaX1jJJ3SKfiJuE6Lr3bd+/1Iv8oMU7oLBjYMluX1UrEPzwN2qCdY6Io0yVicABoCwQ==", "cpu": [ "arm64" ], @@ -5468,9 +5562,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.10.tgz", - "integrity": "sha512-NZpDXtwHH083L40xdyj1sY31MIwLgOxKfZEAGCI8xHXdHa+GWvEiVdGiu4qhkJctoHFzAEc7ZX3GN5phuJcPuQ==", + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.18.tgz", + "integrity": "sha512-wZle0eaQhnzxWX5V/2kEOI6Z9vl/lTFEC6V4EWcn+5pDjhemCpQv9e/TDJ0GIoiClX8EDWRvuZwh+Z3dhL1NAg==", "cpu": [ "x64" ], @@ -5485,9 +5579,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.10.tgz", - "integrity": "sha512-ioieF5iuRziUF1HkH1gg1r93e055dAdeBAPGAk40VjqpL5/igPJ/WxFHGvc6WMLhUubSJI4S0AiZAAhEAp1jDg==", + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.18.tgz", + "integrity": "sha512-ao61HGXVqrJFHAcPtF4/DegmwEkVCo4HApnotLU8ognfmU8x589z7+tcf3hU+qBiU1WOXV5fQX6W9Nzs6hjxDw==", "cpu": [ "arm" ], @@ -5502,9 +5596,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.10.tgz", - "integrity": "sha512-tD6BClOrxSsNus9cJL7Gxdv7z7Y2hlyvZd9l0NQz+YXzmTWqnfzLpg16ovEI7gknH2AgDBB5ywOsqu8hUgSeEQ==", + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.18.tgz", + "integrity": "sha512-3xnctOBLIq3kj8PxOCgPrGjBLP/kNOddr6f5gukYt/1IZxsITQaU9TDyjeX6jG+FiCIHjCuWuffsyQDL5Ew1bg==", "cpu": [ "arm64" ], @@ -5519,9 +5613,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.10.tgz", - "integrity": "sha512-4uAHO3nbfbrTcmO/9YcVweTQdx5fN3l7ewwl5AEK4yoC4wXmoBTEPHAVdKNe4r9+xrTgd4BgyPsy0409OjjlMw==", + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.18.tgz", + "integrity": "sha512-0a+Lix+FSSHBSBOA0XznCcHo5/1nA6oLLjcnocvzXeqtdjnPb+SvchItHI+lfeiuj1sClYPDvPMLSLyXFaiIKw==", "cpu": [ "arm64" ], @@ -5536,9 +5630,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.10.tgz", - "integrity": "sha512-W0h9ONNw1pVIA0cN7wtboOSTl4Jk3tHq+w2cMPQudu9/+3xoCxpFb9ZdehwCAk29IsvdWzGzY6P7dDVTyFwoqg==", + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.18.tgz", + "integrity": "sha512-wG9J8vReUlpaHz4KOD/5UE1AUgirimU4UFT9oZmupUDEofxJKYb1mTA/DrMj0s78bkBiNI+7Fo2EgPuvOJfuAA==", "cpu": [ "x64" ], @@ -5553,9 +5647,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.10.tgz", - "integrity": "sha512-XQNZlLZB62S8nAbw7pqoqwy91Ldy2RpaMRqdRN3T+tAg6Xg6FywXRKCsLh6IQOadr4p1+lGnqM/Wn35z5a/0Vw==", + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.18.tgz", + "integrity": "sha512-4nwbVvCphKzicwNWRmvD5iBaZj8JYsRGa4xOxJmOyHlMDpsvvJ2OR2cODlvWyGFH6BYL1MfIAK3qph3hp0Az6g==", "cpu": [ "x64" ], @@ -5570,9 +5664,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.10.tgz", - "integrity": "sha512-qnAGrRv5Nj/DATxAmCnJQRXXQqnJwR0trxLndhoHoxGci9MuguNIjWahS0gw8YZFjgTinbTxOwzatkoySihnmw==", + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.18.tgz", + "integrity": "sha512-zk0RYO+LjiBCat2RTMHzAWaMky0cra9loH4oRrLKLLNuL+jarxKLFDA8xTZWEkCPLjUTwlRN7d28eDLLMgtUcQ==", "cpu": [ "arm64" ], @@ -5587,9 +5681,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.10.tgz", - "integrity": "sha512-i4X/q8QSvzVlaRtv1xfnfl+hVKpCfiJ+9th484rh937fiEZKxZGf51C+uO0lfKDP1FfnT6C1yBYwHy7FLBVXFw==", + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.18.tgz", + "integrity": "sha512-yVuTrZ0RccD5+PEkpcLOBAuPbYBXS6rslENvIXfvJGXSdX5QGi1ehC4BjAMl5FkKLiam4kJECUI0l7Hq7T1vwg==", "cpu": [ "ia32" ], @@ -5604,9 +5698,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.10.tgz", - "integrity": "sha512-HvY8XUFuoTXn6lSccDLYFlXv1SU/PzYi4PyUqGT++WfTnbw/68N/7BdUZqglGRwiSqr0qhYt/EhmBpULj0J9rA==", + "version": "1.15.18", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.18.tgz", + "integrity": "sha512-7NRmE4hmUQNCbYU3Hn9Tz57mK9Qq4c97ZS+YlamlK6qG9Fb5g/BB3gPDe0iLlJkns/sYv2VWSkm8c3NmbEGjbg==", "cpu": [ "x64" ], @@ -5850,9 +5944,9 @@ } }, "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", "dev": true, "license": "MIT" }, @@ -5944,13 +6038,23 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, + "node_modules/@types/multer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.1.0.tgz", + "integrity": "sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { - "version": "25.0.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", - "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", + "version": "25.3.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", + "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/parse-json": { @@ -5992,10 +6096,22 @@ "@types/passport": "*" } }, + "node_modules/@types/pg": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.18.0.tgz", + "integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", "dev": true, "license": "MIT" }, @@ -6007,9 +6123,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz", - "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", "peer": true, @@ -6103,17 +6219,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz", - "integrity": "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.53.1", - "@typescript-eslint/type-utils": "8.53.1", - "@typescript-eslint/utils": "8.53.1", - "@typescript-eslint/visitor-keys": "8.53.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -6126,8 +6242,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.53.1", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -6142,16 +6258,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.1.tgz", - "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.53.1", - "@typescript-eslint/types": "8.53.1", - "@typescript-eslint/typescript-estree": "8.53.1", - "@typescript-eslint/visitor-keys": "8.53.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "engines": { @@ -6162,19 +6278,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.1.tgz", - "integrity": "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.53.1", - "@typescript-eslint/types": "^8.53.1", + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "engines": { @@ -6189,14 +6305,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.1.tgz", - "integrity": "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.53.1", - "@typescript-eslint/visitor-keys": "8.53.1" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6207,9 +6323,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.1.tgz", - "integrity": "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", "dev": true, "license": "MIT", "engines": { @@ -6224,15 +6340,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.1.tgz", - "integrity": "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.53.1", - "@typescript-eslint/typescript-estree": "8.53.1", - "@typescript-eslint/utils": "8.53.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -6244,14 +6360,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.1.tgz", - "integrity": "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, "license": "MIT", "engines": { @@ -6263,18 +6379,18 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.1.tgz", - "integrity": "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.53.1", - "@typescript-eslint/tsconfig-utils": "8.53.1", - "@typescript-eslint/types": "8.53.1", - "@typescript-eslint/visitor-keys": "8.53.1", + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.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" @@ -6290,17 +6406,56 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "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", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/utils": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.1.tgz", - "integrity": "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.53.1", - "@typescript-eslint/types": "8.53.1", - "@typescript-eslint/typescript-estree": "8.53.1" + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6310,19 +6465,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.1.tgz", - "integrity": "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.53.1", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6332,6 +6487,19 @@ "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/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -6609,42 +6777,42 @@ ] }, "node_modules/@vue/compiler-core": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", - "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz", + "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/shared": "3.5.27", - "entities": "^7.0.0", + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.29", + "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz", - "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz", + "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-core": "3.5.29", + "@vue/shared": "3.5.29" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz", - "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz", + "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/compiler-core": "3.5.27", - "@vue/compiler-dom": "3.5.27", - "@vue/compiler-ssr": "3.5.27", - "@vue/shared": "3.5.27", + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.29", + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", @@ -6662,20 +6830,20 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz", - "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz", + "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-dom": "3.5.29", + "@vue/shared": "3.5.29" } }, "node_modules/@vue/shared": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz", - "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz", + "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==", "dev": true, "license": "MIT" }, @@ -7253,9 +7421,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -7289,9 +7457,9 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", "dev": true, "license": "MIT", "dependencies": { @@ -7711,9 +7879,9 @@ } }, "node_modules/b4a": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", - "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", "dev": true, "license": "Apache-2.0", "peerDependencies": { @@ -7845,6 +8013,84 @@ } } }, + "node_modules/bare-fs": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz", + "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.1.tgz", + "integrity": "sha512-ebvMaS5BgZKmJlvuWh14dg9rbUI84QeV3WlWn6Ph6lFI8jJoh7ADtVTyD2c93euwbe+zgi0DVrl4YmqXeM9aIA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.0.tgz", + "integrity": "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.21.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -7866,13 +8112,16 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.18", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz", - "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bcryptjs": { @@ -7965,9 +8214,9 @@ } }, "node_modules/bowser": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", - "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", "license": "MIT" }, "node_modules/brace-expansion": { @@ -8053,7 +8302,6 @@ "version": "5.6.0", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", - "dev": true, "license": "MIT", "dependencies": { "base64-js": "^1.0.2", @@ -8082,6 +8330,22 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -8103,27 +8367,27 @@ } }, "node_modules/c12": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", - "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", - "devOptional": true, + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.3.tgz", + "integrity": "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==", + "dev": true, "license": "MIT", "dependencies": { - "chokidar": "^4.0.3", + "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", - "dotenv": "^16.6.1", - "exsolve": "^1.0.7", + "dotenv": "^17.2.3", + "exsolve": "^1.0.8", "giget": "^2.0.0", - "jiti": "^2.4.2", + "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", - "perfect-debounce": "^1.0.0", - "pkg-types": "^2.2.0", + "perfect-debounce": "^2.0.0", + "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { - "magicast": "^0.3.5" + "magicast": "*" }, "peerDependenciesMeta": { "magicast": { @@ -8131,17 +8395,34 @@ } } }, - "node_modules/c12/node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "devOptional": true, - "license": "BSD-2-Clause", + "node_modules/c12/node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, "engines": { - "node": ">=12" + "node": ">= 20.19.0" }, "funding": { - "url": "https://dotenvx.com" + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/c12/node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/cacheable-lookup": { @@ -8241,19 +8522,22 @@ } }, "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001766", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", - "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", "dev": true, "funding": [ { @@ -8320,6 +8604,13 @@ "regexp-to-ast": "0.5.0" } }, + "node_modules/chevrotain/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -8347,9 +8638,9 @@ } }, "node_modules/ci-info": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", - "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", "dev": true, "funding": [ { @@ -8386,14 +8677,14 @@ "license": "MIT" }, "node_modules/class-validator": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", - "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.4.tgz", + "integrity": "sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==", "license": "MIT", "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", - "validator": "^13.15.20" + "validator": "^13.15.22" } }, "node_modules/cli-cursor": { @@ -8449,18 +8740,15 @@ } }, "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==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", + "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" } }, "node_modules/cliui/node_modules/wrap-ansi": { @@ -8527,6 +8815,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -8541,13 +8839,13 @@ } }, "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=20" } }, "node_modules/comment-json": { @@ -8598,9 +8896,9 @@ } }, "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", "devOptional": true, "license": "MIT" }, @@ -8685,9 +8983,9 @@ "license": "MIT" }, "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "license": "MIT", "dependencies": { "object-assign": "^4", @@ -8695,33 +8993,27 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", "dev": true, "license": "MIT", "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=10" } }, "node_modules/create-require": { @@ -8732,9 +9024,9 @@ "license": "MIT" }, "node_modules/cron": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/cron/-/cron-4.3.5.tgz", - "integrity": "sha512-hKPP7fq1+OfyCqoePkKfVq7tNAdFwiQORr4lZUHwrf0tebC65fYEeWgOrXOL6prn1/fegGOdTfrM6e34PJfksg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/cron/-/cron-4.4.0.tgz", + "integrity": "sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==", "license": "MIT", "dependencies": { "@types/luxon": "~3.7.0", @@ -8742,6 +9034,10 @@ }, "engines": { "node": ">=18.x" + }, + "funding": { + "type": "ko-fi", + "url": "https://ko-fi.com/intcreator" } }, "node_modules/cross-spawn": { @@ -8895,9 +9191,9 @@ } }, "node_modules/dedent": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", - "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -8936,6 +9232,36 @@ "node": ">=16.0.0" } }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/defaults": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/defaults/-/defaults-2.0.2.tgz", @@ -8977,6 +9303,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", @@ -9070,48 +9409,6 @@ "sprintf-js": "~1.0.2" } }, - "node_modules/depcheck/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/depcheck/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/depcheck/node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/depcheck/node_modules/js-yaml": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", @@ -9127,13 +9424,13 @@ } }, "node_modules/depcheck/node_modules/minimatch": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", - "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.9.tgz", + "integrity": "sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=10" @@ -9149,101 +9446,23 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/depcheck/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/depcheck/node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/depcheck/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/depcheck/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" + "node": ">=8.6" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/depcheck/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "node_modules/depcheck/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "picomatch": "^2.2.1" }, "engines": { - "node": ">=10" - } - }, - "node_modules/depcheck/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" + "node": ">=8.10.0" } }, "node_modules/depd": { @@ -9324,9 +9543,9 @@ } }, "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -9336,9 +9555,9 @@ } }, "node_modules/dotenv-expand": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz", - "integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==", + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", + "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", "license": "BSD-2-Clause", "dependencies": { "dotenv": "^16.4.5" @@ -9409,9 +9628,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.278", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.278.tgz", - "integrity": "sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw==", + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", "dev": true, "license": "ISC" }, @@ -9454,14 +9673,14 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.4", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", - "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -9672,9 +9891,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -9685,32 +9904,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/escalade": { @@ -9743,25 +9962,25 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -9780,7 +9999,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -9847,9 +10066,9 @@ } }, "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -9859,6 +10078,30 @@ "node": "*" } }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/eslint-plugin-react/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -9900,9 +10143,9 @@ } }, "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -9935,9 +10178,9 @@ "license": "MIT" }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -10045,7 +10288,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.x" @@ -10329,10 +10571,22 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-builder": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz", + "integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/fast-xml-parser": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", + "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", "funding": [ { "type": "github", @@ -10341,7 +10595,8 @@ ], "license": "MIT", "dependencies": { - "strnum": "^2.1.0" + "fast-xml-builder": "^1.0.0", + "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" @@ -10563,9 +10818,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", + "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", "dev": true, "license": "ISC" }, @@ -10640,10 +10895,37 @@ "concat-map": "0.0.1" } }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -10990,9 +11272,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", "dev": true, "license": "MIT", "dependencies": { @@ -11058,17 +11340,40 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/glob/node_modules/balanced-match": { + "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", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/glob/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -11120,9 +11425,9 @@ } }, "node_modules/globals": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.1.0.tgz", - "integrity": "sha512-8HoIcWI5fCvG5NADj4bDav+er9B9JMj2vyL2pI8D0eismKyUvPLTSs+Ln3wqhwcp306i73iyVnEKx3F6T47TGw==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", "dev": true, "license": "MIT", "engines": { @@ -11150,17 +11455,16 @@ } }, "node_modules/google-auth-library": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", - "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.1.tgz", + "integrity": "sha512-5awwuLrzNol+pFDmKJd0dKtZ0fPLAtoA5p7YO4ODsDu6ONJUVqbYwvv8y2ZBO5MBNp9TJXigB19710kYpBPdtA==", "license": "Apache-2.0", "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.0.0", - "gcp-metadata": "^8.0.0", - "google-logging-utils": "^1.0.0", - "gtoken": "^8.0.0", + "gaxios": "7.1.3", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", "jws": "^4.0.0" }, "engines": { @@ -11229,23 +11533,11 @@ "license": "MIT" }, "node_modules/graphmatch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.0.tgz", - "integrity": "sha512-0E62MaTW5rPZVRLyIJZG/YejmdA/Xr1QydHEw3Vt+qOKkMIOE8WDLc9ZX2bmAjtJFZcId4lEdrdmASsEy7D1QA==", - "devOptional": true - }, - "node_modules/gtoken": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", - "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", - "license": "MIT", - "dependencies": { - "gaxios": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "devOptional": true, + "license": "MIT" }, "node_modules/handlebars": { "version": "4.7.8", @@ -11535,6 +11827,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -11766,6 +12068,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -11844,6 +12162,38 @@ "node": ">=0.10.0" } }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -12111,6 +12461,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -12354,6 +12720,58 @@ } } }, + "node_modules/jest-cli/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/jest-config": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", @@ -12410,6 +12828,7 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -12741,6 +13160,7 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -12851,19 +13271,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/jest-watcher": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", @@ -13154,9 +13561,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.12.35", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.35.tgz", - "integrity": "sha512-T/Cz6iLcsZdb5jDncDcUNhSAJ0VlSC9TnsqtBNdpkaAmy24/R1RhErtNWVWBrcUZKs9hSgaVsBkc7HxYnazIfw==", + "version": "1.12.38", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.38.tgz", + "integrity": "sha512-vwzxmasAy9hZigxtqTbFEwp8ZdZ975TiqVDwj5bKx5sR+zi5ucUQy9mbVTkKM9GzqdLdxux/hTw2nmN5J7POMA==", "license": "MIT" }, "node_modules/lilconfig": { @@ -13226,9 +13633,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash.includes": { @@ -13348,9 +13755,9 @@ } }, "node_modules/lru.min": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz", - "integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", "devOptional": true, "license": "MIT", "engines": { @@ -13564,12 +13971,12 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -13582,32 +13989,21 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -13615,21 +14011,22 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", - "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", "concat-stream": "^2.0.0", - "mkdirp": "^0.5.6", - "object-assign": "^4.1.1", - "type-is": "^1.6.18", - "xtend": "^4.0.2" + "type-is": "^1.6.18" }, "engines": { "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/multer/node_modules/media-typer": { @@ -13707,9 +14104,9 @@ } }, "node_modules/multimatch/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -13867,6 +14264,35 @@ "lodash": "^4.17.21" } }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/node-fetch": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", @@ -13900,9 +14326,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "dev": true, "license": "MIT" }, @@ -13930,9 +14356,9 @@ } }, "node_modules/npm-check-updates": { - "version": "19.3.1", - "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-19.3.1.tgz", - "integrity": "sha512-v92fHH8fmf9VVmQwwL5JWpX8GDEe8BDyrz4w3GF6D6JBUZKpQNcTfBBgxVkCcAPzVUjCHSZEXYmZAAKfLTsDBA==", + "version": "19.6.3", + "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-19.6.3.tgz", + "integrity": "sha512-VAt9Bp26eLaymZ0nZyh5n/by+YZIuegXlvWR0yv1zBqd984f8VnEnBbn+1lS3nN5LyEjn62BJ+yYgzNSpb6Gzg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -13958,9 +14384,9 @@ } }, "node_modules/nypm": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.4.tgz", - "integrity": "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -13976,9 +14402,9 @@ } }, "node_modules/nypm/node_modules/citty": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.0.tgz", - "integrity": "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", + "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", "devOptional": true, "license": "MIT" }, @@ -14132,6 +14558,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.0.tgz", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.4.0", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.1.0", + "wsl-utils": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -14374,9 +14821,9 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -14384,16 +14831,16 @@ "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -14440,12 +14887,110 @@ "license": "MIT" }, "node_modules/perfect-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", - "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", - "devOptional": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "dev": true, + "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/pg-types/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/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/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -14598,9 +15143,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "dev": true, "funding": [ { @@ -14640,6 +15185,58 @@ "url": "https://github.com/sponsors/porsager" } }, + "node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "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/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -14695,16 +15292,16 @@ } }, "node_modules/prisma": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.3.0.tgz", - "integrity": "sha512-ApYSOLHfMN8WftJA+vL6XwAPOh/aZ0BgUyyKPwUFgjARmG6EBI9LzDPf6SWULQMSAxydV9qn5gLj037nPNlg2w==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.4.2.tgz", + "integrity": "sha512-2bP8Ruww3Q95Z2eH4Yqh4KAENRsj/SxbdknIVBfd6DmjPwmpsC4OVFMLOeHt6tM3Amh8ebjvstrUz3V/hOe1dA==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/config": "7.3.0", + "@prisma/config": "7.4.2", "@prisma/dev": "0.20.0", - "@prisma/engines": "7.3.0", + "@prisma/engines": "7.4.2", "@prisma/studio-core": "0.13.1", "mysql2": "3.15.3", "postgres": "3.4.7" @@ -14807,9 +15404,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -14834,16 +15431,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -14880,9 +15467,9 @@ } }, "node_modules/react": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", - "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "devOptional": true, "license": "MIT", "peer": true, @@ -14891,9 +15478,9 @@ } }, "node_modules/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "devOptional": true, "license": "MIT", "peer": true, @@ -14901,7 +15488,7 @@ "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.3" + "react": "^19.2.4" } }, "node_modules/react-is": { @@ -15034,19 +15621,22 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -15071,16 +15661,6 @@ "node": ">=8" } }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/resolve-dir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", @@ -15096,13 +15676,13 @@ } }, "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/resolve-pkg-maps": { @@ -15181,6 +15761,7 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -15235,6 +15816,19 @@ "node": ">= 18" } }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -15353,9 +15947,9 @@ } }, "node_modules/schema-utils/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -15490,16 +16084,6 @@ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", "devOptional": true }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/serve-static": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", @@ -15756,6 +16340,15 @@ "node": ">=0.10.0" } }, + "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/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -15826,6 +16419,16 @@ "node": ">= 0.4" } }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -16066,9 +16669,9 @@ } }, "node_modules/strnum": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", - "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", "funding": [ { "type": "github", @@ -16215,17 +16818,28 @@ } }, "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", "dev": true, "license": "MIT", "dependencies": { "b4a": "^1.6.4", + "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, "node_modules/terser": { "version": "5.46.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", @@ -16246,16 +16860,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", + "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { @@ -16407,7 +17020,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -16426,9 +17039,9 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -16439,9 +17052,9 @@ } }, "node_modules/text-decoder": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", - "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -16881,16 +17494,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.1.tgz", - "integrity": "sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", + "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.53.1", - "@typescript-eslint/parser": "8.53.1", - "@typescript-eslint/typescript-estree": "8.53.1", - "@typescript-eslint/utils": "8.53.1" + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -16900,7 +17513,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -16973,9 +17586,9 @@ } }, "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==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, "node_modules/universalify": { @@ -17259,9 +17872,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "dev": true, "license": "MIT", "engines": { @@ -17548,6 +18161,23 @@ } } }, + "node_modules/wsl-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -17585,22 +18215,22 @@ } }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^4.2.0", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^20.2.2" }, "engines": { - "node": ">=12" + "node": ">=10" } }, "node_modules/yargs-parser": { @@ -17613,6 +18243,16 @@ "node": ">=12" } }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yauzl": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz", diff --git a/package.json b/package.json index dc92da5..98d1655 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,11 @@ "license": "GPLv3", "scripts": { "build": "nest build", + "generate:swagger": "ts-node -r tsconfig-paths/register tool/generate-swagger.ts", + "generate:client": "npx @hey-api/openapi-ts", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "npm run build && node --openssl-legacy-provider dist/main", - "start:dev": "NODE_OPTIONS='--openssl-legacy-provider' tsx watch src/main.ts", + "start:dev": "NODE_OPTIONS='--openssl-legacy-provider' node --watch -r ts-node/register -r tsconfig-paths/register src/main.ts", "start:debug": "nest start --debug --watch --exec \"node --openssl-legacy-provider\"", "start:prod": "node --openssl-legacy-provider dist/main", "clean": "find ./src -type f \\( -name '*.js' -o -name '*.js.map' -o -name '*.d.ts' -o -name '*.d.ts.map' \\) -delete", @@ -23,8 +25,8 @@ "dependencies": { "@aws-sdk/client-s3": "^3.975.0", "@aws-sdk/cloudfront-signer": "^3.975.0", - "@aws-sdk/lib-storage": "^3.985.0", - "@aws-sdk/s3-request-presigner": "^3.985.0", + "@aws-sdk/lib-storage": "^3.986.0", + "@aws-sdk/s3-request-presigner": "^3.986.0", "@nestjs/common": "^11.1.12", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.1.12", @@ -33,9 +35,8 @@ "@nestjs/platform-express": "^11.1.12", "@nestjs/schedule": "^6.1.0", "@nestjs/swagger": "^11.2.5", - "@prisma/adapter-pg": "^7.3.0", + "@prisma/adapter-pg": "^7.4.1", "@prisma/client": "^7.3.0", - "@types/multer": "^2.0.0", "bcryptjs": "^3.0.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.3", @@ -56,6 +57,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.2", + "@hey-api/openapi-ts": "latest", "@nestjs/cli": "^11.0.16", "@nestjs/testing": "^11.1.12", "@swc/cli": "^0.7.10", @@ -63,6 +65,7 @@ "@types/cookie-parser": "^1.4.10", "@types/express": "^5.0.6", "@types/jest": "^30.0.0", + "@types/multer": "^2.0.0", "@types/node": "^25.0.10", "@types/passport-jwt": "^4.0.1", "@types/pg": "^8.16.0", @@ -80,6 +83,7 @@ "supertest": "^7.2.2", "ts-jest": "^29.4.6", "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", "tsx": "^4.21.0", "typescript": "^5.9.3", "typescript-eslint": "^8.53.1" diff --git a/prisma.config.ts b/prisma.config.ts index 1b0addd..70d3fac 100644 --- a/prisma.config.ts +++ b/prisma.config.ts @@ -1,5 +1,5 @@ import path from "node:path"; -import { defineConfig, env } from "prisma/config"; +import { defineConfig } from "prisma/config"; import dotenv from "dotenv"; dotenv.config(); @@ -8,13 +8,9 @@ export default defineConfig({ migrations: { path: path.join("prisma", "migrations"), }, - views: { - path: path.join("db", "views"), - }, - typedSql: { - path: path.join("db", "queries"), - }, datasource: { - url: env("DATABASE_URL") + // ?? "" -> so that we can launch Prsima without necessarily setting up the DATABASE_URL connx. + url: process.env.DATABASE_URL ?? "" } }); + diff --git a/prisma/migrations/20260121202957_game_session_updates/migration.sql b/prisma/migrations/20260121202957_game_session_updates/migration.sql new file mode 100644 index 0000000..93fb68f --- /dev/null +++ b/prisma/migrations/20260121202957_game_session_updates/migration.sql @@ -0,0 +1,49 @@ +/* + Warnings: + + - You are about to drop the column `userId` on the `GameSession` table. All the data in the column will be lost. + - A unique constraint covering the columns `[sessionId]` on the table `GameSession` will be added. If there are existing duplicate values, this will fail. + - Added the required column `hostId` to the `GameSession` table without a default value. This is not possible if the table is not empty. + - The required column `sessionId` was added to the `GameSession` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required. + +*/ +-- CreateEnum +CREATE TYPE "GameSessionVisibility" AS ENUM ('PUBLIC', 'FRIENDS_ONLY', 'PRIVATE'); + +-- DropForeignKey +ALTER TABLE "GameSession" DROP CONSTRAINT "GameSession_userId_fkey"; + +-- DropIndex +DROP INDEX "GameSession_userId_idx"; + +-- AlterTable +ALTER TABLE "GameSession" DROP COLUMN "userId", +ADD COLUMN "hostId" INTEGER NOT NULL, +ADD COLUMN "sessionId" TEXT NOT NULL, +ADD COLUMN "visibility" "GameSessionVisibility" NOT NULL DEFAULT 'PRIVATE'; + +-- CreateTable +CREATE TABLE "_UserJoinedGameSessions" ( + "A" INTEGER NOT NULL, + "B" INTEGER NOT NULL, + + CONSTRAINT "_UserJoinedGameSessions_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateIndex +CREATE INDEX "_UserJoinedGameSessions_B_index" ON "_UserJoinedGameSessions"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "GameSession_sessionId_key" ON "GameSession"("sessionId"); + +-- CreateIndex +CREATE INDEX "GameSession_hostId_idx" ON "GameSession"("hostId"); + +-- AddForeignKey +ALTER TABLE "GameSession" ADD CONSTRAINT "GameSession_hostId_fkey" FOREIGN KEY ("hostId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_UserJoinedGameSessions" ADD CONSTRAINT "_UserJoinedGameSessions_A_fkey" FOREIGN KEY ("A") REFERENCES "GameSession"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_UserJoinedGameSessions" ADD CONSTRAINT "_UserJoinedGameSessions_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/models/project.prisma b/prisma/models/project.prisma index ebd0a27..aaa5e54 100644 --- a/prisma/models/project.prisma +++ b/prisma/models/project.prisma @@ -1,5 +1,3 @@ -// prisma/models/project.prisma - enum MonetizationType { NONE ADS @@ -23,19 +21,19 @@ model Project { price Float? createdAt DateTime @default(now()) userId Int - + contentKey String? @map("content_key") contentExtension String? @map("content_extension") contentUploadedAt DateTime? @map("content_uploaded_at") - + // Relations creator User @relation("ProjectCreator", fields: [userId], references: [id]) collaborators User[] @relation("ProjectCollaborators") comments Comment[] gameSessions GameSession[] workSession WorkSession? - - // Statistiques agrégées + + // Aggregate statistics uniquePlayers Int @default(0) activePlayers Int @default(0) likes Int @default(0) diff --git a/prisma/models/session.prisma b/prisma/models/session.prisma index e30cc20..8d654d0 100644 --- a/prisma/models/session.prisma +++ b/prisma/models/session.prisma @@ -1,15 +1,24 @@ -// prisma/models/session.prisma +enum GameSessionVisibility { + PUBLIC + FRIENDS_ONLY + PRIVATE +} model GameSession { id Int @id @default(autoincrement()) - user User @relation(fields: [userId], references: [id]) - userId Int + host User @relation("UserHostedGameSessions", fields: [hostId], references: [id]) + hostId Int project Project @relation(fields: [projectId], references: [id]) projectId Int startedAt DateTime @default(now()) endedAt DateTime? - @@index([userId]) + visibility GameSessionVisibility @default(PRIVATE) + otherUsers User[] @relation("UserJoinedGameSessions") + + sessionId String @unique @default(uuid()) + + @@index([hostId]) @@index([projectId]) } diff --git a/prisma/models/user.prisma b/prisma/models/user.prisma index 37c7eaa..8974acb 100644 --- a/prisma/models/user.prisma +++ b/prisma/models/user.prisma @@ -1,5 +1,3 @@ -// prisma/models/user.prisma - model User { id Int @id @default(autoincrement()) email String @unique @@ -10,20 +8,22 @@ model User { roles Role[] @relation("UserRoles") - // Relations friendsInitiated Friendship[] @relation("UserAFriends") friendsReceived Friendship[] @relation("UserBFriends") sentRequests FriendRequest[] @relation("SentFriendRequests") receivedRequests FriendRequest[] @relation("ReceivedFriendRequests") subscriptions Subscription[] comments Comment[] - gameSessions GameSession[] creator Project[] @relation("ProjectCreator") collaborators Project[] @relation("ProjectCollaborators") + // FIXME: After merge, refactor those fields to joinedWorkSessions/hostingWorkSessions workSession WorkSession[] @relation("WorkSessionUsers") hostingSession WorkSession[] @relation("WorkSessionHost") + joinedGameSessions GameSession[] @relation("UserJoinedGameSessions") + hostingGameSessions GameSession[] @relation("UserHostedGameSessions") + refreshTokens RefreshToken[] } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e4b6029..578558c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -5,7 +5,7 @@ datasource db { } generator client { - provider = "prisma-client-js" - output = "../node_modules/.prisma/client" - binaryTargets = ["native", "linux-musl-openssl-3.0.x"] + provider = "prisma-client-js" + output = "../node_modules/.prisma/client" + binaryTargets = ["native", "linux-musl-openssl-3.0.x"] } diff --git a/src/app.config.ts b/src/app.config.ts new file mode 100644 index 0000000..92a72b0 --- /dev/null +++ b/src/app.config.ts @@ -0,0 +1,18 @@ +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class AppConfig { + private _port?: number; + + setPort(port: number): void { + if (this._port) { + return; + } + + this._port = port; + } + + getPort(): number | undefined { + return this._port; + } +} diff --git a/src/app.module.ts b/src/app.module.ts index 60ccdf5..c07b3fb 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,12 +2,15 @@ import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { S3Module } from "@s3/s3.module"; import { UserModule } from "@user/user.module"; -import { ProjectModule } from "@projects/project.module"; -import { WorkSessionModule } from "./routes/work-session/work-session.module"; -import { PrismaModule } from "@prisma/prisma.module"; +import { ProjectModule } from "@project/project.module"; +import { WorkSessionModule } from "@work-session/work-session.module"; +import { PrismaModule } from "@ourPrisma/prisma.module"; import { AuthModule } from "@auth/auth.module"; import { ScheduleModule } from "@nestjs/schedule"; -import { TasksModule } from "./tasks/tasks.module"; +import { TasksModule } from "src/tasks/tasks.module"; +import { WebRTCModule } from "@webrtc/webrtc.module"; +import { MultiplayerModule } from "@multiplayer/multiplayer.module"; +import { AppConfig } from "src/app.config"; @Module({ imports: [ @@ -19,7 +22,13 @@ import { TasksModule } from "./tasks/tasks.module"; UserModule, ProjectModule, WorkSessionModule, - TasksModule - ] + TasksModule, + WebRTCModule, + MultiplayerModule + ], + providers: [ + AppConfig + ], + exports: [AppConfig] }) export class AppModule {} diff --git a/src/auth/auth.controller.spec.ts b/src/auth/auth.controller.spec.ts index 462e53e..12d0635 100644 --- a/src/auth/auth.controller.spec.ts +++ b/src/auth/auth.controller.spec.ts @@ -1,7 +1,11 @@ import { Test, TestingModule } from "@nestjs/testing"; import { AuthController } from "./auth.controller"; -import { PrismaModule } from "@prisma/prisma.module"; +import { PrismaService } from "@ourPrisma/prisma.service"; import { AuthService } from "./auth.service"; +import { ConfigService } from "@nestjs/config"; +import { UnauthorizedException } from "@nestjs/common"; + +import { Response, Request } from "express"; describe("AuthController", () => { let controller: AuthController; @@ -9,7 +13,6 @@ describe("AuthController", () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [PrismaModule], controllers: [AuthController], providers: [ { @@ -18,7 +21,15 @@ describe("AuthController", () => { login: jest.fn(), register: jest.fn() } - } + }, + { + provide: PrismaService, + useValue: { + $connect: jest.fn(), + $disconnect: jest.fn() + } + }, + ConfigService, ] }).compile(); @@ -32,16 +43,211 @@ describe("AuthController", () => { it("should call authService.login and return access_token", async () => { const loginDto = { email: "test@example.com", password: "password" }; - const expectedResult = { access_token: "token123" }; + const expectedResult = { + access_token: "token123", + refresh_token: "refresh123" + }; (authService.login as jest.Mock).mockResolvedValue(expectedResult); - const result = await controller.login(loginDto); + const mockRes: Partial = { cookie: jest.fn() }; + const result = await controller.login(loginDto, mockRes as Response); expect(authService.login).toHaveBeenCalledWith( loginDto.email, loginDto.password ); - expect(result).toEqual(expectedResult); + expect(mockRes.cookie).toHaveBeenCalledWith( + "refresh_token", + "refresh123", + expect.objectContaining({ + httpOnly: true, + sameSite: "lax", + maxAge: 7 * 24 * 60 * 60 * 1000 + }) + ); + expect(result).toEqual({ access_token: "token123" }); + }); + + it("should call authService.register and return access_token", async () => { + const createUserDto = { + email: "newuser@example.com", + username: "newuser", + password: "password123" + }; + const expectedResult = { + access_token: "token456", + refresh_token: "refresh456" + }; + + (authService.register as jest.Mock).mockResolvedValue(expectedResult); + + const mockRes: Partial = { cookie: jest.fn() }; + const result = await controller.register(createUserDto, mockRes as Response); + + expect(authService.register).toHaveBeenCalledWith(createUserDto); + expect(mockRes.cookie).toHaveBeenCalledWith( + "refresh_token", + "refresh456", + expect.objectContaining({ + httpOnly: true, + sameSite: "lax" + }) + ); + expect(result).toEqual({ access_token: "token456" }); + }); + + it("should call authService.loginWithGoogle and return access_token", async () => { + const googleToken = "google-oauth-token"; + const expectedResult = { + access_token: "google-token789", + refresh_token: "refresh789" + }; + + const authServiceWithGoogle = { + ...authService, + loginWithGoogle: jest.fn().mockResolvedValue(expectedResult) + }; + + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + providers: [ + { + provide: AuthService, + useValue: authServiceWithGoogle + }, + { + provide: PrismaService, + useValue: { + $connect: jest.fn(), + $disconnect: jest.fn() + } + }, + ConfigService + ] + }).compile(); + + const testController = module.get(AuthController); + const mockRes: Partial = { cookie: jest.fn() }; + const result = await testController.loginWithGoogle(googleToken, mockRes as Response); + + expect(authServiceWithGoogle.loginWithGoogle).toHaveBeenCalledWith(googleToken); + expect(mockRes.cookie).toHaveBeenCalled(); + expect(result).toEqual({ access_token: "google-token789" }); + }); + + it("should refresh access token using refresh_token cookie", async () => { + const refreshToken = "valid-refresh-token"; + const expectedResult = { + access_token: "new-access-token", + refresh_token: "new-refresh-token" + }; + + const authServiceWithRefresh = { + ...authService, + refreshToken: jest.fn().mockResolvedValue(expectedResult) + }; + + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + providers: [ + { + provide: AuthService, + useValue: authServiceWithRefresh + }, + { + provide: PrismaService, + useValue: { + $connect: jest.fn(), + $disconnect: jest.fn() + } + }, + ConfigService + ] + }).compile(); + + const testController = module.get(AuthController); + const mockReq = { + cookies: { refresh_token: refreshToken } + } as unknown as Request; + const mockRes: Partial = { cookie: jest.fn() }; + const result = await testController.refresh(mockReq, mockRes as Response); + + expect(authServiceWithRefresh.refreshToken).toHaveBeenCalledWith(refreshToken); + expect(mockRes.cookie).toHaveBeenCalledWith( + "refresh_token", + "new-refresh-token", + expect.any(Object) + ); + expect(result).toEqual({ access_token: "new-access-token" }); + }); + + it("should throw UnauthorizedException when refresh_token is missing", async () => { + const mockReq = { + cookies: {} + } as Request; + const mockRes: Partial = { cookie: jest.fn() }; + + await expect( + controller.refresh(mockReq, mockRes as Response) + ).rejects.toThrow(UnauthorizedException); + }); + + it("should logout and clear refresh_token cookie", async () => { + const refreshToken = "token-to-revoke"; + const authServiceWithRevoke = { + ...authService, + revokeRefreshToken: jest.fn().mockResolvedValue(undefined) + }; + + const module: TestingModule = await Test.createTestingModule({ + controllers: [AuthController], + providers: [ + { + provide: AuthService, + useValue: authServiceWithRevoke + }, + { + provide: PrismaService, + useValue: { + $connect: jest.fn(), + $disconnect: jest.fn() + } + }, + ConfigService + ] + }).compile(); + + const testController = module.get(AuthController); + const mockReq = { + cookies: { refresh_token: refreshToken } + } as unknown as Request; + const mockRes: Partial = { + clearCookie: jest.fn() + }; + const result = await testController.logout(mockReq, mockRes as Response); + + expect(authServiceWithRevoke.revokeRefreshToken).toHaveBeenCalledWith(refreshToken); + expect(mockRes.clearCookie).toHaveBeenCalledWith( + "refresh_token", + expect.objectContaining({ + httpOnly: true, + sameSite: "lax" + }) + ); + expect(result).toEqual({ success: true }); + }); + + it("should logout successfully even without refresh_token cookie", async () => { + const mockReq = { + cookies: {} + } as unknown as Request; + const mockRes: Partial = { + clearCookie: jest.fn() + }; + const result = await controller.logout(mockReq, mockRes as Response); + + expect(result).toEqual({ success: true }); + expect(mockRes.clearCookie).not.toHaveBeenCalled(); }); }); diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index a5b29e1..91a5dac 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -4,7 +4,8 @@ import { Body, Req, Res, - UnauthorizedException + UnauthorizedException, + Inject } from "@nestjs/common"; import { AuthService } from "./auth.service"; import { LoginDto } from "./dto/login.dto"; @@ -12,14 +13,18 @@ import { AuthResponseDto } from "./dto/auth-response.dto"; import { CreateUserDto } from "@user/dto/create-user.dto"; import { ApiOperation, ApiResponse, ApiTags, ApiBody } from "@nestjs/swagger"; import { Response, Request } from "express"; +import { ConfigService } from "@nestjs/config"; @ApiTags("auth") @Controller("auth") export class AuthController { - constructor(private readonly authService: AuthService) {} + constructor( + private readonly authService: AuthService, + @Inject(ConfigService) private readonly configService: ConfigService + ) {} private setRefreshCookie(res: Response, token: string): void { - const nodeEnv = process.env["NODE_ENV"] ?? "development"; + const nodeEnv = this.configService.get("NODE_ENV") ?? "development"; res.cookie("refresh_token", token, { httpOnly: true, secure: nodeEnv === "production", @@ -137,7 +142,7 @@ export class AuthController { const refresh_token = req.cookies["refresh_token"]; if (refresh_token) { await this.authService.revokeRefreshToken(refresh_token); - const nodeEnv = process.env["NODE_ENV"] ?? "development"; + const nodeEnv = this.configService.get("NODE_ENV") ?? "development"; res.clearCookie("refresh_token", { httpOnly: true, diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index f3a5b41..49a4f86 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -24,7 +24,7 @@ function parseExpiresIn(v?: string): number | DurationString { imports: [ ConfigModule, UserModule, - PassportModule.register({ defaultStrategy: "jwt" }), + PassportModule.register({}), JwtModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts index ddf4852..7d55f9a 100644 --- a/src/auth/auth.service.spec.ts +++ b/src/auth/auth.service.spec.ts @@ -5,6 +5,9 @@ import { JwtService } from "@nestjs/jwt"; import * as bcrypt from "bcryptjs"; import { ConflictException, UnauthorizedException } from "@nestjs/common"; import { Prisma, User } from "@prisma/client"; +import { GoogleAuthService } from "./google-auth.service"; +import { PrismaService } from "@ourPrisma/prisma.service"; +import { ConfigService } from "@nestjs/config"; jest.mock("bcryptjs", () => ({ compare: jest.fn() @@ -25,6 +28,23 @@ describe("AuthService", () => { sign: jest.fn() }; + const prismaService = { + $transaction: jest.fn((callback: any) => { + // Create a mock transaction client + const txClient = { + refreshToken: { + create: jest.fn().mockResolvedValue({ id: 1, token: "refresh_token123", userId: 1, expiresAt: new Date() }), + deleteMany: jest.fn().mockResolvedValue({ count: 0 }) + } + }; + return callback(txClient); + }), + refreshToken: { + create: jest.fn(), + deleteMany: jest.fn() + } + }; + beforeEach(async () => { jest.clearAllMocks(); (bcrypt.compare as jest.Mock).mockResolvedValue(true); @@ -33,7 +53,14 @@ describe("AuthService", () => { providers: [ AuthService, { provide: UserService, useValue: userService }, - { provide: JwtService, useValue: jwtService } + { provide: JwtService, useValue: jwtService }, + { provide: GoogleAuthService, useValue: {} }, + { provide: PrismaService, useValue: prismaService }, + { provide: ConfigService, useValue: { get: jest.fn((key: string) => { + if (key === "JWT_EXPIRES_IN") return "1h"; + if (key === "JWT_REFRESH_EXPIRES_IN") return "7d"; + return undefined; + }) } } ] }).compile(); @@ -104,11 +131,11 @@ describe("AuthService", () => { jwtService.sign.mockReturnValue("token123"); const result = await authService.login("test@example.com", "password"); - expect(result).toEqual({ access_token: "token123" }); + expect(result).toEqual({ access_token: "token123", refresh_token: "token123" }); expect(jwtService.sign).toHaveBeenCalledWith({ sub: mockUser.id, email: mockUser.email - }); + }, expect.any(Object)); }); }); @@ -222,7 +249,307 @@ describe("AuthService", () => { }); expect(userService.create).toHaveBeenCalled(); - expect(result).toEqual({ access_token: "token123" }); + expect(result).toEqual({ access_token: "token123", refresh_token: "token123" }); + }); + }); + + describe("loginWithGoogle", () => { + it("should create new user and return tokens for new Google user", async () => { + const googleUser = { + email: "google@example.com", + name: "Google User" + }; + + const googleAuthService = { + verifyGoogleToken: jest.fn().mockResolvedValue(googleUser) + }; + + userService.findByEmail.mockResolvedValue(undefined); + + const newUser = { + id: 5, + email: googleUser.email, + username: "Google_User", + nickname: null, + password: "", + createdAt: new Date() + }; + userService.create.mockResolvedValue(newUser); + jwtService.sign.mockReturnValue("google-token-abc"); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AuthService, + { provide: UserService, useValue: userService }, + { provide: JwtService, useValue: jwtService }, + { provide: GoogleAuthService, useValue: googleAuthService }, + { provide: PrismaService, useValue: prismaService }, + { provide: ConfigService, useValue: { get: jest.fn((key: string) => { + if (key === "JWT_EXPIRES_IN") return "1h"; + if (key === "JWT_REFRESH_EXPIRES_IN") return "7d"; + return undefined; + }) } } + ] + }).compile(); + + const testAuthService = module.get(AuthService); + + const result = await testAuthService.loginWithGoogle("google-oauth-token"); + + expect(googleAuthService.verifyGoogleToken).toHaveBeenCalledWith("google-oauth-token"); + expect(userService.create).toHaveBeenCalledWith({ + email: googleUser.email, + username: "Google_User", + password: "", + roles: [] + }); + expect(result).toEqual({ + access_token: "google-token-abc", + refresh_token: "google-token-abc" + }); + }); + + it("should return tokens for existing Google user", async () => { + const googleUser = { + email: "existing@example.com", + name: "Existing User" + }; + + const existingUser = { + id: 6, + email: googleUser.email, + username: "existing_user", + nickname: null, + password: "somepass", + createdAt: new Date() + }; + + const googleAuthService = { + verifyGoogleToken: jest.fn().mockResolvedValue(googleUser) + }; + + userService.findByEmail.mockResolvedValue(existingUser); + jwtService.sign.mockReturnValue("existing-token-xyz"); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AuthService, + { provide: UserService, useValue: userService }, + { provide: JwtService, useValue: jwtService }, + { provide: GoogleAuthService, useValue: googleAuthService }, + { provide: PrismaService, useValue: prismaService }, + { provide: ConfigService, useValue: { get: jest.fn((key: string) => { + if (key === "JWT_EXPIRES_IN") return "1h"; + if (key === "JWT_REFRESH_EXPIRES_IN") return "7d"; + return undefined; + }) } } + ] + }).compile(); + + const testAuthService = module.get(AuthService); + + const result = await testAuthService.loginWithGoogle("google-oauth-token"); + + expect(googleAuthService.verifyGoogleToken).toHaveBeenCalledWith("google-oauth-token"); + expect(userService.create).not.toHaveBeenCalled(); + expect(result).toEqual({ + access_token: "existing-token-xyz", + refresh_token: "existing-token-xyz" + }); + }); + }); + + describe("refreshToken", () => { + it("should throw UnauthorizedException if refresh token not found", async () => { + const mockPrisma = { + ...prismaService, + refreshToken: { + findUnique: jest.fn().mockResolvedValue(null), + create: jest.fn(), + deleteMany: jest.fn(), + delete: jest.fn() + } + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AuthService, + { provide: UserService, useValue: userService }, + { provide: JwtService, useValue: jwtService }, + { provide: GoogleAuthService, useValue: {} }, + { provide: PrismaService, useValue: mockPrisma }, + { provide: ConfigService, useValue: { get: jest.fn((key: string) => { + if (key === "JWT_EXPIRES_IN") return "1h"; + if (key === "JWT_REFRESH_EXPIRES_IN") return "7d"; + return undefined; + }) } } + ] + }).compile(); + + const testAuthService = module.get(AuthService); + + await expect( + testAuthService.refreshToken("invalid-token") + ).rejects.toThrow(UnauthorizedException); + }); + + it("should throw UnauthorizedException if refresh token expired", async () => { + const expiredDate = new Date(Date.now() - 1000 * 60 * 60); // 1 hour ago + const mockPrisma = { + ...prismaService, + refreshToken: { + findUnique: jest.fn().mockResolvedValue({ + id: 1, + token: "expired-token", + userId: 1, + expiresAt: expiredDate, + user: { + id: 1, + email: "user@example.com", + username: "user", + nickname: null, + password: "pass", + createdAt: new Date() + } + }), + delete: jest.fn(), + create: jest.fn(), + deleteMany: jest.fn() + } + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AuthService, + { provide: UserService, useValue: userService }, + { provide: JwtService, useValue: jwtService }, + { provide: GoogleAuthService, useValue: {} }, + { provide: PrismaService, useValue: mockPrisma }, + { provide: ConfigService, useValue: { get: jest.fn((key: string) => { + if (key === "JWT_EXPIRES_IN") return "1h"; + if (key === "JWT_REFRESH_EXPIRES_IN") return "7d"; + return undefined; + }) } } + ] + }).compile(); + + const testAuthService = module.get(AuthService); + + await expect( + testAuthService.refreshToken("expired-token") + ).rejects.toThrow(UnauthorizedException); + + expect(mockPrisma.refreshToken.delete).toHaveBeenCalledWith({ + where: { id: 1 } + }); + }); + + it("should return new tokens for valid refresh token", async () => { + const futureDate = new Date(Date.now() + 1000 * 60 * 60 * 24 * 7); // 7 days from now + const mockPrisma = { + $transaction: jest.fn((callback: any) => { + const txClient = { + refreshToken: { + delete: jest.fn().mockResolvedValue({ id: 1 }), + create: jest.fn().mockResolvedValue({ id: 2, token: "new-refresh", userId: 1, expiresAt: futureDate }) + } + }; + return callback(txClient); + }), + refreshToken: { + findUnique: jest.fn().mockResolvedValue({ + id: 1, + token: "valid-token", + userId: 1, + expiresAt: futureDate, + user: { + id: 1, + email: "user@example.com", + username: "user", + nickname: null, + password: "pass", + createdAt: new Date() + } + }), + create: jest.fn(), + deleteMany: jest.fn() + } + }; + + jwtService.sign.mockReturnValue("new-access-token"); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AuthService, + { provide: UserService, useValue: userService }, + { provide: JwtService, useValue: jwtService }, + { provide: GoogleAuthService, useValue: {} }, + { provide: PrismaService, useValue: mockPrisma }, + { provide: ConfigService, useValue: { get: jest.fn((key: string) => { + if (key === "JWT_EXPIRES_IN") return "1h"; + if (key === "JWT_REFRESH_EXPIRES_IN") return "7d"; + return undefined; + }) } } + ] + }).compile(); + + const testAuthService = module.get(AuthService); + + const result = await testAuthService.refreshToken("valid-token"); + + expect(result).toEqual({ + access_token: "new-access-token", + refresh_token: "new-access-token" + }); + }); + }); + + describe("revokeRefreshToken", () => { + it("should delete refresh token", async () => { + const mockPrisma = { + ...prismaService, + refreshToken: { + deleteMany: jest.fn().mockResolvedValue({ count: 1 }), + create: jest.fn() + } + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AuthService, + { provide: UserService, useValue: userService }, + { provide: JwtService, useValue: jwtService }, + { provide: GoogleAuthService, useValue: {} }, + { provide: PrismaService, useValue: mockPrisma }, + { provide: ConfigService, useValue: { get: jest.fn() } } + ] + }).compile(); + + const testAuthService = module.get(AuthService); + + await testAuthService.revokeRefreshToken("token-to-revoke"); + + expect(mockPrisma.refreshToken.deleteMany).toHaveBeenCalledWith({ + where: { token: "token-to-revoke" } + }); + }); + }); + + describe("validateUser edge cases", () => { + it("should throw UnauthorizedException if user has no password", async () => { + const mockUser = { + id: 1, + email: "google@example.com", + username: "googleuser", + nickname: null, + password: null, + createdAt: new Date() + }; + userService.findByEmail.mockResolvedValue(mockUser as any); + + await expect( + authService.validateUser("google@example.com", "password") + ).rejects.toThrow(UnauthorizedException); }); }); }); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 39c1327..6e04835 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,7 +1,8 @@ import { Injectable, ConflictException, - UnauthorizedException + UnauthorizedException, + Inject } from "@nestjs/common"; import { JwtService } from "@nestjs/jwt"; import { UserService } from "@user/user.service"; @@ -11,7 +12,7 @@ import { UserDto } from "./dto/user.dto"; import { AuthResponseDto } from "./dto/auth-response.dto"; import { JwtPayload } from "./auth.types"; import { CreateUserDto } from "@user/dto/create-user.dto"; -import { PrismaService } from "@prisma/prisma.service"; +import { PrismaService } from "@ourPrisma/prisma.service"; import { ConfigService } from "@nestjs/config"; import { parseExpiresIn, timespanToMs } from "./auth.utils"; @@ -22,7 +23,7 @@ export class AuthService { private readonly jwtService: JwtService, private readonly googleAuthService: GoogleAuthService, private readonly prisma: PrismaService, - private readonly configService: ConfigService + @Inject(ConfigService) private readonly configService: ConfigService ) {} async generateTokens( diff --git a/src/auth/dto/auth-response.dto.ts b/src/auth/dto/auth-response.dto.ts index 692be41..1bbd325 100644 --- a/src/auth/dto/auth-response.dto.ts +++ b/src/auth/dto/auth-response.dto.ts @@ -2,8 +2,8 @@ import { ApiProperty, ApiHideProperty } from "@nestjs/swagger"; export class AuthResponseDto { @ApiProperty({ example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }) - access_token!: string; + access_token!: string; @ApiHideProperty() - refresh_token!: string; + refresh_token!: string; } diff --git a/src/auth/dto/login.dto.ts b/src/auth/dto/login.dto.ts index 8bbd431..468f25e 100644 --- a/src/auth/dto/login.dto.ts +++ b/src/auth/dto/login.dto.ts @@ -8,7 +8,7 @@ export class LoginDto { }) @IsEmail({}, { message: "Email must be a valid email address" }) @IsNotEmpty({ message: "Email is required" }) - email!: string; + email!: string; @ApiProperty({ description: "User password", @@ -18,5 +18,5 @@ export class LoginDto { @IsString() @MinLength(6, { message: "Password must be at least 6 characters" }) @IsNotEmpty({ message: "Password is required" }) - password!: string; + password!: string; } diff --git a/src/auth/dto/role.dto.ts b/src/auth/dto/role.dto.ts index 4a852bb..71a8607 100644 --- a/src/auth/dto/role.dto.ts +++ b/src/auth/dto/role.dto.ts @@ -3,8 +3,8 @@ import { ApiProperty } from "@nestjs/swagger"; export class RoleDto { @ApiProperty({ example: 2, description: "Role ID" }) - id!: number; + id!: number; @ApiProperty({ example: "admin", description: "Role name" }) - name!: string; + name!: string; } diff --git a/src/auth/dto/user.dto.ts b/src/auth/dto/user.dto.ts index 600a93b..ecb2c18 100644 --- a/src/auth/dto/user.dto.ts +++ b/src/auth/dto/user.dto.ts @@ -3,31 +3,31 @@ import { RoleDto } from "./role.dto"; export class UserDto { @ApiProperty({ example: 1, description: "User ID" }) - id!: number; + id!: number; @ApiProperty({ example: "user@example.com", description: "User email" }) - email!: string; + email!: string; @ApiProperty({ example: "user_name", description: "Username" }) - username!: string; + username!: string; @ApiProperty({ example: "First", required: false, description: "Optional nickname" }) - nickname?: string | null; + nickname?: string | null; @ApiProperty({ example: "2025-05-05T12:51:04.098Z", description: "Date of creation" }) - createdAt!: Date; + createdAt!: Date; @ApiProperty({ type: [RoleDto], required: false, description: "List of user roles" }) - roles?: RoleDto[]; + roles?: RoleDto[]; } diff --git a/src/auth/google-auth.service.ts b/src/auth/google-auth.service.ts index 7dc53fa..bf98d7f 100644 --- a/src/auth/google-auth.service.ts +++ b/src/auth/google-auth.service.ts @@ -1,4 +1,5 @@ -import { Injectable, UnauthorizedException } from "@nestjs/common"; +import { Inject, Injectable, UnauthorizedException } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; import { OAuth2Client, LoginTicket } from "google-auth-library"; import { GoogleUserPayload } from "./auth.types"; @@ -6,13 +7,13 @@ import { GoogleUserPayload } from "./auth.types"; export class GoogleAuthService { private readonly client: OAuth2Client; - constructor() { - this.client = new OAuth2Client(process.env["GOOGLE_CLIENT_ID"]); + constructor(@Inject(ConfigService) private readonly configService: ConfigService) { + this.client = new OAuth2Client(this.configService.get("GOOGLE_CLIENT_ID")); } async verifyGoogleToken(token: string): Promise { try { - const audience = process.env["GOOGLE_CLIENT_ID"] ?? ""; + const audience = this.configService.get("GOOGLE_CLIENT_ID") ?? ""; const ticket: LoginTicket = await this.client.verifyIdToken({ idToken: token, diff --git a/src/auth/guards/project.guard.ts b/src/auth/guards/project.guard.ts index cdd8456..fc09565 100644 --- a/src/auth/guards/project.guard.ts +++ b/src/auth/guards/project.guard.ts @@ -5,7 +5,7 @@ import { ForbiddenException, NotFoundException } from "@nestjs/common"; -import { PrismaService } from "@prisma/prisma.service"; +import { PrismaService } from "@ourPrisma/prisma.service"; @Injectable() export class ProjectCreatorGuard implements CanActivate { diff --git a/src/common/dto/pagination.dto.ts b/src/common/dto/pagination.dto.ts index dfd4ebd..f7e8fef 100644 --- a/src/common/dto/pagination.dto.ts +++ b/src/common/dto/pagination.dto.ts @@ -1,19 +1,9 @@ import { ApiProperty } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsInt, IsOptional, Min } from "class-validator"; export class PaginationDto { @ApiProperty({ required: false, default: 1 }) - @IsOptional() - @Type(() => Number) - @IsInt() - @Min(1) - page?: number = 1; + page?: number = 1; @ApiProperty({ required: false, default: 10 }) - @IsOptional() - @Type(() => Number) - @IsInt() - @Min(1) - limit?: number = 10; + limit?: number = 10; } diff --git a/src/main.ts b/src/main.ts index 46315f5..078d9d8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,17 +1,19 @@ import { NestFactory } from "@nestjs/core"; import { ValidationPipe } from "@nestjs/common"; -import { AppModule } from "./app.module"; +import { AppModule } from "src/app.module"; +import { AppConfig } from "src/app.config"; import { NestExpressApplication, ExpressAdapter } from "@nestjs/platform-express"; -import { setupSwagger } from "./swagger"; +import { setupSwagger } from "src/swagger"; import { format } from "date-fns-tz"; -import { setupWebSocketServer } from "./collab/signaling/signal"; +import { setupWebSocketServer } from "@webrtc/signal"; import { Logger } from "@nestjs/common"; import express, { Request, Response, NextFunction } from "express"; import { ConfigService } from "@nestjs/config"; -import * as path from "path"; + +//import * as path from "path"; import * as dotenv from "dotenv"; import * as http from "http"; import cookieParser from "cookie-parser"; @@ -24,9 +26,7 @@ if (process.env["NODE_ENV"] === "production") { (async () => { const expressApp = express(); - const server = http.createServer(expressApp); - const app = await NestFactory.create( AppModule, new ExpressAdapter(expressApp) @@ -34,16 +34,10 @@ if (process.env["NODE_ENV"] === "production") { const logger = new Logger("HTTP"); const configService = app.get(ConfigService); - const frontendUrls = (configService.get("FRONTEND_URL") || "") - .split(",") - .map((value) => value.trim()) - .filter(Boolean); - - if (frontendUrls.length === 0) { - logger.warn( - "FRONTEND_URL is not configured. CORS will reject browser origins until it is set." - ); - } + const frontendUrl = configService.get( + "FRONTEND_URL", + "http://localhost:3001" + ); app.use(cookieParser()); app.useLogger(["log", "error", "warn", "debug"]); @@ -56,12 +50,12 @@ if (process.env["NODE_ENV"] === "production") { }) ); - app.useStaticAssets(path.join(__dirname, "..", "public")); - app.setBaseViewsDir(path.join(__dirname, "..", "views")); - app.setViewEngine("ejs"); + //app.useStaticAssets(path.join(__dirname, "..", "public")); + //app.setBaseViewsDir(path.join(__dirname, "..", "views")); + //app.setViewEngine("ejs"); app.enableCors({ - origin: frontendUrls, + origin: frontendUrl, credentials: true, methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], allowedHeaders: ["Content-Type", "Authorization"] @@ -80,14 +74,25 @@ if (process.env["NODE_ENV"] === "production") { next(); }); + setupSwagger(app); + setupWebSocketServer(server); await app.init(); - setupWebSocketServer(server); + const PORT = configService.get("PORT") || 3000; - const PORT = configService.get("PORT", 3000); server.listen(PORT, () => { - logger.log(`Server listening on port ${PORT}`); + const address = server.address(); + const actualPort = + typeof address === "object" && address !== null + ? address.port + : Number(PORT); + + const appConfig = app.get(AppConfig); + appConfig.setPort(actualPort); + + logger.log(`Server listening on port ${actualPort}`); }); })(); + diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts index c708893..10ff8f7 100644 --- a/src/prisma/prisma.service.ts +++ b/src/prisma/prisma.service.ts @@ -1,32 +1,19 @@ import { Injectable, OnModuleInit, OnModuleDestroy } from "@nestjs/common"; -import { PrismaClient } from "@prisma/client"; import { PrismaPg } from "@prisma/adapter-pg"; -import { Pool } from "pg"; -import { ConfigService } from "@nestjs/config"; - -class MissingDatabaseUrlError extends Error { - constructor() { - super( - "DATABASE_URL is missing. Configure it in your environment before starting the backend." - ); - this.name = "MissingDatabaseUrlError"; - } -} +import { PrismaClient } from "@prisma/client"; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { - constructor(configService: ConfigService) { - const connectionString = configService.get("DATABASE_URL"); + constructor() { + const connectionString = process.env["DATABASE_URL"]; if (!connectionString) { - throw new MissingDatabaseUrlError(); + throw new ReferenceError("DATABASE_URL environment variable is not set"); } - - super({ - adapter: new PrismaPg(new Pool({ connectionString })), - }); + const adapter = new PrismaPg({ connectionString }); + super({ adapter }); } async onModuleInit(): Promise { diff --git a/src/prisma/prisma.ts b/src/prisma/prisma.ts deleted file mode 100644 index 63f8bbd..0000000 --- a/src/prisma/prisma.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); -export default prisma; diff --git a/src/routes/multiplayer/dto/close-host.dto.ts b/src/routes/multiplayer/dto/close-host.dto.ts new file mode 100644 index 0000000..6052848 --- /dev/null +++ b/src/routes/multiplayer/dto/close-host.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsUUID } from "class-validator"; + +// FIXME: If we need to support more than one game session per project/game, this should be +// modified as well to use sessionUuid over projectId. +export class CloseHostRequestDto { + @ApiProperty({ description: "ID of the project whose session to close" }) + @IsUUID() + projectId!: number; +}; diff --git a/src/routes/multiplayer/dto/join-host.dto.ts b/src/routes/multiplayer/dto/join-host.dto.ts new file mode 100644 index 0000000..b5e8957 --- /dev/null +++ b/src/routes/multiplayer/dto/join-host.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsUUID } from "class-validator"; +import { WebRTCOfferDto } from "@webrtc/webrtc.dto"; + +export class JoinHostRequestDto { + @ApiProperty() + @IsUUID() + sessionUuid!: string; +}; + +export class JoinHostResponseDto { + @ApiProperty({ type: () => WebRTCOfferDto }) + @Type(() => WebRTCOfferDto) + webrtcConfig!: WebRTCOfferDto; +}; diff --git a/src/routes/multiplayer/dto/leave-host.dto.ts b/src/routes/multiplayer/dto/leave-host.dto.ts new file mode 100644 index 0000000..e5ea245 --- /dev/null +++ b/src/routes/multiplayer/dto/leave-host.dto.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from "@nestjs/swagger"; + +export class LeaveHostRequestDto { + @ApiProperty() + sessionUuid!: string; +}; diff --git a/src/routes/multiplayer/dto/lookup-hosts.dto.ts b/src/routes/multiplayer/dto/lookup-hosts.dto.ts new file mode 100644 index 0000000..1427c4c --- /dev/null +++ b/src/routes/multiplayer/dto/lookup-hosts.dto.ts @@ -0,0 +1,25 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { GameSessionVisibility } from "@prisma/client"; +import { IsArray, IsEnum, IsInt, IsUUID } from "class-validator"; + +export class LookupHostsResponseDtoHost { + @ApiProperty() + @IsUUID() + sessionUuid!: string; + + @ApiProperty({ enum: GameSessionVisibility }) + @IsEnum(GameSessionVisibility) + sessionVisibility!: GameSessionVisibility; + + @ApiProperty() + @IsInt() + playerCount!: number; +}; + +// This might look pointless to make a struct like this, but it's actually +// useful for future expansion +export class LookupHostsResponseDto { + @ApiProperty({ type: () => [LookupHostsResponseDtoHost] }) + @IsArray() + hosts!: LookupHostsResponseDtoHost[]; +}; diff --git a/src/routes/multiplayer/dto/open-host.dto.ts b/src/routes/multiplayer/dto/open-host.dto.ts new file mode 100644 index 0000000..58c70f0 --- /dev/null +++ b/src/routes/multiplayer/dto/open-host.dto.ts @@ -0,0 +1,25 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { GameSessionVisibility } from "@prisma/client"; +import { IsEnum, IsInt, IsUUID } from "class-validator"; +import { WebRTCOfferDto } from "@webrtc/webrtc.dto"; +import { Type } from "class-transformer"; + +export class OpenHostRequestDto { + @ApiProperty() + @IsInt() + projectId!: number; + + @ApiProperty({ enum: GameSessionVisibility }) + @IsEnum(GameSessionVisibility) + visibility!: GameSessionVisibility; +}; + +export class OpenHostResponseDto { + @ApiProperty() + @IsUUID() + sessionUuid!: string; + + @ApiProperty({ type: () => WebRTCOfferDto }) + @Type(() => WebRTCOfferDto) + webrtcConfig!: WebRTCOfferDto; +}; diff --git a/src/routes/multiplayer/multiplayer.controller.ts b/src/routes/multiplayer/multiplayer.controller.ts new file mode 100644 index 0000000..0fd94dd --- /dev/null +++ b/src/routes/multiplayer/multiplayer.controller.ts @@ -0,0 +1,250 @@ +import { ApiBody, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { GameSessionEx, MultiplayerService } from "./multiplayer.service"; +import { WebRTCService } from "@webrtc/webrtc.service"; +import { + BadRequestException, + Body, + Controller, + Delete, + Get, + HttpStatus, + InternalServerErrorException, + Logger, + NotFoundException, + Patch, + Post, + Req, + UseGuards +} from "@nestjs/common"; +import { GameSession } from "@prisma/client"; +import { JwtAuthGuard } from "@auth/guards/jwt-auth.guard"; +import { RequestWithUser } from "@auth/auth.types"; +import { ProjectNotFoundError } from "@project/project.error"; +import { OpenHostRequestDto, OpenHostResponseDto } from "./dto/open-host.dto"; +import { LookupHostsResponseDto, LookupHostsResponseDtoHost } from "./dto/lookup-hosts.dto"; +import { + MultiplayerHostNotFoundError, + MultiplayerHostOpenedError, + MultiplayerUserAlreadyJoinedError, + MultiplayerUserDoesNotExistError, + MultiplayerUserNotInSessionError +} from "./multiplayer.error"; +import { CloseHostRequestDto } from "./dto/close-host.dto"; +import { getExcerrMessage as getExcerrMessage } from "src/util/errors"; +import { JoinHostRequestDto, JoinHostResponseDto } from "./dto/join-host.dto"; +import { LeaveHostRequestDto } from "./dto/leave-host.dto"; + +@ApiTags("multiplayer") +@Controller("multiplayer") +@UseGuards(JwtAuthGuard) +export class MultiplayerController { + private readonly logger = new Logger(MultiplayerController.name); + + constructor( + private readonly multiplayerService: MultiplayerService, + private readonly webrtcService: WebRTCService + ) {} + + @Get("list-hosts") + @ApiOperation({ summary: "List available game hosts/sessions from the user's perspective" }) + @ApiResponse({ + status: HttpStatus.OK, + description: "A list of available game hosts is returned.", + type: LookupHostsResponseDto + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: "Bad request (wrong project ID)." + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: "Unhandled server error." + }) + async lookupHosts( + @Req() requestCtx: RequestWithUser, + @Body() request: { projectId: number } + ): Promise + { + let hosts: GameSessionEx[]; + + try { + hosts = await this.multiplayerService.lookupHosts(request.projectId, requestCtx.user.id); + } catch (error) { + if (error instanceof ProjectNotFoundError) { + throw new BadRequestException(error.message); + } + + this.logger.error(`Error while looking up hosts for project ID ${request.projectId}`); + this.logger.error(error); + + throw new InternalServerErrorException(getExcerrMessage(error)); + } + + const responseDto = new LookupHostsResponseDto(); + responseDto.hosts = hosts.map((host) => { + const hostDto = new LookupHostsResponseDtoHost(); + hostDto.sessionUuid = host.sessionId; + hostDto.sessionVisibility = host.visibility; + hostDto.playerCount = host.otherUsers.length + 1; + + return hostDto; + }); + + return responseDto; + } + + @Post("open-host") + @ApiOperation({ summary: "Open a new game host/session, with the caller being the game host" }) + @ApiBody({ type: OpenHostRequestDto }) + @ApiResponse({ + status: HttpStatus.OK, + description: "The game host/session has been successfully opened.", + type: OpenHostResponseDto + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: "The user or project was not found." + }) + // FIXME: See comment below + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: "The user is already hosting a game session for this project." + }) + async openHost( + @Req() requestCtx: RequestWithUser, + @Body() request: OpenHostRequestDto + ): Promise + { + let gameSession: GameSession; + + try { + gameSession = await this.multiplayerService.openHost(requestCtx.user.id, request.projectId, request.visibility); + } catch (error) { + if (error instanceof MultiplayerUserDoesNotExistError || + error instanceof MultiplayerHostNotFoundError) { + throw new NotFoundException(error.message); + } else if (error instanceof MultiplayerHostOpenedError) { + // FIXME: Should the user be able to open multiple hosts for the same project? + throw new BadRequestException(error.message); + } + + this.logger.error(`Error while opening host for user ID ${requestCtx.user.id} and project ID ${request.projectId}`); + this.logger.error(error); + + throw new InternalServerErrorException(getExcerrMessage(error)); + } + + const responseDto = new OpenHostResponseDto(); + responseDto.sessionUuid = gameSession.sessionId; + responseDto.webrtcConfig = this.webrtcService.buildOffer(); + + return responseDto; + } + + @Delete("close-host") + @ApiOperation({ summary: "Close an existing game host/session, with the caller being the game host" }) + @ApiResponse({ + status: HttpStatus.OK, + description: "The game host/session has been successfully closed." + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: "The user, project, or game session was not found." + }) + async closeHost( + @Req() requestCtx: RequestWithUser, + @Body() request: CloseHostRequestDto + ): Promise { + try { + await this.multiplayerService.closeHost(requestCtx.user.id, request.projectId); + } catch (error) { + if (error instanceof MultiplayerUserDoesNotExistError || + error instanceof ProjectNotFoundError) { + throw new NotFoundException(error.message); + } else if (error instanceof MultiplayerHostNotFoundError) { + throw new BadRequestException(error.message); + } + + this.logger.error(`Error while closing host for user ID ${requestCtx.user.id} and project ID ${request.projectId}`); + this.logger.error(error); + + throw new InternalServerErrorException(getExcerrMessage(error)); + } + } + + @Patch("join-host") + @ApiOperation({ summary: "Join an existing game host/session as a player" }) + @ApiBody({ type: JoinHostRequestDto }) + @ApiResponse({ + status: HttpStatus.OK, + description: "Successfully joined the game session.", + type: JoinHostResponseDto + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: "Game session or user not found." + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: "User is already in the session or is the host." + }) + async joinHost( + @Req() requestCtx: RequestWithUser, + @Body() request: JoinHostRequestDto + ): Promise { + try { + await this.multiplayerService.joinHost(requestCtx.user.id, request.sessionUuid); + } catch (error) { + if (error instanceof MultiplayerUserDoesNotExistError || + error instanceof MultiplayerHostNotFoundError) { + throw new NotFoundException(error.message); + } else if (error instanceof MultiplayerUserAlreadyJoinedError) { + throw new BadRequestException(error.message); + } + + this.logger.error(`Error while joining host for user ID ${requestCtx.user.id} and session UUID ${request.sessionUuid}`); + this.logger.error(error); + + throw new InternalServerErrorException(getExcerrMessage(error)); + } + + const responseDto = new JoinHostResponseDto(); + responseDto.webrtcConfig = this.webrtcService.buildOffer(); + + return responseDto; + } + + @Patch("leave-host") + @ApiOperation({ summary: "Leave a game host/session as a player" }) + @ApiBody({ type: LeaveHostRequestDto }) + @ApiResponse({ + status: HttpStatus.OK, + description: "Successfully left the game session." + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: "Game session or user not found." + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: "User is not part of the session or is the host." + }) + async leaveHost( + @Req() requestCtx: RequestWithUser, + @Body() request: LeaveHostRequestDto + ): Promise { + try { + await this.multiplayerService.leaveHost(requestCtx.user.id, request.sessionUuid); + } catch (error) { + if (error instanceof MultiplayerUserNotInSessionError || + error instanceof MultiplayerHostNotFoundError) { + throw new NotFoundException(error.message); + } + + this.logger.error(`Error while leaving host for user ID ${requestCtx.user.id} and session UUID ${request.sessionUuid}`); + this.logger.error(error); + + throw new InternalServerErrorException(getExcerrMessage(error)); + } + } +} diff --git a/src/routes/multiplayer/multiplayer.error.ts b/src/routes/multiplayer/multiplayer.error.ts new file mode 100644 index 0000000..07e1f83 --- /dev/null +++ b/src/routes/multiplayer/multiplayer.error.ts @@ -0,0 +1,65 @@ +export class MultiplayerError extends Error { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} + +export class MultiplayerInvalidStateError extends MultiplayerError { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} + +export class MultiplayerHostOpenedError extends MultiplayerError { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} + +export class MultiplayerHostInvalidError extends MultiplayerError { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} + +export class MultiplayerHostNotFoundError extends MultiplayerError { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} + +// FIXME: This should rather be in a src/routes/user/user.error.ts file. +// Also, this should be thrown by the user service, evidently. +// Will do a lot of clean-up with the backend. +export class MultiplayerUserDoesNotExistError extends MultiplayerError { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} + +export class MultiplayerUserAlreadyJoinedError extends MultiplayerError { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} + +export class MultiplayerUserNotInSessionError extends MultiplayerError { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} + +export class MultiplayerGameSessionNotFoundError extends MultiplayerError { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} diff --git a/src/routes/multiplayer/multiplayer.module.ts b/src/routes/multiplayer/multiplayer.module.ts new file mode 100644 index 0000000..f4f4edd --- /dev/null +++ b/src/routes/multiplayer/multiplayer.module.ts @@ -0,0 +1,15 @@ +import { UserModule } from "@user/user.module"; +import { MultiplayerController } from "./multiplayer.controller"; +import { MultiplayerService } from "./multiplayer.service"; +import { Module } from "@nestjs/common"; +import { PrismaModule } from "@ourPrisma/prisma.module"; +import { ProjectModule } from "@project/project.module"; +import { WebRTCModule } from "@webrtc/webrtc.module"; + +@Module({ + imports: [UserModule, ProjectModule, PrismaModule, WebRTCModule], + controllers: [MultiplayerController], + providers: [MultiplayerService], + exports: [MultiplayerService] +}) +export class MultiplayerModule {} diff --git a/src/routes/multiplayer/multiplayer.service.spec.ts b/src/routes/multiplayer/multiplayer.service.spec.ts new file mode 100644 index 0000000..8a17fcd --- /dev/null +++ b/src/routes/multiplayer/multiplayer.service.spec.ts @@ -0,0 +1,223 @@ +import { ProjectService, ProjectEx } from "@project/project.service"; +import { UserService } from "@user/user.service"; +import { PrismaService } from "@ourPrisma/prisma.service"; +import { ConfigService } from "@nestjs/config"; +import { S3Service } from "@s3/s3.service"; +import { MultiplayerService } from "./multiplayer.service"; +import { Test } from "@nestjs/testing"; +import { User, GameSession, GameSessionVisibility } from "@prisma/client"; + +describe("MultiplayerService", () => { + let service: MultiplayerService; + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [ + MultiplayerService, + ProjectService, + UserService, + { + provide: PrismaService, + useValue: { + project: {}, + user: {}, + workSession: {}, + gameSession: { + create: jest.fn(), + findUnique: jest.fn(), + findMany: jest.fn(), + update: jest.fn() + }, + $connect: jest.fn(), + $disconnect: jest.fn() + } + }, + { + provide: ConfigService, + useValue: { + get: jest.fn((key: string) => { + if (key === "S3_MAX_AUTO_HISTORY_VERSION") return "5"; + if (key === "S3_AUTO_HISTORY_DELAY") return "10"; + if (key === "S3_MAX_CHECKPOINTS") return "5"; + return undefined; + }) + } + }, + { + provide: S3Service, + useValue: {} + } + ] + }).compile(); + + service = module.get(MultiplayerService); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); + + describe("lookupHosts", () => { + // TODO: Implement tests for lookupHosts once friends logic is implemented in the service + }); + + describe("openHost", () => { + it("should throw if user does not exist", async () => { + jest.spyOn(service["userService"], "findOne").mockResolvedValueOnce(null as any); + await expect(service.openHost(1, 1, "PUBLIC" as import("@prisma/client").GameSessionVisibility)).rejects.toThrow("User with ID 1 not found"); + }); + it("should throw if project does not exist", async () => { + jest.spyOn(service["userService"], "findOne").mockResolvedValueOnce({ + id: 1, + email: "user@example.com", + username: "user", + nickname: null, + password: "hashed", + createdAt: new Date(), + hostingGameSessions: [] + } as User & { hostingGameSessions: GameSession[] }); + jest.spyOn(service["projectService"], "findOne").mockResolvedValueOnce(null as any); + await expect(service.openHost(1, 1, "PUBLIC" as GameSessionVisibility)).rejects.toThrow("Project with ID 1 not found"); + }); + it("should throw if user already hosting", async () => { + jest.spyOn(service["userService"], "findOne").mockResolvedValueOnce({ + id: 1, + email: "user@example.com", + username: "user", + nickname: null, + password: "hashed", + createdAt: new Date(), + hostingGameSessions: [{ + id: 1, + hostId: 1, + projectId: 1, + startedAt: new Date(), + endedAt: null, + visibility: "PUBLIC" as GameSessionVisibility, + sessionId: "uuid" + }] + } as User & { hostingGameSessions: GameSession[] }); + jest.spyOn(service["projectService"], "findOne").mockResolvedValueOnce({ + id: 1, + name: "Project", + createdAt: new Date(), + updatedAt: new Date(), + collaborators: [], + creator: { id: 1, username: "creator", email: "creator@example.com" } + } as unknown as ProjectEx); + await expect(service.openHost(1, 1, "PUBLIC" as GameSessionVisibility)).rejects.toThrow("User already hosting a game session for this project"); + }); + it("should create a new game session", async () => { + jest.spyOn(service["userService"], "findOne").mockResolvedValueOnce({ + id: 1, + email: "user@example.com", + username: "user", + nickname: null, + password: "hashed", + createdAt: new Date(), + hostingGameSessions: [] + } as User & { hostingGameSessions: GameSession[] }); + jest.spyOn(service["projectService"], "findOne").mockResolvedValueOnce({ + id: 1, + name: "Project", + createdAt: new Date(), + updatedAt: new Date(), + collaborators: [], + creator: { id: 1, username: "creator", email: "creator@example.com" } + } as unknown as ProjectEx); + jest.spyOn(service["prismaService"].gameSession, "create").mockResolvedValueOnce({ + id: 1, + hostId: 1, + projectId: 1, + startedAt: new Date(), + endedAt: null, + visibility: "PUBLIC" as GameSessionVisibility, + sessionId: "uuid" + } as GameSession); + jest.spyOn(service["userService"], "attachGameSession").mockResolvedValueOnce(Promise.resolve()); + const result = await service.openHost(1, 1, "PUBLIC" as GameSessionVisibility); + expect(result).toHaveProperty("id", 1); + }); + }); + + describe("closeHost", () => { + it("should throw if user does not exist", async () => { + jest.spyOn(service["userService"], "findOne").mockResolvedValueOnce(null as never); + await expect(service.closeHost(1, 1)).rejects.toThrow(); + }); + it("should throw if project does not exist", async () => { + jest.spyOn(service["userService"], "findOne").mockResolvedValueOnce({ + id: 1, + email: "user@example.com", + username: "user", + nickname: null, + password: "hashed", + createdAt: new Date(), + hostingGameSessions: [] + } as User & { hostingGameSessions: GameSession[] }); + jest.spyOn(service["projectService"], "findOne").mockResolvedValueOnce(null as never); + await expect(service.closeHost(1, 1)).rejects.toThrow(); + }); + it("should throw if not hosting", async () => { + jest.spyOn(service["userService"], "findOne").mockResolvedValueOnce({ + id: 1, + email: "user@example.com", + username: "user", + nickname: null, + password: "hashed", + createdAt: new Date(), + hostingGameSessions: [] + } as User & { hostingGameSessions: GameSession[] }); + jest.spyOn(service["projectService"], "findOne").mockResolvedValueOnce({ + id: 1, + name: "Project", + createdAt: new Date(), + updatedAt: new Date(), + collaborators: [], + creator: { id: 1, username: "creator", email: "creator@example.com" } + } as unknown as ProjectEx); + await expect(service.closeHost(1, 1)).rejects.toThrow(); + }); + }); + + describe("joinHost", () => { + it("should throw if user does not exist", async () => { + jest.spyOn(service["userService"], "findOne").mockResolvedValueOnce(null as never); + await expect(service.joinHost(1, "uuid")).rejects.toThrow(); + }); + it("should throw if host session does not exist", async () => { + jest.spyOn(service["userService"], "findOne").mockResolvedValueOnce({ + id: 2, + email: "user2@example.com", + username: "user2", + nickname: null, + password: "hashed", + createdAt: new Date(), + joinedGameSessions: [] + } as User & { joinedGameSessions: GameSession[] }); + jest.spyOn(service["prismaService"].gameSession, "findUnique").mockResolvedValueOnce(null); + await expect(service.joinHost(1, "uuid")).rejects.toThrow(); + }); + }); + + describe("leaveHost", () => { + it("should throw if user does not exist", async () => { + jest.spyOn(service["userService"], "findOne").mockResolvedValueOnce(null as never); + await expect(service.leaveHost(1, "uuid")).rejects.toThrow(); + }); + it("should throw if host session does not exist", async () => { + jest.spyOn(service["userService"], "findOne").mockResolvedValueOnce({ + id: 2, + email: "user2@example.com", + username: "user2", + nickname: null, + password: "hashed", + createdAt: new Date(), + joinedGameSessions: [] + } as User & { joinedGameSessions: GameSession[] }); + jest.spyOn(service["prismaService"].gameSession, "findUnique").mockResolvedValueOnce(null); + await expect(service.leaveHost(1, "uuid")).rejects.toThrow(); + }); + }); +}); + diff --git a/src/routes/multiplayer/multiplayer.service.ts b/src/routes/multiplayer/multiplayer.service.ts new file mode 100644 index 0000000..ecba2bd --- /dev/null +++ b/src/routes/multiplayer/multiplayer.service.ts @@ -0,0 +1,188 @@ +import { Injectable } from "@nestjs/common"; +import { GameSession, GameSessionVisibility, User } from "@prisma/client"; +import { PrismaService } from "@ourPrisma/prisma.service"; +import { UserService } from "@user/user.service"; +import { ProjectService } from "@project/project.service"; +import { MultiplayerHostNotFoundError, MultiplayerHostOpenedError, MultiplayerInvalidStateError, MultiplayerUserAlreadyJoinedError, MultiplayerUserDoesNotExistError, MultiplayerUserNotInSessionError } from "./multiplayer.error"; +import { ProjectNotFoundError } from "@project/project.error"; + +// I made this "extended" type (Ex) for complex fields that do relations with +// other stuff. +export type GameSessionEx = GameSession & { + otherUsers: User[] +}; + +@Injectable() +export class MultiplayerService { + constructor( + private userService: UserService, + private projectService: ProjectService, + private prismaService: PrismaService + ) {} + + async lookupHosts(projectId: number, userId: number): Promise { + const matchingGSes = await this.prismaService.gameSession.findMany({ + include: { otherUsers: true }, + where: { projectId: projectId } + }); + + if (!matchingGSes) { + throw new ProjectNotFoundError(`Project ID ${projectId} not found`); + } + + const userAvailableGSes = new Array(); + + matchingGSes.forEach((gameSession) => { + switch (gameSession.visibility) { + case GameSessionVisibility.PUBLIC: + break; + + case GameSessionVisibility.FRIENDS_ONLY: + // FIXME: Check if otherUsers contains at least userId in their friend list + // gameSession.otherUsers.some(otherUser => userService.areFriends(userId, otherUserId)); + void userId; + break; + + case GameSessionVisibility.PRIVATE: + return; + } + + userAvailableGSes.push(gameSession); + }); + + return userAvailableGSes; + } + + async openHost(userId: number, projectId: number, visibility: GameSessionVisibility): Promise { + // Check if the user & project exists by simply getting them. If the object does not exist, + // the exception throw serves as a guard. + + const requestedUser = await this.userService.findOne<{ + hostingGameSessions: GameSession[] + }>(userId, { hostingGameSessions: true }); + if (!requestedUser) { + throw new MultiplayerUserDoesNotExistError(`User with ID ${userId} not found`); + } + const project = await this.projectService.findOne(projectId); + if (!project) { + throw new ProjectNotFoundError(`Project with ID ${projectId} not found`); + } + + requestedUser.hostingGameSessions.forEach((hostedGSes) => { + if (hostedGSes.projectId != projectId) { + return; + } + + throw new MultiplayerHostOpenedError("User already hosting a game session for this project"); + }); + + const createdGS = await this.prismaService.gameSession.create({ + data: { + hostId: userId, + projectId: projectId, + visibility: visibility + } + }); + + await this.userService.attachGameSession(userId, createdGS.id, true); + + return createdGS; + } + + async closeHost(userId: number, projectId: number): Promise { + // Same as for openHost + const requestedUser = await this.userService.findOne<{ + hostingGameSessions: GameSession[] + }>(userId, { hostingGameSessions: true }); + if (!requestedUser) { + throw new MultiplayerUserDoesNotExistError(`User with ID ${userId} not found`); + } + const project = await this.projectService.findOne(projectId); + if (!project) { + throw new ProjectNotFoundError(`Project with ID ${projectId} not found`); + } + + const basicHostedGS = + requestedUser.hostingGameSessions.find((hostedGS) => hostedGS.projectId === projectId); + + if (!basicHostedGS) { + throw new MultiplayerHostNotFoundError("User is not hosting a game session for this project"); + } + + await this.userService.detachGameSession(userId, basicHostedGS.id, true); + + const hostedGS = await this.prismaService.gameSession.findUnique({ + where: { id: basicHostedGS.id }, + include: { otherUsers: true } + }); + + if (!hostedGS) { + // This can only be reached if the database is in an inconsitent state + throw new MultiplayerInvalidStateError("Game session attached to user no longer exists"); + } + + await Promise.all( + hostedGS.otherUsers.map(async (user) => { + await this.userService.detachGameSession(user.id, hostedGS.id, false); + }) + ); + } + + async joinHost(joiningUserId: number, hostUuid: string): Promise { + // Same as for openHost + const joiningUser = await this.userService.findOne<{ + joinedGameSessions: GameSession[] + }>(joiningUserId, { joinedGameSessions: true }); + if (!joiningUser) { + throw new MultiplayerUserNotInSessionError(`User with ID ${joiningUserId} not found`); + } + const hostedGS = await this.prismaService.gameSession.findUnique({ + where: { sessionId: hostUuid }, + }); + + if (!hostedGS) { + throw new MultiplayerHostNotFoundError("No game session found for the provided host UUID"); + } else if (hostedGS.hostId === joiningUserId) { + throw new MultiplayerUserAlreadyJoinedError("User is already the host of this game session"); + } else if (joiningUser.joinedGameSessions.some(joinedGS => joinedGS.id === hostedGS.id)) { + throw new MultiplayerUserAlreadyJoinedError("User has already joined this game session"); + } + + await this.prismaService.gameSession.update({ + where: { id: hostedGS.id }, + data: { + otherUsers: { connect: { id: joiningUserId } } + } + }); + } + + async leaveHost(leavingUserId: number, hostUuid: string): Promise { + // Same as for openHost + const leavingUser = await this.userService.findOne<{ + joinedGameSessions: GameSession[] + }>(leavingUserId, { joinedGameSessions: true }); + if (!leavingUser) { + throw new MultiplayerUserNotInSessionError(`User with ID ${leavingUserId} not found`); + } + + const hostedGS = await this.prismaService.gameSession.findUnique({ + where: { sessionId: hostUuid }, + include: { otherUsers: true } + }); + + if (!hostedGS) { + throw new MultiplayerHostNotFoundError("No game session found for the provided host UUID"); + } else if (hostedGS.hostId === leavingUserId) { + throw new MultiplayerUserNotInSessionError("Host user cannot leave their own game session"); + } else if (!leavingUser.joinedGameSessions.some(joinedGS => joinedGS.id === hostedGS.id)) { + throw new MultiplayerUserNotInSessionError("User is not part of this game session"); + } + + await this.prismaService.gameSession.update({ + where: { id: hostedGS.id }, + data: { + otherUsers: { disconnect: { id: leavingUserId } } + } + }); + } +} diff --git a/src/routes/project/dto/collaborator-project.dto.ts b/src/routes/project/dto/collaborator-project.dto.ts index e69975e..02fc60f 100644 --- a/src/routes/project/dto/collaborator-project.dto.ts +++ b/src/routes/project/dto/collaborator-project.dto.ts @@ -17,7 +17,7 @@ export class AddCollaboratorDto { @IsOptional() @IsInt() @Validate(AtLeastOneConstraint, [["userId", "username", "email"]]) - userId?: number; + userId?: number; @ApiProperty({ description: "Username of the user to add as collaborator", @@ -26,7 +26,7 @@ export class AddCollaboratorDto { }) @IsOptional() @IsString() - username?: string; + username?: string; @ApiProperty({ description: "Email of the user to add as collaborator", @@ -35,7 +35,7 @@ export class AddCollaboratorDto { }) @IsOptional() @IsEmail() - email?: string; + email?: string; } export class RemoveCollaboratorDto { @@ -47,7 +47,7 @@ export class RemoveCollaboratorDto { @IsOptional() @IsInt() @Validate(AtLeastOneConstraint, [["userId", "username", "email"]]) - userId?: number; + userId?: number; @ApiProperty({ description: "Username of the user to remove as collaborator", @@ -56,7 +56,7 @@ export class RemoveCollaboratorDto { }) @IsOptional() @IsString() - username?: string; + username?: string; @ApiProperty({ description: "Email of the user to remove as collaborator", @@ -65,5 +65,5 @@ export class RemoveCollaboratorDto { }) @IsOptional() @IsEmail() - email?: string; + email?: string; } diff --git a/src/routes/project/dto/project-response.dto.ts b/src/routes/project/dto/project-response.dto.ts index 1e4ec53..be35f01 100644 --- a/src/routes/project/dto/project-response.dto.ts +++ b/src/routes/project/dto/project-response.dto.ts @@ -6,19 +6,19 @@ export class UserBasicInfoDto { example: 1, description: "The unique identifier of the user" }) - id!: number; + id!: number; @ApiProperty({ example: "john_doe", description: "The username" }) - username!: string; + username!: string; @ApiProperty({ example: "john.doe@example.com", description: "The email address" }) - email!: string; + email!: string; } export class ProjectResponseDto { @@ -26,33 +26,33 @@ export class ProjectResponseDto { example: 1, description: "The unique identifier of the project" }) - id!: number; + id!: number; @ApiProperty({ description: "The name of the project", example: "MySuperVideoGame" }) - name!: string; + name!: string; @ApiProperty({ description: "A short description of the project", example: "A 2D platformer game with pixel art graphics" }) - shortDesc!: string; + shortDesc!: string; @ApiProperty({ description: "A detailed description of the project", example: "This game features multiple levels, power-ups, and boss fights.", nullable: true }) - longDesc?: string | null; + longDesc?: string | null; @ApiProperty({ description: "URL to the project icon", example: "https://example.com/icons/MySuperVideoGame.png", nullable: true }) - iconUrl?: string | null; + iconUrl?: string | null; @ApiProperty({ description: "The current status of the project", @@ -60,7 +60,7 @@ export class ProjectResponseDto { example: ProjectStatus.IN_PROGRESS, nullable: true }) - status?: ProjectStatus | null; + status?: ProjectStatus | null; @ApiProperty({ description: "The monetization strategy for this project", @@ -68,59 +68,59 @@ export class ProjectResponseDto { example: MonetizationType.NONE, nullable: true }) - monetization?: MonetizationType | null; + monetization?: MonetizationType | null; @ApiProperty({ example: 99.99, description: "The price of the project, if applicable", nullable: true }) - price?: number | null; + price?: number | null; @ApiProperty({ example: 1, description: "The ID of the user who owns this project" }) - userId!: number; + userId!: number; @ApiProperty({ example: "2023-04-15T12:00:00Z", description: "The date and time when the project was created" }) - createdAt!: Date; + createdAt!: Date; @ApiProperty({ example: 123, description: "The number of unique players who have interacted with this project" }) - uniquePlayers!: number; + uniquePlayers!: number; @ApiProperty({ example: 42, description: "The number of currently active players in this project" }) - activePlayers!: number; + activePlayers!: number; @ApiProperty({ example: 87, description: "The number of likes received by the project" }) - likes!: number; + likes!: number; } -export class ProjectWithRelationsResponseDto extends ProjectResponseDto { +export class ProjectExResponseDto extends ProjectResponseDto { @ApiProperty({ description: "The users collaborating on this project", type: [UserBasicInfoDto] }) - collaborators!: UserBasicInfoDto[]; + collaborators!: UserBasicInfoDto[]; @ApiProperty({ description: "The creator of this project", type: UserBasicInfoDto }) - creator!: UserBasicInfoDto; + creator!: UserBasicInfoDto; } export class CdnUrlResponseDto { @@ -128,14 +128,14 @@ export class CdnUrlResponseDto { example: "https://cdn.example.com/files/project-123?signature=abc123", description: "The signed CDN URL for accessing the project file" }) - url!: string; + url!: string; } export class SignedUrlResponseDto { @ApiProperty({ example: "https://cdn.example.com/files/project-123?Expires=1640995200&Signature=abc123", - description: "The signed Edge URL for accessing the protected file" + description: "The signed CloudFront URL for accessing the protected file" }) - signedUrl!: string; + signedUrl!: string; } diff --git a/src/routes/project/dto/update-project.dto.ts b/src/routes/project/dto/update-project.dto.ts index 9681e16..756a4b6 100644 --- a/src/routes/project/dto/update-project.dto.ts +++ b/src/routes/project/dto/update-project.dto.ts @@ -15,14 +15,14 @@ export class UpdateProjectDto { example: "MySuperVideoGame" }) @IsString() - name!: string; + name!: string; @ApiProperty({ description: "A short description of the project", example: "A 2D platformer game with pixel art graphics" }) @IsString() - shortDesc!: string; + shortDesc!: string; @ApiProperty({ description: "A detailed description of the project", @@ -30,7 +30,7 @@ export class UpdateProjectDto { required: false }) @IsString() - longDesc?: string | null; + longDesc?: string | null; @ApiProperty({ description: "URL to the project icon", @@ -39,7 +39,7 @@ export class UpdateProjectDto { }) @IsUrl() @IsOptional() - iconUrl?: string; + iconUrl?: string; @ApiProperty({ description: "Project status", @@ -49,7 +49,7 @@ export class UpdateProjectDto { }) @IsEnum(ProjectStatus) @IsOptional() - status?: ProjectStatus; + status?: ProjectStatus; @ApiProperty({ description: "Monetization type", @@ -59,7 +59,7 @@ export class UpdateProjectDto { }) @IsEnum(MonetizationType) @IsOptional() - monetization?: MonetizationType; + monetization?: MonetizationType; @ApiProperty({ description: "The price of the project", @@ -69,5 +69,5 @@ export class UpdateProjectDto { @IsNumber() @Min(0) @IsOptional() - price?: number; + price?: number; } diff --git a/src/routes/project/entities/project.entity.ts b/src/routes/project/entities/project.entity.ts index 237722a..ba61192 100644 --- a/src/routes/project/entities/project.entity.ts +++ b/src/routes/project/entities/project.entity.ts @@ -5,84 +5,84 @@ export class Project { example: 1, description: "The unique identifier of the project" }) - id!: number; + id!: number; @ApiProperty({ description: "The name of the project", example: "MySuperVideoGame" }) - name!: string; + name!: string; @ApiProperty({ description: "A short description of the project", example: "A 2D platformer game with pixel art graphics" }) - shortDesc!: string; + shortDesc!: string; @ApiProperty({ description: "A detailed description of the project", example: "This game features multiple levels, power-ups, and boss fights." }) - longDesc?: string; + longDesc?: string; @ApiProperty({ description: "URL to the project icon", example: "https://example.com/icons/MySuperVideoGame.png", required: false }) - iconUrl?: string; + iconUrl?: string; @ApiProperty({ description: "The current status of the project", example: "IN_PROGRESS", enum: ["IN_PROGRESS", "COMPLETED", "ARCHIVED"] }) - status!: string; + status!: string; @ApiProperty({ description: "The monetization strategy for this project", example: "NONE", enum: ["NONE", "ADS", "PAID"] }) - monetization!: string; + monetization!: string; @ApiProperty({ example: 99.99, description: "The price of the project, if applicable", required: false }) - price?: number; + price?: number; @ApiProperty({ example: 1, description: "The ID of the user who owns this project" }) - userId!: number; + userId!: number; @ApiProperty({ example: "2023-04-15T12:00:00Z", description: "The date and time when the project was created" }) - createdAt!: Date; + createdAt!: Date; @ApiProperty({ example: 123, description: "The number of unique players who have interacted with this project" }) - uniquePlayers!: number; + uniquePlayers!: number; @ApiProperty({ example: 42, description: "The number of currently active players in this project" }) - activePlayers!: number; + activePlayers!: number; @ApiProperty({ example: 87, description: "The number of likes received by the project" }) - likes!: number; + likes!: number; // Relations @@ -90,24 +90,24 @@ export class Project { description: "The users collaborating on this project", type: () => [Number] }) - collaborators!: number[]; + collaborators!: number[]; @ApiProperty({ description: "Comments associated with this project", type: () => [Number] }) - comments!: number[]; + comments!: number[]; @ApiProperty({ description: "Game sessions associated with this project", type: () => [Number] }) - gameSessions!: number[]; + gameSessions!: number[]; @ApiProperty({ description: "Work session associated with this project", type: () => Number, required: false }) - workSession?: number; + workSession?: number; } diff --git a/src/routes/project/project.controller.spec.ts b/src/routes/project/project.controller.spec.ts index dd4da71..466ce1b 100644 --- a/src/routes/project/project.controller.spec.ts +++ b/src/routes/project/project.controller.spec.ts @@ -1,17 +1,53 @@ import { Test, TestingModule } from "@nestjs/testing"; import { ProjectController } from "./project.controller"; import { ProjectService } from "./project.service"; -import { PrismaModule } from "@prisma/prisma.module"; -import { S3Module } from "@s3/s3.module"; +import { PrismaService } from "@ourPrisma/prisma.service"; +import { ConfigService } from "@nestjs/config"; +import { S3Service } from "@s3/s3.service"; +import { CloudfrontService } from "src/routes/s3/edge.service"; describe("ProjectController", () => { let controller: ProjectController; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [PrismaModule, S3Module], controllers: [ProjectController], - providers: [ProjectService] + providers: [ + ProjectService, + { + provide: PrismaService, + useValue: { + project: {}, + user: {}, + workSession: {}, + $connect: jest.fn(), + $disconnect: jest.fn() + } + }, + { + provide: ConfigService, + useValue: { + get: jest.fn((key: string) => { + if (key === "S3_ENDPOINT") return "https://s3.fr-par.scw.cloud"; + if (key === "S3_REGION") return "fr-par"; + if (key === "S3_ACCESS_KEY_ID") return "test-key"; + if (key === "S3_SECRET_ACCESS_KEY") return "test-secret"; + if (key === "S3_MAX_AUTO_HISTORY_VERSION") return "5"; + if (key === "S3_AUTO_HISTORY_DELAY") return "10"; + if (key === "S3_MAX_CHECKPOINTS") return "5"; + return undefined; + }) + } + }, + { + provide: S3Service, + useValue: {} + }, + { + provide: CloudfrontService, + useValue: {} + } + ] }).compile(); controller = module.get(ProjectController); diff --git a/src/routes/project/project.controller.ts b/src/routes/project/project.controller.ts index e3f6ef9..5cb81fd 100644 --- a/src/routes/project/project.controller.ts +++ b/src/routes/project/project.controller.ts @@ -21,9 +21,9 @@ import { } from "@nestjs/common"; import { FileInterceptor } from "@nestjs/platform-express"; import { Response } from "express"; -import { ProjectSave, ProjectService } from "@projects/project.service"; -import { CreateProjectDto } from "@projects/dto/create-project.dto"; -import { UpdateProjectDto } from "@projects/dto/update-project.dto"; +import { ProjectSave, ProjectService } from "@project/project.service"; +import { CreateProjectDto } from "@project/dto/create-project.dto"; +import { UpdateProjectDto } from "@project/dto/update-project.dto"; import { JwtAuthGuard } from "@auth/guards/jwt-auth.guard"; import { ProjectCollaboratorGuard, @@ -41,18 +41,18 @@ import { import { AddCollaboratorDto, RemoveCollaboratorDto -} from "@projects/dto/collaborator-project.dto"; +} from "@project/dto/collaborator-project.dto"; import { Request } from "express"; import { UserDto } from "@auth/dto/user.dto"; import { Project } from "@prisma/client"; import { ProjectResponseDto, - ProjectWithRelationsResponseDto, + ProjectExResponseDto, SignedUrlResponseDto } from "./dto/project-response.dto"; import { S3DownloadException } from "@s3/s3.error"; import { S3Service } from "@s3/s3.service"; -import { CloudfrontService } from "@s3/cloudfront.service"; +import { CloudfrontService } from "src/routes/s3/edge.service"; import { SignedCdnResourceDto } from "@common/dto/signed-cdn-resource.dto"; interface RequestWithUser extends Request { @@ -71,7 +71,6 @@ export class ProjectController { ) {} private readonly logger = new Logger(ProjectController.name); - private readonly sessionCookieTimeout = 600; @Get("releases") @ApiOperation({ summary: "Get all released projects" }) @@ -103,7 +102,8 @@ export class ProjectController { @ApiParam({ name: "id", type: "string" }) @ApiResponse({ status: 200, - description: "Project release file" + description: "Project release file", + content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } }, }) async getReleaseContent( @Param("id") id: string, @@ -165,7 +165,7 @@ export class ProjectController { status: 200, description: "A JSON array of projects with collaborators and creator information", - type: [ProjectWithRelationsResponseDto] + type: [ProjectExResponseDto] }) @ApiResponse({ status: 500, description: "Internal server error" }) async findAll(@Req() request: RequestWithUser): Promise { @@ -274,7 +274,7 @@ export class ProjectController { @ApiResponse({ status: 200, description: "Updated project object with collaborators", - type: ProjectWithRelationsResponseDto + type: ProjectExResponseDto }) @ApiResponse({ status: 400, @@ -329,7 +329,7 @@ export class ProjectController { @ApiResponse({ status: 200, description: "Updated project object with collaborators", - type: ProjectWithRelationsResponseDto + type: ProjectExResponseDto }) @ApiResponse({ status: 400, @@ -475,46 +475,10 @@ export class ProjectController { res.status(HttpStatus.NOT_FOUND).json({ message: "Project image not found" }); return; } - - const resourceUrl = this.cloudfrontService.getCDNUrl(key); - const cookies = this.cloudfrontService.createSignedCookies( - resourceUrl, - this.sessionCookieTimeout - ); - - const cookieOptions = { - httpOnly: true, - secure: true, - path: "/", - domain: this.cloudfrontService.getCookieDomain(), - sameSite: "lax" as const, - maxAge: 60 * 60 * 1000 - }; - - if (cookies["CloudFront-Expires"]) { - res.cookie("CloudFront-Expires", cookies["CloudFront-Expires"], cookieOptions); - } - if (cookies["CloudFront-Signature"]) { - res.cookie( - "CloudFront-Signature", - cookies["CloudFront-Signature"], - cookieOptions - ); - } - if (cookies["CloudFront-Key-Pair-Id"]) { - res.cookie( - "CloudFront-Key-Pair-Id", - cookies["CloudFront-Key-Pair-Id"], - cookieOptions - ); - } - if (cookies["CloudFront-Policy"]) { - res.cookie("CloudFront-Policy", cookies["CloudFront-Policy"], cookieOptions); - } + const resourceUrl = this.cloudfrontService.generateSignedUrl(key); res.status(HttpStatus.OK).json({ - resourceUrl, - cookies + url: resourceUrl }); } @@ -522,12 +486,12 @@ export class ProjectController { @UseGuards(ProjectCollaboratorGuard) @ApiOperation({ summary: "Fetch project's content" }) @ApiParam({ name: "id", type: "string" }) - @ApiResponse({ status: 200, description: "File fetched successfully" }) + @ApiResponse({ status: 200, description: "File fetched successfully", content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } } }) @ApiResponse({ status: 403, description: "Forbidden" }) @ApiResponse({ status: 404, description: "File not found" }) - async fetchProjectContent(@Param("id") id: string, @Res() res: Response): Promise { + async fetchProjectContent(@Param("id") id: number, @Res() res: Response): Promise { try { - const file = await this.projectService.fetchLastVersion(Number(id)); + const file = await this.projectService.fetchLastVersion(id); res.set({ "Content-Type": file.contentType, @@ -672,7 +636,8 @@ export class ProjectController { @ApiParam({ name: "version", type: "string" }) @ApiResponse({ status: 200, - description: "Project version retrieved successfully" + description: "Project version retrieved successfully", + content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } }, }) @ApiResponse({ status: 403, description: "Forbidden" }) async getVersion( @@ -720,7 +685,8 @@ export class ProjectController { @ApiParam({ name: "checkpoint", type: "string" }) @ApiResponse({ status: 200, - description: "Project checkpoint retrieved successfully" + description: "Project checkpoint retrieved successfully", + content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } }, }) @ApiResponse({ status: 403, description: "Forbidden" }) async getCheckpoint( diff --git a/src/routes/project/project.error.ts b/src/routes/project/project.error.ts new file mode 100644 index 0000000..6601f47 --- /dev/null +++ b/src/routes/project/project.error.ts @@ -0,0 +1,6 @@ +export class ProjectNotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +}; diff --git a/src/routes/project/project.module.ts b/src/routes/project/project.module.ts index a39feab..8f05b35 100644 --- a/src/routes/project/project.module.ts +++ b/src/routes/project/project.module.ts @@ -1,7 +1,7 @@ import { Module } from "@nestjs/common"; import { ProjectController } from "./project.controller"; import { ProjectService } from "./project.service"; -import { PrismaModule } from "@prisma/prisma.module"; +import { PrismaModule } from "@ourPrisma/prisma.module"; import { S3Module } from "@s3/s3.module"; @Module({ diff --git a/src/routes/project/project.service.spec.ts b/src/routes/project/project.service.spec.ts index bf69ced..889a00a 100644 --- a/src/routes/project/project.service.spec.ts +++ b/src/routes/project/project.service.spec.ts @@ -1,13 +1,15 @@ import { Test, TestingModule } from "@nestjs/testing"; import { ProjectService } from "./project.service"; import { S3Service } from "@s3/s3.service"; -import { PrismaService } from "@prisma/prisma.service"; +import { PrismaService } from "@ourPrisma/prisma.service"; +import { ConfigService } from "@nestjs/config"; import { BadRequestException, ForbiddenException, InternalServerErrorException, NotFoundException } from "@nestjs/common"; + import { CREATOR_SELECT, COLLABORATOR_SELECT } from "./project.service"; import { ProjectStatus, MonetizationType, Prisma } from "@prisma/client"; @@ -37,6 +39,9 @@ const mockProjects: ProjectWithCreatorAndCollaborators[] = [ uniquePlayers: 0, activePlayers: 0, likes: 0, + contentKey: "keyA", + contentExtension: ".zip", + contentUploadedAt: new Date(), creator: { id: 42, email: "creator@example.com", @@ -64,6 +69,9 @@ const mockProjects: ProjectWithCreatorAndCollaborators[] = [ uniquePlayers: 10897, activePlayers: 600, likes: 187, + contentKey: "keyB", + contentExtension: ".zip", + contentUploadedAt: new Date(), creator: { id: 42, email: "creator@example.com", @@ -100,7 +108,18 @@ describe("ProjectService", () => { }; const s3ServiceMock = { - deleteFile: jest.fn() + deleteFile: jest.fn(), + listObjects: jest.fn(), + deleteFiles: jest.fn() + }; + + const configServiceMock = { + get: jest.fn((key: string) => { + if (key === "S3_MAX_AUTO_HISTORY_VERSION") return "5"; + if (key === "S3_AUTO_HISTORY_DELAY") return "10"; + if (key === "S3_MAX_CHECKPOINTS") return "5"; + return undefined; + }) }; beforeEach(async () => { @@ -114,6 +133,10 @@ describe("ProjectService", () => { { provide: S3Service, useValue: s3ServiceMock + }, + { + provide: ConfigService, + useValue: configServiceMock } ] }).compile(); @@ -160,7 +183,23 @@ describe("ProjectService", () => { const result = await service.findOne(projectId); expect(prismaMock.project.findUnique).toHaveBeenCalledWith({ - where: { id: projectId } + where: { id: projectId }, + include: { + creator: { + select: { + id: true, + username: true, + email: true + } + }, + collaborators: { + select: { + id: true, + username: true, + email: true + } + } + } }); expect(result).toEqual(mockProjects[0]); }); @@ -175,7 +214,23 @@ describe("ProjectService", () => { ); expect(prismaMock.project.findUnique).toHaveBeenCalledWith({ - where: { id: projectId } + where: { id: projectId }, + include: { + creator: { + select: { + id: true, + username: true, + email: true + } + }, + collaborators: { + select: { + id: true, + username: true, + email: true + } + } + } }); }); }); @@ -274,7 +329,23 @@ describe("ProjectService", () => { const result = await service.update(projectId, updateDto); expect(prismaMock.project.findUnique).toHaveBeenCalledWith({ - where: { id: projectId } + where: { id: projectId }, + include: { + creator: { + select: { + id: true, + username: true, + email: true + } + }, + collaborators: { + select: { + id: true, + username: true, + email: true + } + } + } }); expect(prismaMock.project.update).toHaveBeenCalledWith({ where: { id: projectId }, @@ -302,15 +373,33 @@ describe("ProjectService", () => { prismaMock.project.findUnique.mockResolvedValue(mockProjects[0]); prismaMock.project.delete.mockResolvedValue(mockProjects[0]); s3ServiceMock.deleteFile.mockResolvedValue(undefined); + s3ServiceMock.listObjects.mockResolvedValue([]); + s3ServiceMock.deleteFiles.mockResolvedValue(undefined); await service.remove(projectId); expect(prismaMock.project.findUnique).toHaveBeenCalledWith({ - where: { id: projectId } + where: { id: projectId }, + include: { + creator: { + select: { + id: true, + username: true, + email: true + } + }, + collaborators: { + select: { + id: true, + username: true, + email: true + } + } + } + }); + expect(s3ServiceMock.deleteFile).toHaveBeenCalledWith({ + key: `release/${projectId}` }); - expect(s3ServiceMock.deleteFile).toHaveBeenCalledWith( - projectId.toString() - ); expect(prismaMock.project.delete).toHaveBeenCalledWith({ where: { id: projectId } }); @@ -355,11 +444,27 @@ describe("ProjectService", () => { ); expect(prismaMock.project.findUnique).toHaveBeenCalledWith({ - where: { id: projectId } + where: { id: projectId }, + include: { + creator: { + select: { + id: true, + username: true, + email: true + } + }, + collaborators: { + select: { + id: true, + username: true, + email: true + } + } + } + }); + expect(s3ServiceMock.deleteFile).toHaveBeenCalledWith({ + key: `release/${projectId}` }); - expect(s3ServiceMock.deleteFile).toHaveBeenCalledWith( - projectId.toString() - ); }); }); @@ -381,7 +486,22 @@ describe("ProjectService", () => { }); expect(prismaMock.project.findUnique).toHaveBeenCalledWith({ where: { id: 1 }, - include: { collaborators: true } + include: { + creator: { + select: { + id: true, + username: true, + email: true + } + }, + collaborators: { + select: { + id: true, + username: true, + email: true + } + } + } }); expect(prismaMock.project.update).toHaveBeenCalledWith( expect.objectContaining({ @@ -425,7 +545,6 @@ describe("ProjectService", () => { describe("removeCollaborator", () => { const removeDto = { userId: 2 }; - const initiator = 1; it("should remove collaborator successfully", async () => { prismaMock.user.findUnique.mockResolvedValue({ id: 2 }); @@ -435,14 +554,29 @@ describe("ProjectService", () => { }); prismaMock.project.update.mockResolvedValue(mockProjects[0]); - const result = await service.removeCollaborator(1, initiator, removeDto); + const result = await service.removeCollaborator(1, removeDto); expect(prismaMock.user.findUnique).toHaveBeenCalledWith({ where: { id: removeDto.userId } }); expect(prismaMock.project.findUnique).toHaveBeenCalledWith({ where: { id: 1 }, - include: { collaborators: true } + include: { + creator: { + select: { + id: true, + username: true, + email: true + } + }, + collaborators: { + select: { + id: true, + username: true, + email: true + } + } + } }); expect(prismaMock.project.update).toHaveBeenCalledWith( expect.objectContaining({ @@ -458,7 +592,7 @@ describe("ProjectService", () => { prismaMock.user.findUnique.mockResolvedValue(null); await expect( - service.removeCollaborator(1, initiator, removeDto) + service.removeCollaborator(1, removeDto) ).rejects.toThrow(NotFoundException); }); @@ -467,19 +601,20 @@ describe("ProjectService", () => { prismaMock.project.findUnique.mockResolvedValue(null); await expect( - service.removeCollaborator(1, initiator, removeDto) + service.removeCollaborator(1, removeDto) ).rejects.toThrow(NotFoundException); }); it("should throw ForbiddenException if trying to remove creator", async () => { - prismaMock.user.findUnique.mockResolvedValue({ id: initiator }); + const creatorId = (mockProjects && mockProjects[0]) ? mockProjects[0].userId : 1; + prismaMock.user.findUnique.mockResolvedValue({ id: creatorId }); prismaMock.project.findUnique.mockResolvedValue({ ...mockProjects[0], - collaborators: [{ id: initiator }] + collaborators: [{ id: creatorId }] }); await expect( - service.removeCollaborator(1, initiator, { userId: initiator }) + service.removeCollaborator(1, { userId: creatorId }) ).rejects.toThrow(ForbiddenException); }); @@ -491,7 +626,7 @@ describe("ProjectService", () => { }); await expect( - service.removeCollaborator(1, initiator, removeDto) + service.removeCollaborator(1, removeDto) ).rejects.toThrow(BadRequestException); }); }); diff --git a/src/routes/project/project.service.ts b/src/routes/project/project.service.ts index fccf170..2bc27dd 100644 --- a/src/routes/project/project.service.ts +++ b/src/routes/project/project.service.ts @@ -6,7 +6,7 @@ import { InternalServerErrorException, NotFoundException } from "@nestjs/common"; -import { PrismaService } from "@prisma/prisma.service"; +import { PrismaService } from "@ourPrisma/prisma.service"; import { CreateProjectDto } from "./dto/create-project.dto"; import { UpdateProjectDto } from "./dto/update-project.dto"; import { @@ -30,7 +30,7 @@ export const COLLABORATOR_SELECT = { email: true }; -type ProjectWithRelations = Project & { +export type ProjectEx = Project & { collaborators: Array<{ id: number; username: string; email: string }>; creator: { id: number; username: string; email: string }; }; @@ -82,7 +82,7 @@ export class ProjectService { }); } - async findOne(id: number): Promise { + async findOne(id: number): Promise { const project = await this.prisma.project.findUnique({ where: { id }, include: { @@ -393,10 +393,12 @@ export class ProjectService { }); const file = await this.fetchLastVersion(projectId); + const releaseKey = `release/${projectId}`; await this.s3Service.uploadFile({ file: file, - keyName: `release/${projectId}` + keyName: releaseKey }); + await this.s3Service.setObjectPublicRead(releaseKey); } async unpublish(projectId: number): Promise { diff --git a/src/routes/s3/bucket.service.spec.ts b/src/routes/s3/bucket.service.spec.ts deleted file mode 100644 index 1c4e3f6..0000000 --- a/src/routes/s3/bucket.service.spec.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { BucketService } from "./bucket.service"; -import { BucketPolicy } from "./s3.interface"; -import { - S3Client, - ListBucketsCommand, - DeleteBucketCommand, - CreateBucketCommand, - PutBucketPolicyCommand -} from "@aws-sdk/client-s3"; -import { - S3ListBucketsException, - S3DeleteBucketException, - S3CreateBucketException, - S3ApplyPolicyException, - BucketResolutionException -} from "./s3.error"; -import { ConfigService } from "@nestjs/config"; - -describe("BucketService", () => { - let bucketService: BucketService; - let mockS3: S3Client & { send: jest.Mock }; - let mockConfigService: ConfigService; - - beforeEach(() => { - mockS3 = { - send: jest.fn() - } as unknown as S3Client & { send: jest.Mock }; - - mockConfigService = { - get: jest.fn((key: string) => { - if (key === "S3_BUCKET_NAME") return undefined; - return undefined; - }) - } as unknown as ConfigService; - - bucketService = new BucketService(mockS3, mockConfigService); - }); - - describe("resolveBucket", () => { - it("returns provided bucket name", () => { - const result = ( - bucketService as unknown as { - resolveBucket: (bucket: string) => string; - } - ).resolveBucket("my-bucket"); - expect(result).toBe("my-bucket"); - }); - - it("throws when no bucket provided and no default bucket", () => { - expect(() => - ( - bucketService as unknown as { resolveBucket: () => void } - ).resolveBucket() - ).toThrow(BucketResolutionException); - }); - }); - - describe("listBuckets", () => { - it("returns buckets", async () => { - mockS3.send.mockResolvedValueOnce({ Buckets: [{ Name: "bucket1" }] }); - - const result = await bucketService.listBuckets(); - - expect(result).toEqual([{ Name: "bucket1" }]); - expect(mockS3.send).toHaveBeenCalledWith(expect.any(ListBucketsCommand)); - }); - - it("returns empty array when no buckets", async () => { - mockS3.send.mockResolvedValueOnce({}); - - const result = await bucketService.listBuckets(); - - expect(result).toEqual([]); - }); - - it("throws S3ListBucketsException on error", async () => { - const err = new Error("Access denied"); - mockS3.send.mockRejectedValueOnce(err); - - await expect(bucketService.listBuckets()).rejects.toThrow( - S3ListBucketsException - ); - }); - - it("throws S3ListBucketsException on unknown error", async () => { - mockS3.send.mockRejectedValueOnce("something bad" as unknown); - - await expect(bucketService.listBuckets()).rejects.toThrow( - "An unknown error occurred while listing buckets." - ); - }); - }); - - describe("deleteBucket", () => { - it("deletes a bucket", async () => { - mockS3.send.mockResolvedValueOnce({}); - await bucketService.deleteBucket("my-bucket"); - expect(mockS3.send).toHaveBeenCalledWith(expect.any(DeleteBucketCommand)); - }); - - it("throws S3DeleteBucketException on error", async () => { - mockS3.send.mockRejectedValueOnce(new Error("fail")); - await expect(bucketService.deleteBucket("my-bucket")).rejects.toThrow( - S3DeleteBucketException - ); - }); - }); - - describe("createBucket", () => { - it("creates a bucket", async () => { - mockS3.send.mockResolvedValueOnce({}); - await bucketService.createBucket("my-bucket"); - expect(mockS3.send).toHaveBeenCalledWith(expect.any(CreateBucketCommand)); - }); - - it("throws S3CreateBucketException on error", async () => { - mockS3.send.mockRejectedValueOnce(new Error("fail")); - await expect(bucketService.createBucket("my-bucket")).rejects.toThrow( - S3CreateBucketException - ); - }); - }); - - describe("generateBucketPolicy", () => { - it("generates default policy", () => { - const [statement] = - bucketService.generateBucketPolicy("my-bucket").Statement; - if (!statement) throw new Error("Statement is undefined"); - - expect(statement.Resource).toBe("arn:aws:s3:::my-bucket/*"); - expect(statement.Effect).toBe("Allow"); - expect(statement.Principal).toBe("*"); - expect(statement.Action).toEqual(["s3:GetObject"]); - }); - - it("generates policy with custom values", () => { - const [statement] = bucketService.generateBucketPolicy( - "my-bucket", - ["s3:PutObject"], - "Deny", - "123", - "prefix/*" - ).Statement; - - if (!statement) throw new Error("Statement is undefined"); - expect(statement.Effect).toBe("Deny"); - expect(statement.Principal).toEqual({ AWS: "123" }); - expect(statement.Resource).toBe("arn:aws:s3:::my-bucket/prefix/*"); - expect(statement.Action).toEqual(["s3:PutObject"]); - }); - }); - - describe("applyBucketPolicy", () => { - it("applies a bucket policy (object)", async () => { - mockS3.send.mockResolvedValueOnce({}); - const policy = bucketService.generateBucketPolicy("my-bucket"); - - await bucketService.applyBucketPolicy(policy, "my-bucket"); - - expect(mockS3.send).toHaveBeenCalledWith( - expect.any(PutBucketPolicyCommand) - ); - }); - - it("throws S3ApplyPolicyException on error", async () => { - mockS3.send.mockRejectedValueOnce(new Error("fail")); - const policy = bucketService.generateBucketPolicy("my-bucket"); - - await expect( - bucketService.applyBucketPolicy(policy, "my-bucket") - ).rejects.toThrow(S3ApplyPolicyException); - }); - - it("applies a bucket policy passed as a string", async () => { - mockS3.send.mockResolvedValueOnce({}); - - const policyString = JSON.stringify( - bucketService.generateBucketPolicy("my-bucket") - ); - - await bucketService.applyBucketPolicy( - policyString as unknown as BucketPolicy, - "my-bucket" - ); - - expect(mockS3.send).toHaveBeenCalledWith( - expect.any(PutBucketPolicyCommand) - ); - }); - }); - - describe("applyPublicReadPrefixPolicy", () => { - it("applies policy for release/* by default", async () => { - mockS3.send.mockResolvedValueOnce({}); - - await bucketService.applyPublicReadPrefixPolicy(undefined, "my-bucket"); - - expect(mockS3.send).toHaveBeenCalledWith( - expect.any(PutBucketPolicyCommand) - ); - }); - - it("applies policy for a custom prefix", async () => { - mockS3.send.mockResolvedValueOnce({}); - - await bucketService.applyPublicReadPrefixPolicy("release/special/*", "my-bucket"); - - expect(mockS3.send).toHaveBeenCalledWith( - expect.any(PutBucketPolicyCommand) - ); - }); - }); -}); diff --git a/src/routes/s3/bucket.service.ts b/src/routes/s3/bucket.service.ts deleted file mode 100644 index 4ba8103..0000000 --- a/src/routes/s3/bucket.service.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { Inject, Injectable } from "@nestjs/common"; -import { - S3Client, - ListBucketsCommand, - DeleteBucketCommand, - CreateBucketCommand, - PutBucketPolicyCommand -} from "@aws-sdk/client-s3"; -import { BucketPolicy } from "./s3.interface"; -import { - BucketResolutionException, - S3ListBucketsException, - S3DeleteBucketException, - S3CreateBucketException, - S3ApplyPolicyException -} from "./s3.error"; -import { Bucket } from "@aws-sdk/client-s3"; -import { - ListBucketsCommandInput, - DeleteBucketCommandInput, - CreateBucketCommandInput, - PutBucketPolicyCommandInput -} from "@aws-sdk/client-s3"; -import { BucketPolicyStatement } from "./s3.interface"; -import { ConfigService } from "@nestjs/config"; - -@Injectable() -export class BucketService { - private readonly defaultBucket: string | undefined; - - constructor( - private readonly s3: S3Client, - @Inject(ConfigService) private readonly configService: ConfigService - ) { - this.defaultBucket = this.configService.get("S3_BUCKET_NAME"); - } - - private resolveBucket(bucketName?: string): string { - const resolved = bucketName || this.defaultBucket; - if (!resolved) { - throw new BucketResolutionException( - "No bucket name provided and no default bucket configured." - ); - } - return resolved; - } - - async listBuckets(): Promise { - try { - const input: ListBucketsCommandInput = {}; - const command = new ListBucketsCommand(input); - const result = await this.s3.send(command); - return result.Buckets || []; - } catch (error: unknown) { - if (error instanceof Error) { - throw new S3ListBucketsException(error.message, { cause: error }); - } else { - throw new S3ListBucketsException( - "An unknown error occurred while listing buckets." - ); - } - } - } - - async deleteBucket(bucketName?: string): Promise { - const resolvedBucketName = this.resolveBucket(bucketName); - try { - const input: DeleteBucketCommandInput = { Bucket: resolvedBucketName }; - const command = new DeleteBucketCommand(input); - await this.s3.send(command); - } catch (error) { - throw new S3DeleteBucketException(resolvedBucketName, error); - } - } - - async createBucket(bucketName?: string): Promise { - const resolvedBucketName = this.resolveBucket(bucketName); - try { - const input: CreateBucketCommandInput = { - Bucket: resolvedBucketName - }; - const command = new CreateBucketCommand(input); - await this.s3.send(command); - } catch (error) { - throw new S3CreateBucketException(resolvedBucketName, error); - } - } - - generateBucketPolicy( - bucketName?: string, - actions: string[] = ["s3:GetObject"], - effect = "Allow", - principal = "*", - prefix = "*" - ): BucketPolicy { - const resolvedBucketName = this.resolveBucket(bucketName); - - const statement: BucketPolicyStatement = { - Sid: "BucketPolicy", - Effect: effect, - Principal: principal === "*" ? "*" : { AWS: principal }, - Action: actions, - Resource: - prefix === "*" - ? `arn:aws:s3:::${resolvedBucketName}/*` - : `arn:aws:s3:::${resolvedBucketName}/${prefix}` - }; - - return { - Version: "2012-10-17", - Statement: [statement] - }; - } - - async applyBucketPolicy( - policy: BucketPolicy, - bucketName?: string - ): Promise { - const resolvedBucketName = this.resolveBucket(bucketName); - try { - const input: PutBucketPolicyCommandInput = { - Bucket: resolvedBucketName, - Policy: typeof policy === "string" ? policy : JSON.stringify(policy) - }; - - const command = new PutBucketPolicyCommand(input); - await this.s3.send(command); - } catch (error) { - throw new S3ApplyPolicyException(resolvedBucketName, error); - } - } - - async applyPublicReadPrefixPolicy( - prefix: string = "release/*", - bucketName?: string - ): Promise { - const normalizedPrefix = prefix.trim() || "release/*"; - const policy = this.generateBucketPolicy( - bucketName, - ["s3:GetObject"], - "Allow", - "*", - normalizedPrefix - ); - - await this.applyBucketPolicy(policy, bucketName); - } -} diff --git a/src/routes/s3/cloudfront.service.spec.ts b/src/routes/s3/cloudfront.service.spec.ts deleted file mode 100644 index 4ea2822..0000000 --- a/src/routes/s3/cloudfront.service.spec.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { Test, TestingModule } from "@nestjs/testing"; -import { CloudfrontService } from "./cloudfront.service"; -import { ConfigService } from "@nestjs/config"; -import * as fs from "fs"; -import { - getSignedUrl as getSignedCFUrl, - getSignedCookies -} from "@aws-sdk/cloudfront-signer"; -import { MissingEnvVarError } from "@auth/auth.error"; -import * as cloudfrontSigner from "@aws-sdk/cloudfront-signer"; - -jest.mock("fs"); -jest.mock("@aws-sdk/cloudfront-signer"); - -describe("CloudfrontService", () => { - let cloudfrontService: CloudfrontService; - let configService: ConfigService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - CloudfrontService, - { - provide: ConfigService, - useValue: { - get: jest.fn() - } - } - ] - }).compile(); - - cloudfrontService = module.get(CloudfrontService); - configService = module.get(ConfigService); - (fs.existsSync as jest.Mock).mockReturnValue(true); - }); - - describe("generateSignedUrl", () => { - beforeEach(() => { - (configService.get as jest.Mock).mockImplementation((key: string) => { - switch (key) { - case "EDGE_ENDPOINT": - return "https://cdn.example.com"; - case "EDGE_KEY_PAIR_ID": - return "KEYPAIRID"; - case "EDGE_PRIVATE_KEY_PATH": - return "/fake/path.pem"; - default: - return undefined; - } - }); - - (fs.readFileSync as jest.Mock).mockReturnValue("FAKE_PRIVATE_KEY"); - (getSignedCFUrl as jest.Mock).mockReturnValue("SIGNED_URL"); - }); - - it("generates signed URL correctly", () => { - const url = cloudfrontService.generateSignedUrl("file.txt"); - expect(url).toBe("SIGNED_URL"); - expect(fs.readFileSync).toHaveBeenCalledWith("/fake/path.pem", "utf8"); - expect(getSignedCFUrl).toHaveBeenCalledWith( - expect.objectContaining({ - url: "https://cdn.example.com/file.txt", - keyPairId: "KEYPAIRID", - privateKey: "FAKE_PRIVATE_KEY" - }) - ); - }); - - it("throws MissingEnvVarError if EDGE_ENDPOINT missing", () => { - (configService.get as jest.Mock).mockImplementation((key: string) => { - if (key === "EDGE_KEY_PAIR_ID") return "KEYPAIRID"; - if (key === "EDGE_PRIVATE_KEY_PATH") return "/fake/path.pem"; - return undefined; - }); - - expect(() => cloudfrontService.generateSignedUrl("file.txt")).toThrow( - MissingEnvVarError - ); - }); - - it("supports legacy CLOUDFRONT_* variables as fallback", () => { - (configService.get as jest.Mock).mockImplementation((key: string) => { - switch (key) { - case "CDN_URL": - return "cdn.example.com"; - case "CLOUDFRONT_KEY_PAIR_ID": - return "LEGACY_KEYPAIR"; - case "CLOUDFRONT_PRIVATE_KEY_PATH": - return "/legacy/path.pem"; - default: - return undefined; - } - }); - - (fs.readFileSync as jest.Mock).mockReturnValue("LEGACY_PRIVATE_KEY"); - (getSignedCFUrl as jest.Mock).mockReturnValue("SIGNED_LEGACY_URL"); - - const url = cloudfrontService.generateSignedUrl("folder/file.txt"); - expect(url).toBe("SIGNED_LEGACY_URL"); - expect(getSignedCFUrl).toHaveBeenCalledWith( - expect.objectContaining({ - url: "https://cdn.example.com/folder/file.txt", - keyPairId: "LEGACY_KEYPAIR", - privateKey: "LEGACY_PRIVATE_KEY" - }) - ); - }); - }); - - describe("generateSignedUrl - missing env vars", () => { - it("throws if EDGE_ENDPOINT is missing", () => { - (configService.get as jest.Mock).mockImplementation((key: string) => { - if (key === "EDGE_KEY_PAIR_ID") return "KEYPAIRID"; - if (key === "EDGE_PRIVATE_KEY_PATH") return "/fake/path.pem"; - return undefined; - }); - - expect(() => cloudfrontService.generateSignedUrl("file.txt")).toThrow( - MissingEnvVarError - ); - }); - - it("returns unsigned URL if EDGE_KEY_PAIR_ID is missing", () => { - (configService.get as jest.Mock).mockImplementation((key: string) => { - if (key === "EDGE_ENDPOINT") return "cdn.example.com"; - if (key === "EDGE_PRIVATE_KEY_PATH") return "/fake/path.pem"; - return undefined; - }); - - expect(cloudfrontService.generateSignedUrl("file.txt")).toBe( - "https://cdn.example.com/file.txt" - ); - }); - - it("returns unsigned URL if EDGE_PRIVATE_KEY_PATH is missing", () => { - (configService.get as jest.Mock).mockImplementation((key: string) => { - if (key === "EDGE_ENDPOINT") return "cdn.example.com"; - if (key === "EDGE_KEY_PAIR_ID") return "KEYPAIRID"; - return undefined; - }); - - expect(cloudfrontService.generateSignedUrl("file.txt")).toBe( - "https://cdn.example.com/file.txt" - ); - }); - }); - - describe("generateSignedCookies", () => { - beforeEach(() => { - (configService.get as jest.Mock).mockImplementation((key: string) => { - switch (key) { - case "EDGE_ENDPOINT": - return "https://cdn.example.com"; - case "EDGE_KEY_PAIR_ID": - return "KEYPAIRID"; - case "EDGE_PRIVATE_KEY_PATH": - return "/fake/path.pem"; - default: - return undefined; - } - }); - - (fs.readFileSync as jest.Mock).mockReturnValue("FAKE_PRIVATE_KEY"); - (getSignedCookies as jest.Mock).mockReturnValue({ - "CloudFront-Policy": "policy", - "CloudFront-Signature": "signature", - "CloudFront-Key-Pair-Id": "KEYPAIRID" - }); - }); - - it("returns signed cookies with expiration", () => { - const cookies = cloudfrontService.generateSignedCookies(); - expect(cookies).toHaveProperty("CloudFront-Policy"); - expect(cookies).toHaveProperty("CloudFront-Signature"); - expect(cookies).toHaveProperty("CloudFront-Key-Pair-Id"); - }); - }); - - describe("generateSignedCookies - missing env vars", () => { - it("throws if EDGE_ENDPOINT is missing", () => { - (configService.get as jest.Mock).mockImplementation((key: string) => { - if (key === "EDGE_KEY_PAIR_ID") return "KEYPAIRID"; - if (key === "EDGE_PRIVATE_KEY_PATH") return "/fake/path.pem"; - return undefined; - }); - - expect(() => cloudfrontService.generateSignedCookies()).toThrow( - "EDGE_ENDPOINT" - ); - }); - - it("returns empty cookies if EDGE_KEY_PAIR_ID is missing", () => { - (configService.get as jest.Mock).mockImplementation((key: string) => { - if (key === "EDGE_ENDPOINT") return "cdn.example.com"; - if (key === "EDGE_PRIVATE_KEY_PATH") return "/fake/path.pem"; - return undefined; - }); - - expect(cloudfrontService.generateSignedCookies()).toEqual({}); - }); - - it("returns empty cookies if EDGE_PRIVATE_KEY_PATH is missing", () => { - (configService.get as jest.Mock).mockImplementation((key: string) => { - if (key === "EDGE_ENDPOINT") return "cdn.example.com"; - if (key === "EDGE_KEY_PAIR_ID") return "KEYPAIRID"; - return undefined; - }); - - expect(cloudfrontService.generateSignedCookies()).toEqual({}); - }); - }); - - describe("createSignedCookies - missing env vars", () => { - it("returns empty cookies if EDGE_KEY_PAIR_ID is missing", () => { - (configService.get as jest.Mock).mockImplementation((key: string) => { - if (key === "EDGE_PRIVATE_KEY_PATH") return "/fake/path.pem"; - return undefined; - }); - - expect(cloudfrontService.createSignedCookies("url", 3600)).toEqual({}); - }); - - it("returns empty cookies if EDGE_PRIVATE_KEY_PATH is missing", () => { - (configService.get as jest.Mock).mockImplementation((key: string) => { - if (key === "EDGE_KEY_PAIR_ID") return "KEYPAIRID"; - return undefined; - }); - - expect(cloudfrontService.createSignedCookies("url", 3600)).toEqual({}); - }); - - it("throws if signed cookies are incomplete", () => { - (cloudfrontSigner.getSignedCookies as jest.Mock).mockReturnValue({ - "CloudFront-Policy": "policy" - }); - - (configService.get as jest.Mock).mockImplementation((key: string) => { - if (key === "EDGE_KEY_PAIR_ID") return "KEYPAIRID"; - if (key === "EDGE_PRIVATE_KEY_PATH") return "/fake/path.pem"; - return undefined; - }); - - expect(() => cloudfrontService.createSignedCookies("url", 3600)).toThrow( - "Signed cookies are incomplete" - ); - }); - }); - - describe("createSignedCookies", () => { - beforeEach(() => { - (configService.get as jest.Mock).mockImplementation((key: string) => { - switch (key) { - case "EDGE_KEY_PAIR_ID": - return "KEYPAIRID"; - case "EDGE_PRIVATE_KEY_PATH": - return "/fake/path.pem"; - default: - return undefined; - } - }); - - (fs.readFileSync as jest.Mock).mockReturnValue("FAKE_PRIVATE_KEY"); - (getSignedCookies as jest.Mock).mockReturnValue({ - "CloudFront-Signature": "signature", - "CloudFront-Key-Pair-Id": "KEYPAIRID" - }); - }); - - it("creates signed cookies with expires", () => { - const cookies = cloudfrontService.createSignedCookies( - "https://cdn.example.com/file.txt", - 3600 - ); - expect(cookies).toHaveProperty("CloudFront-Signature"); - expect(cookies).toHaveProperty("CloudFront-Key-Pair-Id"); - expect(cookies).toHaveProperty("CloudFront-Expires"); - }); - - it("throws if signed cookies incomplete", () => { - (getSignedCookies as jest.Mock).mockReturnValue({}); - expect(() => - cloudfrontService.createSignedCookies( - "https://cdn.example.com/file.txt", - 3600 - ) - ).toThrow("Signed cookies are incomplete"); - }); - - it("returns empty cookies if env vars missing", () => { - (configService.get as jest.Mock).mockImplementation(() => undefined); - expect(cloudfrontService.createSignedCookies("url", 3600)).toEqual({}); - }); - }); - - describe("getCDNUrl", () => { - it("returns correctly encoded CDN URL", () => { - (configService.get as jest.Mock).mockImplementation((key: string) => { - if (key === "EDGE_ENDPOINT") return "https://cdn.example.com"; - return undefined; - }); - - const url = cloudfrontService.getCDNUrl("path/to/file.txt"); - expect(url).toBe("https://cdn.example.com/path/to/file.txt"); - }); - - it("extracts cookie domain from endpoint", () => { - (configService.get as jest.Mock).mockImplementation((key: string) => { - if (key === "EDGE_ENDPOINT") { - return "https://b9f865e8-4b05-48b0-abeb-7e850e58f081.svc.edge.scw.cloud"; - } - return undefined; - }); - - expect(cloudfrontService.getCookieDomain()).toBe( - "b9f865e8-4b05-48b0-abeb-7e850e58f081.svc.edge.scw.cloud" - ); - }); - }); -}); diff --git a/src/routes/s3/cloudfront.service.ts b/src/routes/s3/cloudfront.service.ts deleted file mode 100644 index 3b70e8d..0000000 --- a/src/routes/s3/cloudfront.service.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { Injectable, Logger } from "@nestjs/common"; -import { ConfigService } from "@nestjs/config"; -import * as fs from "fs"; -import { - getSignedCookies, - getSignedUrl as getSignedCFUrl, - CloudfrontSignedCookiesOutput -} from "@aws-sdk/cloudfront-signer"; -import { - BadEnvVarError, - MissingEnvVarError, - CloudfrontSignedCookiesException -} from "@auth/auth.error"; -import { CloudFrontPrivateKeyException } from "./s3.error"; - -@Injectable() -export class CloudfrontService { - private readonly logger = new Logger(CloudfrontService.name); - private privateKey: string | null = null; - constructor(private readonly configService: ConfigService) {} - - private hasSigningConfiguration(): boolean { - const keyPairId = - this.configService.get("EDGE_KEY_PAIR_ID") ?? - this.configService.get("CLOUDFRONT_KEY_PAIR_ID"); - const inlinePrivateKey = - this.configService.get("EDGE_PRIVATE_KEY") ?? - this.configService.get("CLOUDFRONT_PRIVATE_KEY"); - const privateKeyPath = - this.configService.get("EDGE_PRIVATE_KEY_PATH") ?? - this.configService.get("CLOUDFRONT_PRIVATE_KEY_PATH"); - - return Boolean(keyPairId && (inlinePrivateKey || privateKeyPath)); - } - - private getEdgeEndpointRaw(): string { - const endpoint = - this.configService.get("EDGE_ENDPOINT") ?? - this.configService.get("CDN_URL"); - if (!endpoint) throw new MissingEnvVarError("EDGE_ENDPOINT"); - return endpoint; - } - - private normalizeEndpoint(endpoint: string): string { - const trimmed = endpoint.trim().replace(/\/+$/, ""); - if (/^https?:\/\//i.test(trimmed)) { - return trimmed; - } - return `https://${trimmed}`; - } - - private getEdgeEndpointUrl(): string { - return this.normalizeEndpoint(this.getEdgeEndpointRaw()); - } - - private getKeyPairId(): string { - const keyPairId = - this.configService.get("EDGE_KEY_PAIR_ID") ?? - this.configService.get("CLOUDFRONT_KEY_PAIR_ID"); - if (!keyPairId) throw new MissingEnvVarError("EDGE_KEY_PAIR_ID"); - return keyPairId; - } - - private buildResourceUrl( - key: string, - options?: { allowWildcard?: boolean } - ): string { - const endpoint = this.getEdgeEndpointUrl(); - - if (options?.allowWildcard && key === "*") { - return `${endpoint}/*`; - } - - const encodedKey = key - .split("/") - .map((segment) => encodeURIComponent(segment)) - .join("/"); - - return `${endpoint}/${encodedKey}`; - } - - private getPrivateKey(): string { - if (this.privateKey) { - return this.privateKey; - } - - const envPrivateKey = - this.configService.get("EDGE_PRIVATE_KEY") ?? - this.configService.get("CLOUDFRONT_PRIVATE_KEY"); - if (envPrivateKey) { - this.privateKey = envPrivateKey.replace(/\\n/g, "\n"); - return this.privateKey; - } - - const privateKeyPath = - this.configService.get("EDGE_PRIVATE_KEY_PATH") ?? - this.configService.get("CLOUDFRONT_PRIVATE_KEY_PATH"); - if (!privateKeyPath) throw new MissingEnvVarError("EDGE_PRIVATE_KEY_PATH"); - - try { - if (!fs.existsSync(privateKeyPath)) { - throw new CloudFrontPrivateKeyException( - privateKeyPath, - "File does not exist on disk" - ); - } - this.privateKey = fs.readFileSync(privateKeyPath, "utf8"); - return this.privateKey; - } catch (error) { - if (error instanceof CloudFrontPrivateKeyException) { - throw error; - } - this.logger.error(`Failed to read Edge private key: ${error}`); - throw new CloudFrontPrivateKeyException(privateKeyPath, error); - } - } - - getCookieDomain(): string { - try { - return new URL(this.getEdgeEndpointUrl()).hostname; - } catch { - throw new BadEnvVarError("EDGE_ENDPOINT"); - } - } - - private validateCookies( - cookies: CloudfrontSignedCookiesOutput - ): Record { - const normalizedCookies: Record = { - "CloudFront-Policy": cookies["CloudFront-Policy"], - "CloudFront-Signature": cookies["CloudFront-Signature"], - "CloudFront-Key-Pair-Id": cookies["CloudFront-Key-Pair-Id"] - }; - if ( - !cookies["CloudFront-Signature"] || - !cookies["CloudFront-Key-Pair-Id"] - ) { - throw new CloudfrontSignedCookiesException(normalizedCookies); - } - return normalizedCookies; - } - - generateSignedUrl(fileKey: string): string { - if (!this.hasSigningConfiguration()) { - return this.buildResourceUrl(fileKey); - } - - const keyPairId = this.getKeyPairId(); - - const resourceUrl = this.buildResourceUrl(fileKey); - const privateKey = this.getPrivateKey(); - - const signedUrl = getSignedCFUrl({ - url: resourceUrl, - dateLessThan: new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(), // 24h - privateKey, - keyPairId - }); - - return signedUrl; - } - - getCDNUrl(key: string): string { - return this.buildResourceUrl(key); - } - - generateSignedCookies( - expiresInSeconds: number = 86400 - ): CloudfrontSignedCookiesOutput { - if (!this.hasSigningConfiguration()) { - return {} as CloudfrontSignedCookiesOutput; - } - - const keyPairId = this.getKeyPairId(); - - const privateKey = this.getPrivateKey(); - const dateLessThan = new Date( - Date.now() + expiresInSeconds * 1000 - ).toISOString(); - - const cookies = getSignedCookies({ - url: this.buildResourceUrl("*", { allowWildcard: true }), - keyPairId, - privateKey, - dateLessThan - }); - this.validateCookies(cookies); - - return cookies; - } - - createSignedCookies( - resourceUrl: string, - sessionCookieTimeout: number - ): Record { - if (!this.hasSigningConfiguration()) { - return {}; - } - - const keyPairId = this.getKeyPairId(); - - const privateKey = this.getPrivateKey(); - const expires = Math.floor(Date.now() / 1000) + sessionCookieTimeout; - - const cookies = getSignedCookies({ - url: resourceUrl, - keyPairId, - privateKey, - dateLessThan: expires - }); - this.validateCookies(cookies); - - return { - "CloudFront-Policy": cookies["CloudFront-Policy"] ?? "", - "CloudFront-Signature": cookies["CloudFront-Signature"]!, - "CloudFront-Key-Pair-Id": cookies["CloudFront-Key-Pair-Id"]!, - "CloudFront-Expires": expires.toString() - }; - } -} diff --git a/src/routes/s3/dto/apply-policy.dto.ts b/src/routes/s3/dto/apply-policy.dto.ts index 0097d04..ea84702 100644 --- a/src/routes/s3/dto/apply-policy.dto.ts +++ b/src/routes/s3/dto/apply-policy.dto.ts @@ -4,29 +4,29 @@ import { Type } from "class-transformer"; class BucketPolicyStatementDto { @ApiProperty() - Sid!: string; + Sid!: string; @ApiProperty() - Effect!: string; + Effect!: string; @ApiProperty() - Principal!: "*" | { AWS: string }; + Principal!: "*" | { AWS: string }; @ApiProperty({ type: [String] }) - Action!: string[]; + Action!: string[]; @ApiProperty() - Resource!: string; + Resource!: string; } class BucketPolicyDto { @ApiProperty() - Version!: string; + Version!: string; @ApiProperty({ type: [BucketPolicyStatementDto] }) @ValidateNested({ each: true }) @Type(() => BucketPolicyStatementDto) - Statement!: BucketPolicyStatementDto[]; + Statement!: BucketPolicyStatementDto[]; } export class ApplyPolicyDto { diff --git a/src/routes/s3/dto/upload-file.dto.ts b/src/routes/s3/dto/upload-file.dto.ts index 81b19ce..35410bf 100644 --- a/src/routes/s3/dto/upload-file.dto.ts +++ b/src/routes/s3/dto/upload-file.dto.ts @@ -7,7 +7,7 @@ export class UploadFileDto { format: "binary", description: "File to upload" }) - file!: Express.Multer.File; + file!: Express.Multer.File; @ApiProperty({ description: "Optional metadata for the file", diff --git a/src/routes/s3/edge.service.spec.ts b/src/routes/s3/edge.service.spec.ts new file mode 100644 index 0000000..0c5ca38 --- /dev/null +++ b/src/routes/s3/edge.service.spec.ts @@ -0,0 +1,125 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { ConfigService } from "@nestjs/config"; +import { BadEnvVarError, MissingEnvVarError } from "@auth/auth.error"; +import { CloudfrontService } from "./edge.service"; + +describe("CloudfrontService", () => { + let cloudfrontService: CloudfrontService; + let configService: ConfigService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + CloudfrontService, + { + provide: ConfigService, + useValue: { + get: jest.fn() + } + } + ] + }).compile(); + + cloudfrontService = module.get(CloudfrontService); + configService = module.get(ConfigService); + }); + + describe("generateSignedUrl", () => { + it("returns the resource URL using EDGE_ENDPOINT", () => { + (configService.get as jest.Mock).mockImplementation((key: string) => { + if (key === "EDGE_ENDPOINT") return "cdn.example.com"; + return undefined; + }); + + const url = cloudfrontService.generateSignedUrl("file.txt"); + + expect(url).toBe("https://cdn.example.com/file.txt"); + }); + + it("encodes each path segment", () => { + (configService.get as jest.Mock).mockImplementation((key: string) => { + if (key === "EDGE_ENDPOINT") return "cdn.example.com"; + return undefined; + }); + + const url = cloudfrontService.generateSignedUrl("path with spaces/file #1.txt"); + + expect(url).toBe("https://cdn.example.com/path%20with%20spaces/file%20%231.txt"); + }); + + it("preserves an explicit protocol", () => { + (configService.get as jest.Mock).mockImplementation((key: string) => { + if (key === "EDGE_ENDPOINT") return "http://cdn.example.com"; + return undefined; + }); + + const url = cloudfrontService.generateSignedUrl("file.txt"); + + expect(url).toBe("http://cdn.example.com/file.txt"); + }); + + it("trims whitespace and trailing slashes from EDGE_ENDPOINT", () => { + (configService.get as jest.Mock).mockImplementation((key: string) => { + if (key === "EDGE_ENDPOINT") return " cdn.example.com/// "; + return undefined; + }); + + const url = cloudfrontService.generateSignedUrl("nested/file.txt"); + + expect(url).toBe("https://cdn.example.com/nested/file.txt"); + }); + + it("throws MissingEnvVarError if EDGE_ENDPOINT is missing", () => { + (configService.get as jest.Mock).mockReturnValue(undefined); + + expect(() => cloudfrontService.generateSignedUrl("file.txt")).toThrow( + MissingEnvVarError + ); + }); + }); + + describe("getCDNUrl", () => { + it("returns the same encoded resource URL", () => { + (configService.get as jest.Mock).mockImplementation((key: string) => { + if (key === "EDGE_ENDPOINT") return "cdn.example.com"; + return undefined; + }); + + const url = cloudfrontService.getCDNUrl("path/to/file.txt"); + + expect(url).toBe("https://cdn.example.com/path/to/file.txt"); + }); + }); + + describe("getCookieDomain", () => { + it("returns the hostname from EDGE_ENDPOINT", () => { + (configService.get as jest.Mock).mockImplementation((key: string) => { + if (key === "EDGE_ENDPOINT") return "https://cdn.example.com"; + return undefined; + }); + + const domain = cloudfrontService.getCookieDomain(); + + expect(domain).toBe("cdn.example.com"); + }); + + it("adds https before extracting the hostname", () => { + (configService.get as jest.Mock).mockImplementation((key: string) => { + if (key === "EDGE_ENDPOINT") return "cdn.example.com"; + return undefined; + }); + + const domain = cloudfrontService.getCookieDomain(); + + expect(domain).toBe("cdn.example.com"); + }); + + it("throws BadEnvVarError if EDGE_ENDPOINT is missing", () => { + (configService.get as jest.Mock).mockReturnValue(undefined); + + expect(() => cloudfrontService.getCookieDomain()).toThrow( + BadEnvVarError + ); + }); + }); +}); diff --git a/src/routes/s3/edge.service.ts b/src/routes/s3/edge.service.ts new file mode 100644 index 0000000..9fceaa7 --- /dev/null +++ b/src/routes/s3/edge.service.ts @@ -0,0 +1,64 @@ +import { Injectable } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { + BadEnvVarError, + MissingEnvVarError, +} from "@auth/auth.error"; + +@Injectable() +export class CloudfrontService { + constructor(private readonly configService: ConfigService) {} + + private getEdgeEndpointRaw(): string { + const endpoint = + this.configService.get("EDGE_ENDPOINT"); + if (!endpoint) throw new MissingEnvVarError("EDGE_ENDPOINT"); + return endpoint; + } + + private normalizeEndpoint(endpoint: string): string { + const trimmed = endpoint.trim().replace(/\/+$/, ""); + if (/^https?:\/\//i.test(trimmed)) { + return trimmed; + } + return `https://${trimmed}`; + } + + private getEdgeEndpointUrl(): string { + return this.normalizeEndpoint(this.getEdgeEndpointRaw()); + } + + private buildResourceUrl( + key: string, + options?: { allowWildcard?: boolean } + ): string { + const endpoint = this.getEdgeEndpointUrl(); + + if (options?.allowWildcard && key === "*") { + return `${endpoint}/*`; + } + + const encodedKey = key + .split("/") + .map((segment) => encodeURIComponent(segment)) + .join("/"); + + return `${endpoint}/${encodedKey}`; + } + + getCookieDomain(): string { + try { + return new URL(this.getEdgeEndpointUrl()).hostname; + } catch { + throw new BadEnvVarError("EDGE_ENDPOINT"); + } + } + + generateSignedUrl(fileKey: string): string { + return this.buildResourceUrl(fileKey); + } + + getCDNUrl(key: string): string { + return this.buildResourceUrl(key); + } +} \ No newline at end of file diff --git a/src/routes/s3/s3.controller.spec.ts b/src/routes/s3/s3.controller.spec.ts deleted file mode 100644 index 28187f1..0000000 --- a/src/routes/s3/s3.controller.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Test, TestingModule } from "@nestjs/testing"; -import { S3Controller } from "./s3.controller"; -import { S3Service } from "./s3.service"; -import { BucketService } from "./bucket.service"; -import { CloudfrontService } from "./cloudfront.service"; -import { ConfigService } from "@nestjs/config"; -import { S3Client } from "@aws-sdk/client-s3"; - -describe("S3Controller", () => { - let controller: S3Controller; - - const mockS3Client = { send: jest.fn() } as unknown as S3Client; - - const mockS3Service = { - listBuckets: jest.fn(), - createBucket: jest.fn(), - deleteBucket: jest.fn(), - generateBucketPolicy: jest.fn(), - applyBucketPolicy: jest.fn(), - generateSignedUrl: jest.fn(), - createSignedCookies: jest.fn(), - generateSignedCookies: jest.fn() - }; - - const mockBucketService = { - listBuckets: jest.fn(), - createBucket: jest.fn(), - deleteBucket: jest.fn(), - applyBucketPolicy: jest.fn(), - generateBucketPolicy: jest.fn() - }; - - const mockCloudfrontService = {}; - - const mockConfigService = { - get: jest.fn() - }; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [S3Controller], - providers: [ - { - provide: S3Service, - useValue: mockS3Service - }, - { - provide: BucketService, - useValue: mockBucketService - }, - { - provide: CloudfrontService, - useValue: mockCloudfrontService - }, - { - provide: ConfigService, - useValue: mockConfigService - }, - { - provide: S3Client, - useValue: mockS3Client - } - ] - }).compile(); - - controller = module.get(S3Controller); - }); - - it("should be defined", () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/src/routes/s3/s3.controller.ts b/src/routes/s3/s3.controller.ts deleted file mode 100644 index 39eec56..0000000 --- a/src/routes/s3/s3.controller.ts +++ /dev/null @@ -1,434 +0,0 @@ -import { - Controller, - Get, - Post, - Delete, - Param, - Body, - Res, - UploadedFile, - UseInterceptors, - HttpStatus, - Logger -} from "@nestjs/common"; -import { ConfigService } from "@nestjs/config"; -import { FileInterceptor } from "@nestjs/platform-express"; -import { Response } from "express"; -import { - ApiTags, - ApiOperation, - ApiResponse, - ApiConsumes, - ApiBody, - ApiParam -} from "@nestjs/swagger"; -import { S3Service } from "./s3.service"; -import { BucketService } from "./bucket.service"; -import { CloudfrontService } from "./cloudfront.service"; -import { DeleteS3FilesDto } from "./dto/delete-files.dto"; -import { UploadFileDto } from "./dto/upload-file.dto"; - -import { Bucket, _Object } from "@aws-sdk/client-s3"; -import { CloudfrontSignedCookiesOutput } from "@aws-sdk/cloudfront-signer"; - -import { S3ObjectMetadata } from "./s3.interface"; -import { MissingEnvVarError } from "@auth/auth.error"; - -@ApiTags("s3") -@Controller("s3") -export class S3Controller { - private readonly sessionCookieTimeout = 600; - private readonly logger = new Logger(S3Controller.name); - constructor( - private readonly s3Service: S3Service, - private readonly bucketService: BucketService, - private readonly cloudfrontService: CloudfrontService, - private readonly configService: ConfigService - ) {} - - @Get("list") - @ApiOperation({ summary: "List all S3 buckets" }) - @ApiResponse({ status: 200, description: "Returns a list of all buckets" }) - @ApiResponse({ status: 500, description: "Server error" }) - async listBuckets(): Promise<{ buckets: Bucket[] }> { - const buckets = await this.bucketService.listBuckets(); - return { buckets }; - } - - @Get("list/:bucketName") - @ApiOperation({ summary: "List objects in a bucket" }) - @ApiParam({ name: "bucketName", description: "Name of the bucket" }) - @ApiResponse({ - status: 200, - description: "Returns a list of objects in the bucket" - }) - @ApiResponse({ status: 500, description: "Server error" }) - async listObjects( - @Param("bucketName") bucketName?: string - ): Promise<{ contents: _Object[] | [] }> { - const contents = await this.s3Service.listObjects({ - bucketName: bucketName! - }); - return { contents }; - } - - @Get("download-url/:bucketName/:key") - @ApiOperation({ summary: "Generate a signed download URL" }) - @ApiParam({ name: "key", description: "Object key" }) - @ApiParam({ name: "bucketName", description: "Name of the bucket" }) - @ApiResponse({ - status: 200, - description: "Returns a signed URL for downloading the file" - }) - @ApiResponse({ status: 500, description: "Server error" }) - async getSignedDownloadUrl( - @Param("key") key: string, - @Param("bucketName") bucketName?: string - ): Promise<{ url: string }> { - const url = await this.s3Service.getSignedDownloadUrl( - decodeURIComponent(key), - bucketName - ); - return { url }; - } - - @Get("download/:bucketName/:key") - @ApiOperation({ summary: "Download a file directly" }) - @ApiParam({ name: "key", description: "Object key" }) - @ApiParam({ name: "bucketName", description: "Name of the bucket" }) - @ApiResponse({ status: 200, description: "File stream" }) - @ApiResponse({ status: 500, description: "Server error" }) - async downloadFile( - @Param("key") key: string, - @Res() res: Response, - @Param("bucketName") bucketName?: string - ): Promise { - const decodedKey = decodeURIComponent(key); - try { - const { body, contentType, contentLength } = - await this.s3Service.downloadFile({ - key: decodedKey, - bucketName: bucketName! - }); - - res.setHeader("Content-Type", contentType || "application/octet-stream"); - res.setHeader( - "Content-Disposition", - `attachment; filename="${decodedKey.split("/").pop()}"` - ); - - if (contentLength) { - res.setHeader("Content-Length", contentLength.toString()); - } - - body.pipe(res); - } catch (error: unknown) { - if (error instanceof Error) { - this.logger.error( - `Error downloading project's content ${decodedKey} from bucket ${bucketName}: ${error.message}`, - error.stack - ); - res - .status(HttpStatus.INTERNAL_SERVER_ERROR) - .json({ - error: "Server error while downloading file", - message: error.message - }); - } else { - this.logger.error( - `Unknown error downloading content ${decodedKey} from bucket ${bucketName}` - ); - res - .status(HttpStatus.INTERNAL_SERVER_ERROR) - .json({ - error: "Server error while downloading file", - message: "Unknown error occurred" - }); - } - return; - } - } - - @Post("upload/:bucketName") - @ApiOperation({ summary: "Upload a file to S3" }) - @ApiParam({ name: "bucketName", description: "Name of the bucket" }) - @ApiConsumes("multipart/form-data") - @ApiBody({ type: UploadFileDto }) - @UseInterceptors(FileInterceptor("file")) - @ApiResponse({ status: 200, description: "File uploaded successfully" }) - @ApiResponse({ status: 400, description: "No file provided" }) - @ApiResponse({ status: 500, description: "Server error" }) - async uploadFile( - @UploadedFile() file: Express.Multer.File, - @Body() uploadFileDto: UploadFileDto, - @Param("bucketName") bucketName?: string - ): Promise<{ message: string } | { statusCode: number; error: string }> { - if (!file) { - return { - statusCode: HttpStatus.BAD_REQUEST, - error: "No file provided" - }; - } - - await this.s3Service.uploadFile({ - file: file, - metadata: uploadFileDto.metadata || {}, - bucketName: bucketName! - }); - return { message: "File uploaded successfully" }; - } - - @Delete("delete/:bucketName/:key") - @ApiOperation({ summary: "Delete a file from S3" }) - @ApiParam({ name: "key", description: "Object key" }) - @ApiParam({ name: "bucketName", description: "Name of the bucket" }) - @ApiResponse({ status: 200, description: "File deleted successfully" }) - @ApiResponse({ status: 500, description: "Server error" }) - async deleteFile( - @Param("key") key: string, - @Param("bucketName") bucketName?: string - ): Promise<{ message: string }> { - await this.s3Service.deleteFile({ - key: decodeURIComponent(key), - bucketName: bucketName! - }); - return { message: "File deleted successfully" }; - } - - @Delete("delete-multiple/:bucketName") - @ApiOperation({ summary: "Delete multiple files from S3" }) - @ApiParam({ name: "bucketName", description: "Name of the bucket" }) - @ApiResponse({ status: 200, description: "Files deleted successfully" }) - @ApiResponse({ status: 500, description: "Server error" }) - async deleteFiles( - @Body() deleteFilesDto: DeleteS3FilesDto, - @Param("bucketName") bucketName?: string - ): Promise<{ message: string; deleted: _Object[] }> { - const result = await this.s3Service.deleteFiles({ - keys: deleteFilesDto.keys.map((key) => decodeURIComponent(key)), - bucketName: bucketName! - }); - return { message: "Files deleted successfully", deleted: result }; - } - - @Delete("bucket/:bucketName") - @ApiOperation({ summary: "Delete a bucket" }) - @ApiParam({ name: "bucketName", description: "Name of the bucket" }) - @ApiResponse({ status: 200, description: "Bucket deleted successfully" }) - @ApiResponse({ status: 500, description: "Server error" }) - async deleteBucket( - @Param("bucketName") bucketName?: string - ): Promise<{ message: string }> { - await this.bucketService.deleteBucket(bucketName); - return { message: "Bucket deleted successfully" }; - } - - @Post("bucket/:bucketName") - @ApiOperation({ summary: "Create a new bucket" }) - @ApiParam({ name: "bucketName", description: "Name of the bucket" }) - @ApiResponse({ status: 201, description: "Bucket created successfully" }) - @ApiResponse({ status: 500, description: "Server error" }) - async createBucket( - @Param("bucketName") bucketName?: string - ): Promise<{ message: string }> { - await this.bucketService.createBucket(bucketName); - return { message: "Bucket created successfully" }; - } - - @Post("policy/public-read") - @ApiOperation({ - summary: "Apply public read policy for a prefix (default: release/*)" - }) - @ApiBody({ - schema: { - type: "object", - properties: { - bucketName: { - type: "string", - description: "Target bucket name (optional, defaults to S3_BUCKET_NAME)" - }, - prefix: { - type: "string", - description: "Object key prefix to expose publicly", - default: "release/*" - } - } - } - }) - @ApiResponse({ status: 201, description: "Policy applied successfully" }) - async applyPublicReadPolicy( - @Body() body: { bucketName?: string; prefix?: string } - ): Promise<{ - message: string; - bucketName?: string; - prefix: string; - mode: "bucket-policy" | "object-acl"; - updatedObjects?: number; - }> { - const prefix = body?.prefix?.trim() || "release/*"; - const bucketName = body?.bucketName; - - const response: { - message: string; - bucketName?: string; - prefix: string; - mode: "bucket-policy" | "object-acl"; - updatedObjects?: number; - } = { - message: "Public read policy applied successfully", - prefix, - mode: "bucket-policy" - }; - - try { - await this.bucketService.applyPublicReadPrefixPolicy(prefix, bucketName); - } catch (error) { - const normalizedPrefix = prefix.replace(/\*+$/, ""); - const listOptions: { bucketName?: string; prefix?: string } = { - prefix: normalizedPrefix - }; - if (bucketName) { - listOptions.bucketName = bucketName; - } - - const objects = await this.s3Service.listObjects(listOptions); - - await Promise.all( - objects - .map((object) => object.Key) - .filter((key): key is string => Boolean(key)) - .map((key) => this.s3Service.setObjectPublicRead(key, bucketName)) - ); - - response.mode = "object-acl"; - response.updatedObjects = objects.length; - response.message = - error instanceof Error - ? `Bucket policy denied; applied object ACL fallback instead (${error.message})` - : "Bucket policy denied; applied object ACL fallback instead"; - } - - if (bucketName) { - response.bucketName = bucketName; - } - - return response; - } - - @Get("metadata/:bucketName/:key") - @ApiOperation({ summary: "Get object metadata" }) - @ApiParam({ name: "key", description: "Object key" }) - @ApiParam({ name: "bucketName", description: "Name of the bucket" }) - @ApiResponse({ status: 200, description: "Returns object metadata" }) - @ApiResponse({ status: 500, description: "Server error" }) - async getObjectMetadata( - @Param("key") key: string, - @Param("bucketName") bucketName?: string - ): Promise<{ metadata: S3ObjectMetadata }> { - const metadata = await this.s3Service.getObjectMetadata({ - key: decodeURIComponent(key), - bucketName: bucketName! - }); - return { metadata }; - } - - @Get("cdn-url/:key") - @ApiOperation({ summary: "Get the CDN URL for a file" }) - @ApiParam({ name: "key", description: "Object key" }) - @ApiResponse({ status: 200, description: "Returns the CDN URL" }) - @ApiResponse({ status: 500, description: "Server error" }) - async getCdnUrl(@Param("key") key: string): Promise<{ url: string }> { - const url = this.cloudfrontService.generateSignedUrl( - decodeURIComponent(key) - ); - return { url }; - } - - @Get("signed-cookies/:key") - @ApiOperation({ - summary: "Generate CloudFront signed cookies for a resource" - }) - @ApiParam({ name: "key", description: "Object key (relative path in CDN)" }) - @ApiResponse({ status: 200, description: "Returns signed cookies" }) - @ApiResponse({ status: 500, description: "Server error" }) - async getSignedCookies( - @Param("key") key: string, - @Res() res: Response - ): Promise { - try { - const cdnUrl = this.configService.get("CDN_URL"); - if (!cdnUrl) { - throw new MissingEnvVarError("CDN_URL"); - } - const resourceUrl = `https://${cdnUrl}/${key.split("/").map(encodeURIComponent).join("/")}`; - - const cookies = this.cloudfrontService.createSignedCookies( - resourceUrl, - this.sessionCookieTimeout - ); - - const cookieOptions = { - httpOnly: true, - secure: true, - path: "/", - domain: `.${cdnUrl}`, - sameSite: "lax" as const, - maxAge: 60 * 60 * 1000 - }; - - res.cookie( - "CloudFront-Expires", - cookies["CloudFront-Expires"], - cookieOptions - ); - res.cookie( - "CloudFront-Signature", - cookies["CloudFront-Signature"], - cookieOptions - ); - res.cookie( - "CloudFront-Key-Pair-Id", - cookies["CloudFront-Key-Pair-Id"], - cookieOptions - ); - - const response = { - message: "Signed cookies set successfully", - resourceUrl, - cookies: { - "CloudFront-Expires": cookies["CloudFront-Expires"], - "CloudFront-Signature": cookies["CloudFront-Signature"], - "CloudFront-Key-Pair-Id": cookies["CloudFront-Key-Pair-Id"] - } - }; - this.logger.debug("Response JSON:", JSON.stringify(response)); - res.status(HttpStatus.OK).json(response); - } catch (error) { - this.logger.error("Error generating signed cookies:", error); - res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ - error: "Could not generate signed cookies", - message: error instanceof Error ? error.message : "Unknown error" - }); - } - } - - @Get("/signed-cookies") - async setSignedCookies( - @Res({ passthrough: true }) res: Response - ): Promise<{ success: boolean; cookies: CloudfrontSignedCookiesOutput }> { - const cookies = this.cloudfrontService.generateSignedCookies(); - - Object.entries(cookies).forEach(([name, value]) => { - res.cookie(name, value, { - // domain: "d3puh88kxjv1qg.cloudfront.net", - httpOnly: true, - secure: false, - path: "/", - sameSite: "lax", - maxAge: 60 * 60 * 1000 - }); - }); - - return { success: true, cookies }; - } -} diff --git a/src/routes/s3/s3.module.ts b/src/routes/s3/s3.module.ts index 2d4f471..0e8a2df 100644 --- a/src/routes/s3/s3.module.ts +++ b/src/routes/s3/s3.module.ts @@ -2,16 +2,11 @@ import { Module } from "@nestjs/common"; import { MulterModule } from "@nestjs/platform-express"; import { ConfigModule, ConfigService } from "@nestjs/config"; import { S3Client } from "@aws-sdk/client-s3"; -import { S3Controller } from "./s3.controller"; import { S3Service } from "./s3.service"; -import { BucketService } from "./bucket.service"; -import { CloudfrontService } from "./cloudfront.service"; -import { PrismaService } from "@prisma/prisma.service"; +import { CloudfrontService } from "./edge.service"; +import { PrismaService } from "@ourPrisma/prisma.service"; import { S3ConfigurationException } from "./s3.error"; -const controllers = - process.env["NODE_ENV"] === "production" ? [] : [S3Controller]; - @Module({ imports: [ ConfigModule, @@ -19,7 +14,6 @@ const controllers = limits: { fileSize: 10 * 1024 * 1024 } // 10MB }) ], - controllers, providers: [ { provide: S3Client, @@ -54,10 +48,9 @@ const controllers = inject: [ConfigService] }, S3Service, - BucketService, CloudfrontService, PrismaService ], - exports: [S3Service, BucketService, CloudfrontService] + exports: [S3Service, CloudfrontService] }) export class S3Module {} diff --git a/src/routes/s3/s3.service.spec.ts b/src/routes/s3/s3.service.spec.ts index ded5702..ebeb287 100644 --- a/src/routes/s3/s3.service.spec.ts +++ b/src/routes/s3/s3.service.spec.ts @@ -15,11 +15,19 @@ describe("S3Service", () => { beforeEach(() => { mockConfig = { - get: (key: string) => - key === "S3_BUCKET_NAME" ? "my-default-bucket" : "us-east-1" + get: (key: string) => { + if (key === "S3_BUCKET_NAME") return "my-default-bucket"; + if (key === "S3_REGION") return "fr-par"; + if (key === "S3_ENDPOINT") return "https://s3.fr-par.scw.cloud"; + if (key === "S3_ACCESS_KEY_ID") return "test-access-key"; + if (key === "S3_SECRET_ACCESS_KEY") return "test-secret-key"; + return undefined; + } }; mockS3 = { send: jest.fn() } as unknown as S3Client & { send: jest.Mock }; - s3Service = new S3Service(mockS3, mockConfig as ConfigService); + s3Service = new S3Service(mockConfig as unknown as ConfigService); + // Mock the S3 client on the instance + (s3Service as any).s3 = mockS3; }); describe("resolveBucket", () => { @@ -32,8 +40,17 @@ describe("S3Service", () => { }); it("throws when no bucket", () => { - const emptyConfig: Pick = { get: () => undefined }; - const service = new S3Service(mockS3, emptyConfig as ConfigService); + const emptyConfig: Pick = { + get: (key: string) => { + if (key === "S3_ENDPOINT") return "https://s3.fr-par.scw.cloud"; + if (key === "S3_REGION") return "fr-par"; + if (key === "S3_ACCESS_KEY_ID") return "test-access-key"; + if (key === "S3_SECRET_ACCESS_KEY") return "test-secret-key"; + return undefined; + } + }; + const service = new S3Service(emptyConfig as unknown as ConfigService); + (service as any).s3 = mockS3; expect(() => service["resolveBucket"]()).toThrow( BucketResolutionException ); @@ -59,7 +76,7 @@ describe("S3Service", () => { it("throws S3ListObjectsException on error", async () => { const err = new Error("Access Denied"); mockS3.send.mockRejectedValueOnce(err); - await expect(s3Service.listObjects("bucket")).rejects.toThrow( + await expect(s3Service.listObjects({ bucketName: "bucket" })).rejects.toThrow( S3ListObjectsException ); }); diff --git a/src/routes/user/dto/collaborator-project.dto.ts b/src/routes/user/dto/collaborator-project.dto.ts index 85c6457..3bfbdd5 100644 --- a/src/routes/user/dto/collaborator-project.dto.ts +++ b/src/routes/user/dto/collaborator-project.dto.ts @@ -7,7 +7,7 @@ export class AddCollaboratorDto { example: "1234" }) @IsNumber() - userId!: number; + userId!: number; } export class RemoveCollaboratorDto { @@ -16,5 +16,5 @@ export class RemoveCollaboratorDto { example: "1234" }) @IsNumber() - userId!: number; + userId!: number; } diff --git a/src/routes/user/dto/create-user.dto.ts b/src/routes/user/dto/create-user.dto.ts index 340b4a8..34ef434 100644 --- a/src/routes/user/dto/create-user.dto.ts +++ b/src/routes/user/dto/create-user.dto.ts @@ -16,12 +16,12 @@ export class CreateUserDto { }) @IsEmail() @IsNotEmpty() - email!: string; + email!: string; @ApiProperty({ description: "User username", example: "xX_DarkGamer_Xx" }) @IsString() @Length(3, 20, { message: "Username must be between 3 and 20 characters" }) - username!: string; + username!: string; @ApiProperty({ description: "User nick name", @@ -31,15 +31,15 @@ export class CreateUserDto { @IsString() @IsOptional() @Length(3, 30, { message: "Nickname must be between 3 and 30 characters" }) - nickname?: string; + nickname?: string; @ApiProperty({ description: "User password", example: "password123" }) @IsString() @MinLength(6, { message: "Password must be at least 6 characters" }) @IsNotEmpty() - password!: string; + password!: string; @IsOptional() @IsArray() - roles?: string[]; + roles?: string[]; } diff --git a/src/routes/user/dto/user-filter.dto.ts b/src/routes/user/dto/user-filter.dto.ts index afb297b..07f576e 100644 --- a/src/routes/user/dto/user-filter.dto.ts +++ b/src/routes/user/dto/user-filter.dto.ts @@ -8,24 +8,24 @@ export class UserFilterDto { @Type(() => Number) @IsInt() @Min(1) - page?: number; + page?: number; @ApiPropertyOptional({ description: "Items per page", example: 10 }) @IsOptional() @Type(() => Number) @IsInt() @Min(1) - limit?: number; + limit?: number; @ApiPropertyOptional({ description: "Filter by nickname" }) @IsOptional() @IsString() - nickname?: string; + nickname?: string; @ApiPropertyOptional({ description: "Filter by email" }) @IsOptional() @IsString() - email?: string; + email?: string; @ApiPropertyOptional({ enum: ["id", "username", "email", "createdAt"], @@ -33,10 +33,10 @@ export class UserFilterDto { }) @IsOptional() @IsEnum(["id", "username", "email", "createdAt"]) - sortBy?: string; + sortBy?: string; @ApiPropertyOptional({ enum: ["asc", "desc"], description: "Sort order" }) @IsOptional() @IsEnum(["asc", "desc"]) - order?: "asc" | "desc"; + order?: "asc" | "desc"; } diff --git a/src/routes/user/dto/user-list-response.dto.ts b/src/routes/user/dto/user-list-response.dto.ts index 57f72bb..8cfa89c 100644 --- a/src/routes/user/dto/user-list-response.dto.ts +++ b/src/routes/user/dto/user-list-response.dto.ts @@ -3,31 +3,31 @@ import { UserResponseDto } from "./user-response.dto"; export class PaginationMetaDto { @ApiProperty({ description: "Current page number", example: 1 }) - page!: number; + page!: number; @ApiProperty({ description: "Number of items per page", example: 10 }) - limit!: number; + limit!: number; @ApiProperty({ description: "Total number of items", example: 100 }) - total!: number; + total!: number; @ApiProperty({ description: "Total number of pages", example: 10 }) - totalPages!: number; + totalPages!: number; } export class UserListResponseDto { @ApiProperty({ description: "HTTP status code", example: 200 }) - statusCode!: number; + statusCode!: number; @ApiProperty({ description: "Response message", example: "Users retrieved successfully" }) - message!: string; + message!: string; @ApiProperty({ description: "List of users", type: [UserResponseDto] }) - data!: UserResponseDto[]; + data!: UserResponseDto[]; @ApiProperty({ description: "Pagination metadata", type: PaginationMetaDto }) - meta!: PaginationMetaDto; + meta!: PaginationMetaDto; } diff --git a/src/routes/user/dto/user-profile-response.dto.ts b/src/routes/user/dto/user-profile-response.dto.ts index 4d1dac2..3298682 100644 --- a/src/routes/user/dto/user-profile-response.dto.ts +++ b/src/routes/user/dto/user-profile-response.dto.ts @@ -6,5 +6,5 @@ export class UserProfileResponseDto extends UserResponseDto { description: "User profile message", example: "Profile retrieved successfully" }) - message?: string; + message?: string; } diff --git a/src/routes/user/dto/user-response.dto.ts b/src/routes/user/dto/user-response.dto.ts index 03a8288..c8482de 100644 --- a/src/routes/user/dto/user-response.dto.ts +++ b/src/routes/user/dto/user-response.dto.ts @@ -4,28 +4,28 @@ import { Expose, Type } from "class-transformer"; export class UserRoleDto { @ApiProperty({ description: "Role ID", example: 1 }) @Expose() - id!: number; + id!: number; @ApiProperty({ description: "Role name", example: "Admin" }) @Expose() - name!: string; + name!: string; } export class UserResponseDto { @ApiProperty({ description: "User ID", example: 1 }) @Expose() - id!: number; + id!: number; @ApiProperty({ description: "User email address", example: "user@example.com" }) @Expose() - email!: string; + email!: string; @ApiProperty({ description: "Username", example: "xX_DarkGamer_Xx" }) @Expose() - username!: string; + username!: string; @ApiProperty({ description: "User nickname", @@ -33,7 +33,7 @@ export class UserResponseDto { nullable: true }) @Expose() - nickname?: string; + nickname?: string; @ApiProperty({ description: "User roles", @@ -42,19 +42,19 @@ export class UserResponseDto { }) @Expose() @Type(() => UserRoleDto) - roles?: UserRoleDto[]; + roles?: UserRoleDto[]; @ApiProperty({ description: "User creation date", example: "2023-01-01T00:00:00.000Z" }) @Expose() - createdAt!: Date; + createdAt!: Date; @ApiProperty({ description: "User last update date", example: "2023-01-01T00:00:00.000Z" }) @Expose() - updatedAt!: Date; + updatedAt!: Date; } diff --git a/src/routes/user/dto/user-single-response.dto.ts b/src/routes/user/dto/user-single-response.dto.ts index d7b469b..fb6e9f1 100644 --- a/src/routes/user/dto/user-single-response.dto.ts +++ b/src/routes/user/dto/user-single-response.dto.ts @@ -3,14 +3,14 @@ import { UserResponseDto } from "./user-response.dto"; export class UserSingleResponseDto { @ApiProperty({ description: "HTTP status code", example: 200 }) - statusCode!: number; + statusCode!: number; @ApiProperty({ description: "Response message", example: "User retrieved successfully" }) - message!: string; + message!: string; @ApiProperty({ description: "User data", type: UserResponseDto }) - data!: UserResponseDto; + data!: UserResponseDto; } diff --git a/src/routes/user/user.controller.spec.ts b/src/routes/user/user.controller.spec.ts index 19e584d..d3fd6f6 100644 --- a/src/routes/user/user.controller.spec.ts +++ b/src/routes/user/user.controller.spec.ts @@ -1,16 +1,58 @@ import { Test, TestingModule } from "@nestjs/testing"; import { UserController } from "./user.controller"; import { UserService } from "./user.service"; -import { PrismaModule } from "@prisma/prisma.module"; +import { PrismaService } from "@ourPrisma/prisma.service"; +import { S3Service } from "@s3/s3.service"; +import { CloudfrontService } from "src/routes/s3/edge.service"; +import { ConfigService } from "@nestjs/config"; describe("UserController", () => { let controller: UserController; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [PrismaModule], controllers: [UserController], - providers: [UserService] + providers: [ + UserService, + { + provide: PrismaService, + useValue: { + $connect: jest.fn(), + $disconnect: jest.fn(), + user: { + findUnique: jest.fn(), + findMany: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + count: jest.fn() + } + } + }, + { + provide: S3Service, + useValue: { + uploadFile: jest.fn(), + fileExists: jest.fn(), + downloadFile: jest.fn(), + deleteFile: jest.fn() + } + }, + { + provide: CloudfrontService, + useValue: { + getCDNUrl: jest.fn(), + createSignedCookies: jest.fn(), + getCookieDomain: jest.fn(), + generateSignedUrl: jest.fn() + } + }, + { + provide: ConfigService, + useValue: { + get: jest.fn() + } + } + ] }).compile(); controller = module.get(UserController); diff --git a/src/routes/user/user.controller.ts b/src/routes/user/user.controller.ts index aa0b4ab..016ca63 100644 --- a/src/routes/user/user.controller.ts +++ b/src/routes/user/user.controller.ts @@ -26,7 +26,6 @@ import { ApiOperation, ApiResponse, ApiTags, - ApiQuery, ApiBearerAuth, ApiParam, ApiBody, @@ -47,7 +46,7 @@ import { UserDto } from "@auth/dto/user.dto"; import { FileInterceptor } from "@nestjs/platform-express"; import { Response } from "express"; import { S3Service } from "@s3/s3.service"; -import { CloudfrontService } from "@s3/cloudfront.service"; +import { CloudfrontService } from "src/routes/s3/edge.service"; import { ConfigService } from "@nestjs/config"; import { SignedCdnResourceDto } from "@common/dto/signed-cdn-resource.dto"; import { MissingEnvVarError } from "@auth/auth.error"; @@ -63,7 +62,6 @@ import { MissingEnvVarError } from "@auth/auth.error"; @Controller("users") export class UserController { private readonly logger = new Logger(UserController.name); - private readonly sessionCookieTimeout = 600; constructor( private readonly userService: UserService, @@ -161,39 +159,10 @@ export class UserController { throw new MissingEnvVarError("CDN_URL"); } - const resourceUrl = this.cloudfrontService.getCDNUrl(key); - const cookies = this.cloudfrontService.createSignedCookies( - resourceUrl, - this.sessionCookieTimeout - ); - - const cookieOptions = { - httpOnly: true, - secure: true, - path: "/", - domain: `.${cdnUrl}`, - sameSite: "lax" as const, - maxAge: 60 * 60 * 1000 - }; - - res.cookie("CloudFront-Expires", cookies["CloudFront-Expires"], cookieOptions); - res.cookie( - "CloudFront-Signature", - cookies["CloudFront-Signature"], - cookieOptions - ); - res.cookie( - "CloudFront-Key-Pair-Id", - cookies["CloudFront-Key-Pair-Id"], - cookieOptions - ); - if (cookies["CloudFront-Policy"]) { - res.cookie("CloudFront-Policy", cookies["CloudFront-Policy"], cookieOptions); - } + const resourceUrl = this.cloudfrontService.generateSignedUrl(key); res.status(HttpStatus.OK).json({ resourceUrl, - cookies }); } @@ -205,7 +174,6 @@ export class UserController { type: UserListResponseDto }) @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: "Unauthorized" }) - @ApiQuery({ type: UserFilterDto }) @UseGuards(JwtAuthGuard) async findAll( @Query() filterDto: UserFilterDto diff --git a/src/routes/user/user.error.ts b/src/routes/user/user.error.ts new file mode 100644 index 0000000..cc78957 --- /dev/null +++ b/src/routes/user/user.error.ts @@ -0,0 +1,20 @@ +export class UserError extends Error { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} + +export class UserNotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +}; + +export class UserGameSessionNotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +}; diff --git a/src/routes/user/user.service.spec.ts b/src/routes/user/user.service.spec.ts index b44434b..fde47cf 100644 --- a/src/routes/user/user.service.spec.ts +++ b/src/routes/user/user.service.spec.ts @@ -1,14 +1,22 @@ import { Test, TestingModule } from "@nestjs/testing"; import { UserService } from "./user.service"; -import { PrismaModule } from "@prisma/prisma.module"; +import { PrismaService } from "@ourPrisma/prisma.service"; describe("UserService", () => { let service: UserService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [PrismaModule], - providers: [UserService] + providers: [ + UserService, + { + provide: PrismaService, + useValue: { + $connect: jest.fn(), + $disconnect: jest.fn() + } + } + ] }).compile(); service = module.get(UserService); diff --git a/src/routes/user/user.service.ts b/src/routes/user/user.service.ts index eb0dd3f..c99442f 100644 --- a/src/routes/user/user.service.ts +++ b/src/routes/user/user.service.ts @@ -1,11 +1,12 @@ import { Injectable, NotFoundException } from "@nestjs/common"; -import { PrismaService } from "@prisma/prisma.service"; +import { PrismaService } from "@ourPrisma/prisma.service"; import { CreateUserDto } from "./dto/create-user.dto"; import { UpdateUserDto } from "./dto/update-user.dto"; -import { User } from "@prisma/client"; +import { User, Prisma } from "@prisma/client"; import * as bcrypt from "bcryptjs"; -import { Prisma } from "@prisma/client"; + import { Role } from "@prisma/client"; +import { UserNotFoundError, UserGameSessionNotFoundError } from "./user.error"; @Injectable() export class UserService { @@ -16,7 +17,7 @@ export class UserService { return this.prisma.role.findMany({ where: { name: { in: names } - } + }, }); } @@ -30,14 +31,11 @@ export class UserService { throw new NotFoundException(`User with ID ${userId} not found`); } - return user.roles.map((role) => role.name); + return user.roles.map(role => role.name); } async create(createUserDto: CreateUserDto): Promise { - const hashedPassword = await bcrypt.hash( - createUserDto.password, - UserService.BCRYPT_SALT_ROUNDS - ); + const hashedPassword = await bcrypt.hash(createUserDto.password, UserService.BCRYPT_SALT_ROUNDS); const rolesToAssign: { id: number }[] = []; @@ -54,12 +52,7 @@ export class UserService { }); } - async findAll(params?: { - skip?: number; - take?: number; - where?: Prisma.UserWhereInput; - orderBy?: Prisma.UserOrderByWithRelationInput; - }): Promise { + async findAll(params?: {skip?: number, take?: number, where?: Prisma.UserWhereInput, orderBy?: Prisma.UserOrderByWithRelationInput }): Promise { const query: Prisma.UserFindManyArgs = {}; if (params?.skip !== undefined) query.skip = params.skip; if (params?.take !== undefined) query.take = params.take; @@ -76,45 +69,38 @@ export class UserService { return this.prisma.user.count(countArgs); } - async findOne(id: number): Promise { + async findOne(id: number, whatElse: Record = {}): Promise { const user = await this.prisma.user.findUnique({ - where: { id } + where: { id }, + include: whatElse }); if (!user) { throw new NotFoundException(`User with ID ${id} not found`); } - return user; + return user as User & ComplexFieldsT; } async update(id: number, updateUserDto: UpdateUserDto): Promise { const { roles, ...rest } = updateUserDto; - + const data: Prisma.UserUpdateInput = { ...rest, - ...(roles - ? { roles: { connect: roles.map((roleName) => ({ name: roleName })) } } - : {}) + ...(roles ? { roles: { connect: roles.map(roleName => ({ name: roleName })) } } : {}), }; if (updateUserDto.password) { - data.password = await bcrypt.hash( - updateUserDto.password, - UserService.BCRYPT_SALT_ROUNDS - ); + data.password = await bcrypt.hash(updateUserDto.password, UserService.BCRYPT_SALT_ROUNDS); } try { return await this.prisma.user.update({ where: { id }, - data + data, }); } catch (error: unknown) { - if ( - error instanceof Prisma.PrismaClientKnownRequestError && - error.code === "P2025" - ) { + if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") { throw new NotFoundException(`User with ID ${id} not found`); } throw error; @@ -133,4 +119,46 @@ export class UserService { }); return user ?? undefined; } + + async attachGameSession(userId: number, gameSessionId: number, hosted: boolean): Promise { + const user = await this.prisma.user.findUnique({ where: { id: userId } }); + if (!user) { + throw new UserNotFoundError("User not found"); + } + + const gameSession = await this.prisma.gameSession.findUnique({ where: { id: gameSessionId } }); + if (!gameSession) { + throw new UserGameSessionNotFoundError("Game session not found"); + } + + await this.prisma.user.update({ + where: { id: userId }, + data: { + [hosted ? "hostedGameSessions" : "joinedGameSessions"]: { + connect: { id: gameSessionId } + } + } + }); + } + + async detachGameSession(userId: number, gameSessionId: number, hosted: boolean): Promise { + const user = await this.prisma.user.findUnique({ where: { id: userId } }); + if (!user) { + throw new UserNotFoundError("User not found"); + } + + const gameSession = await this.prisma.gameSession.findUnique({ where: { id: gameSessionId } }); + if (!gameSession) { + throw new UserGameSessionNotFoundError("Game session not found"); + } + + await this.prisma.user.update({ + where: { id: userId }, + data: { + [hosted ? "hostedGameSessions" : "joinedGameSessions"]: { + disconnect: { id: gameSessionId } + } + } + }); + } } diff --git a/src/routes/work-session/dto/create-work-session.dto.ts b/src/routes/work-session/dto/create-work-session.dto.ts index 59c9b60..990c551 100644 --- a/src/routes/work-session/dto/create-work-session.dto.ts +++ b/src/routes/work-session/dto/create-work-session.dto.ts @@ -7,9 +7,9 @@ export class CreateWorkSessionDto { example: 1 }) @IsInt() - projectId!: number; + projectId!: number; @IsOptional() @IsDate() - startTime?: Date; + startTime?: Date; } diff --git a/src/routes/work-session/dto/fetch-work-session.dto.ts b/src/routes/work-session/dto/fetch-work-session.dto.ts index f27134f..84a8421 100644 --- a/src/routes/work-session/dto/fetch-work-session.dto.ts +++ b/src/routes/work-session/dto/fetch-work-session.dto.ts @@ -7,40 +7,40 @@ export class FetchWorkSessionDto { example: [1, 2, 3] }) @IsArray() - users!: number[]; + users!: number[]; @ApiProperty({ description: "The ID of the session's host", example: 1 }) @IsNumber() - host!: number; + host!: number; @ApiProperty({ description: "The ID of the project this work session belongs to", example: 1 }) @IsNumber() - project!: number; + project!: number; @ApiProperty({ description: "The date and time when the work session started", example: "2023-04-15T12:00:00Z" }) @IsDate() - startedAt!: Date; + startedAt!: Date; @ApiProperty({ description: "The ID of the room for this work session", example: "room-12345" }) @IsString() - roomId!: string; + roomId!: string; @ApiProperty({ description: "The password for the room of this work session", example: "password123" }) @IsString() - roomPassword!: string; + roomPassword!: string; } diff --git a/src/routes/work-session/dto/kick-work-session.dto.ts b/src/routes/work-session/dto/kick-work-session.dto.ts index f725bb2..979ce5f 100644 --- a/src/routes/work-session/dto/kick-work-session.dto.ts +++ b/src/routes/work-session/dto/kick-work-session.dto.ts @@ -7,5 +7,5 @@ export class KickWorkSessionDto { example: 1 }) @IsNumber() - userId!: number; + userId!: number; } diff --git a/src/routes/work-session/work-session.controller.spec.ts b/src/routes/work-session/work-session.controller.spec.ts index 3209217..3d6462d 100644 --- a/src/routes/work-session/work-session.controller.spec.ts +++ b/src/routes/work-session/work-session.controller.spec.ts @@ -1,16 +1,24 @@ import { Test, TestingModule } from "@nestjs/testing"; import { WorkSessionController } from "./work-session.controller"; import { WorkSessionService } from "./work-session.service"; -import { PrismaModule } from "@prisma/prisma.module"; +import { PrismaService } from "@ourPrisma/prisma.service"; describe("WorkSessionController", () => { let controller: WorkSessionController; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [PrismaModule], controllers: [WorkSessionController], - providers: [WorkSessionService] + providers: [ + WorkSessionService, + { + provide: PrismaService, + useValue: { + $connect: jest.fn(), + $disconnect: jest.fn() + } + } + ] }).compile(); controller = module.get(WorkSessionController); diff --git a/src/routes/work-session/work-session.module.ts b/src/routes/work-session/work-session.module.ts index 1c3cc1c..d23c6df 100644 --- a/src/routes/work-session/work-session.module.ts +++ b/src/routes/work-session/work-session.module.ts @@ -1,7 +1,7 @@ import { Module } from "@nestjs/common"; import { WorkSessionController } from "./work-session.controller"; import { WorkSessionService } from "./work-session.service"; -import { PrismaModule } from "@prisma/prisma.module"; +import { PrismaModule } from "@ourPrisma/prisma.module"; @Module({ imports: [PrismaModule], diff --git a/src/routes/work-session/work-session.service.spec.ts b/src/routes/work-session/work-session.service.spec.ts index f5f5f96..3abf828 100644 --- a/src/routes/work-session/work-session.service.spec.ts +++ b/src/routes/work-session/work-session.service.spec.ts @@ -1,14 +1,22 @@ import { Test, TestingModule } from "@nestjs/testing"; import { WorkSessionService } from "./work-session.service"; -import { PrismaModule } from "@prisma/prisma.module"; +import { PrismaService } from "@ourPrisma/prisma.service"; describe("WorkSessionService", () => { let service: WorkSessionService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [PrismaModule], - providers: [WorkSessionService] + providers: [ + WorkSessionService, + { + provide: PrismaService, + useValue: { + $connect: jest.fn(), + $disconnect: jest.fn() + } + } + ] }).compile(); service = module.get(WorkSessionService); diff --git a/src/routes/work-session/work-session.service.ts b/src/routes/work-session/work-session.service.ts index 4499854..e0765a8 100644 --- a/src/routes/work-session/work-session.service.ts +++ b/src/routes/work-session/work-session.service.ts @@ -1,5 +1,5 @@ import { Injectable, NotFoundException } from "@nestjs/common"; -import { PrismaService } from "@prisma/prisma.service"; +import { PrismaService } from "@ourPrisma/prisma.service"; import { CreateWorkSessionDto } from "./dto/create-work-session.dto"; import { UpdateWorkSessionDto } from "./dto/update-work-session.dto"; import { uuidv4 } from "lib0/random"; diff --git a/src/swagger.app.module.ts b/src/swagger.app.module.ts new file mode 100644 index 0000000..d0ecdee --- /dev/null +++ b/src/swagger.app.module.ts @@ -0,0 +1,52 @@ +/** + * A lightweight AppModule used exclusively for Swagger JSON generation. + * It includes all controllers (for full API documentation) but replaces + * infrastructure-heavy providers (S3, Prisma, etc.) with no-op stubs so + * the app can boot without real credentials or a database connection. + */ + +import { Module, InjectionToken, Provider } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import { ScheduleModule } from "@nestjs/schedule"; + +import { AuthModule } from "@auth/auth.module"; + +import { ProjectController } from "@project/project.controller"; +import { MultiplayerController } from "src/routes/multiplayer/multiplayer.controller"; + +import { ProjectService } from "@project/project.service"; +import { S3Service } from "@s3/s3.service"; +import { CloudfrontService } from "src/routes/s3/edge.service"; +import { PrismaService } from "@ourPrisma/prisma.service"; +import { S3Client } from "@aws-sdk/client-s3"; +import { MultiplayerService } from "src/routes/multiplayer/multiplayer.service"; + +import { UserModule } from "@user/user.module"; +import { WorkSessionModule } from "@work-session/work-session.module"; +import { WebRTCModule } from "@webrtc/webrtc.module"; + +const nullProvider = (token: InjectionToken): Provider => ({ + provide: token, + useValue: null +}); + +@Module({ + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + ScheduleModule.forRoot(), + AuthModule, + UserModule, + WorkSessionModule, + WebRTCModule + ], + controllers: [ProjectController, MultiplayerController], + providers: [ + nullProvider(PrismaService), + nullProvider(ProjectService), + nullProvider(S3Client), + nullProvider(S3Service), + nullProvider(CloudfrontService), + nullProvider(MultiplayerService) + ] +}) +export class SwaggerAppModule {} diff --git a/src/swagger.ts b/src/swagger.ts index de12318..1e1197a 100644 --- a/src/swagger.ts +++ b/src/swagger.ts @@ -1,9 +1,9 @@ import { INestApplication } from "@nestjs/common"; -import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; +import { DocumentBuilder, OpenAPIObject, SwaggerModule } from "@nestjs/swagger"; import { join } from "path"; import * as express from "express"; -export function setupSwagger(app: INestApplication): void { +export function buildSwaggerDocument(app: INestApplication): OpenAPIObject { const config = new DocumentBuilder() .setTitle("Naucto API") .setDescription("The Naucto API documentation") @@ -19,7 +19,11 @@ export function setupSwagger(app: INestApplication): void { ) .build(); - const document = SwaggerModule.createDocument(app, config); + return SwaggerModule.createDocument(app, config); +} + +export function setupSwagger(app: INestApplication): void { + const document = buildSwaggerDocument(app); app.use( "/swagger-ui", diff --git a/src/tasks/tasks.module.ts b/src/tasks/tasks.module.ts index 89204ba..583a613 100644 --- a/src/tasks/tasks.module.ts +++ b/src/tasks/tasks.module.ts @@ -1,5 +1,5 @@ import { Module } from "@nestjs/common"; -import { TasksService } from "./tasks/tasks.service"; +import { TasksService } from "src/tasks/tasks/tasks.service"; @Module({ providers: [TasksService] diff --git a/src/tasks/tasks/tasks.service.spec.ts b/src/tasks/tasks/tasks.service.spec.ts index 2e5ea0c..7aac9b5 100644 --- a/src/tasks/tasks/tasks.service.spec.ts +++ b/src/tasks/tasks/tasks.service.spec.ts @@ -1,14 +1,22 @@ import { Test, TestingModule } from "@nestjs/testing"; import { TasksService } from "./tasks.service"; -import { PrismaModule } from "@prisma/prisma.module"; +import { PrismaService } from "@ourPrisma/prisma.service"; describe("TasksService", () => { let service: TasksService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [PrismaModule], - providers: [TasksService] + providers: [ + TasksService, + { + provide: PrismaService, + useValue: { + $connect: jest.fn(), + $disconnect: jest.fn() + } + } + ] }).compile(); service = module.get(TasksService); diff --git a/src/tasks/tasks/tasks.service.ts b/src/tasks/tasks/tasks.service.ts index 6676642..49e22f1 100644 --- a/src/tasks/tasks/tasks.service.ts +++ b/src/tasks/tasks/tasks.service.ts @@ -1,6 +1,6 @@ import { Injectable } from "@nestjs/common"; import { Cron, CronExpression } from "@nestjs/schedule"; -import { PrismaService } from "@prisma/prisma.service"; +import { PrismaService } from "@ourPrisma/prisma.service"; @Injectable() export class TasksService { diff --git a/src/util/errors.ts b/src/util/errors.ts new file mode 100644 index 0000000..4746e96 --- /dev/null +++ b/src/util/errors.ts @@ -0,0 +1,5 @@ +// Extracts the message from an unknown error object, providing a fallback message if necessary. +// "Excerr" = exception + error +export function getExcerrMessage(error: unknown, fallback: string = "Unexpected server error"): string { + return error instanceof Error && error.message ? error.message : fallback; +} diff --git a/src/collab/signaling/signal.ts b/src/webrtc/signal.ts similarity index 100% rename from src/collab/signaling/signal.ts rename to src/webrtc/signal.ts diff --git a/src/webrtc/webrtc.dto.ts b/src/webrtc/webrtc.dto.ts new file mode 100644 index 0000000..48337ee --- /dev/null +++ b/src/webrtc/webrtc.dto.ts @@ -0,0 +1,48 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsInt, IsOptional, IsString, IsUrl, ValidateNested } from "class-validator"; + +export class WebRTCOfferPeerICEServerConfig { + @ApiProperty() + @IsUrl() + urls!: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + username?: string | undefined; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + credentials?: string | undefined; +}; + +export class WebRTCOfferPeerOptsConfig { + @ApiProperty({ type: () => [WebRTCOfferPeerICEServerConfig] }) + @ValidateNested() + @Type(() => WebRTCOfferPeerICEServerConfig) + iceServers!: WebRTCOfferPeerICEServerConfig[]; +}; + +export class WebRTCOfferPeerOpts { + @ApiProperty({ type: () => WebRTCOfferPeerOptsConfig }) + @ValidateNested() + @Type(() => WebRTCOfferPeerOptsConfig) + config!: WebRTCOfferPeerOptsConfig; +}; + +export class WebRTCOfferDto { + @ApiProperty() + @IsUrl() + signaling!: string; + + @ApiProperty() + @IsInt() + maxConns!: number; + + @ApiProperty({ type: () => WebRTCOfferPeerOpts }) + @ValidateNested() + @Type(() => WebRTCOfferPeerOpts) + peerOpts!: WebRTCOfferPeerOpts; +}; diff --git a/src/webrtc/webrtc.error.ts b/src/webrtc/webrtc.error.ts new file mode 100644 index 0000000..561d713 --- /dev/null +++ b/src/webrtc/webrtc.error.ts @@ -0,0 +1,13 @@ +export class WebRTCServiceError extends Error { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} + +export class WebRTCServiceOfferError extends WebRTCServiceError { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + } +} diff --git a/src/webrtc/webrtc.module.ts b/src/webrtc/webrtc.module.ts new file mode 100644 index 0000000..1147c6f --- /dev/null +++ b/src/webrtc/webrtc.module.ts @@ -0,0 +1,9 @@ +import { Module } from "@nestjs/common"; +import { WebRTCService } from "./webrtc.service"; +import { AppConfig } from "src/app.config"; + +@Module({ + providers: [WebRTCService, AppConfig], + exports: [WebRTCService] +}) +export class WebRTCModule {} diff --git a/src/webrtc/webrtc.service.ts b/src/webrtc/webrtc.service.ts new file mode 100644 index 0000000..32d2f9a --- /dev/null +++ b/src/webrtc/webrtc.service.ts @@ -0,0 +1,122 @@ +import { Injectable, Logger, OnModuleInit } from "@nestjs/common"; +import { WebRTCOfferDto, WebRTCOfferPeerICEServerConfig } from "./webrtc.dto"; +import { IsArray, IsInt, IsOptional, IsString, IsUrl, validateSync } from "class-validator"; +import { plainToInstance } from "class-transformer"; +import { WebRTCServiceOfferError } from "./webrtc.error"; +import { AppConfig } from "src/app.config"; + +import path from "path"; +import fs from "fs/promises"; + +class WebRTCServiceConfigRelay { + @IsUrl() + url!: string; + @IsString() + @IsOptional() + username?: string; + @IsString() + @IsOptional() + credential?: string; +}; + +class WebRTCServiceConfig { + @IsInt() + maxClients!: number; + @IsArray() + relays!: WebRTCServiceConfigRelay[]; +}; + +// FIXME: If we need to use this elsewhere, move it to a separate file +type IpifyResponse = { + ip: string; +}; + +@Injectable() +export class WebRTCService implements OnModuleInit { + private readonly logger = new Logger(WebRTCService.name); + + private config?: WebRTCServiceConfig; + private publicAddress?: string; + + constructor(private readonly appConfig: AppConfig) {} + + async onModuleInit(): Promise { + await Promise.all([ + this.fetchPublicAddress(), + this.loadConfig(), + ]); + } + + private async fetchPublicAddress(): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + + try { + const res = await fetch("https://api.ipify.org?format=json", { signal: controller.signal }); + const data = await res.json() as IpifyResponse; + this.publicAddress = data.ip; + } catch (err) { + if (err instanceof Error) { + this.logger.error(`Failed to fetch public IP address for WebRTC service: ${err.message}`); + } + this.logger.error(err); + } finally { + clearTimeout(timeout); + } + } + + private async loadConfig(): Promise { + const configPath = path.resolve(process.cwd(), "config", "webrtc.json"); + + try { + const rawFile = await fs.readFile(configPath, "utf-8"); + const parsedRawObject = JSON.parse(rawFile); + + const configInstance = plainToInstance(WebRTCServiceConfig, parsedRawObject); + const configErrors = validateSync(configInstance, { whitelist: true, forbidNonWhitelisted: true }); + + if (configErrors.length > 0) { + this.logger.error(`Invalid WebRTC service config in ${configPath}`); + this.logger.error(JSON.stringify(configErrors)); + return; + } + + this.config = configInstance; + this.logger.log(`WebRTC service config loaded successfully from ${configPath}`); + } catch (err) { + if (err instanceof Error) { + this.logger.error(`Failed to read WebRTC service config from ${configPath}: ${err.message}`); + } + this.logger.error(err); + } + } + + buildOffer(): WebRTCOfferDto { + if (!this.config) { + this.logger.error("Attempt at creating WebRTC offer without a valid initialization, bailing out."); + throw new WebRTCServiceOfferError("WebRTC service is not properly initialized"); + } + + const offerDto = new WebRTCOfferDto(); + + offerDto.signaling = `ws://${this.publicAddress}:${this.appConfig.getPort()}`; + offerDto.maxConns = this.config.maxClients; + offerDto.peerOpts = { + config: { + iceServers: this.config.relays.map( + relay => { + const relayConfig: WebRTCOfferPeerICEServerConfig = { + urls: relay.url, + username: relay.username, + credentials: relay.credential + }; + + return relayConfig; + } + ) + } + }; + + return offerDto; + } +}; diff --git a/swagger.json b/swagger.json new file mode 100644 index 0000000..0f2dda5 --- /dev/null +++ b/swagger.json @@ -0,0 +1,2826 @@ +{ + "openapi": "3.0.0", + "paths": { + "/projects/releases": { + "get": { + "operationId": "ProjectController_getAllReleases", + "parameters": [], + "responses": { + "200": { + "description": "A JSON array of projects with collaborators and creator information", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProjectResponseDto" + } + } + } + } + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Get all released projects", + "tags": [ + "projects" + ] + } + }, + "/projects/releases/{id}": { + "get": { + "operationId": "ProjectController_getRelease", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project release file" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Get project release version", + "tags": [ + "projects" + ] + } + }, + "/projects/releases/{id}/content": { + "get": { + "operationId": "ProjectController_getReleaseContent", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project release file", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Get project release version", + "tags": [ + "projects" + ] + } + }, + "/projects/releases/{id}/content-url": { + "get": { + "operationId": "ProjectController_getReleaseContentUrl", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Signed Edge URL for the release", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignedUrlResponseDto" + } + } + } + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Get signed CDN URL for a release", + "tags": [ + "projects" + ] + } + }, + "/projects": { + "get": { + "operationId": "ProjectController_findAll", + "parameters": [], + "responses": { + "200": { + "description": "A JSON array of projects with collaborators and creator information", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProjectExResponseDto" + } + } + } + } + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Retrieve the list of projects", + "tags": [ + "projects" + ] + }, + "post": { + "operationId": "ProjectController_create", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateProjectDto" + } + } + } + }, + "responses": { + "201": { + "description": "Project created successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectResponseDto" + } + } + } + }, + "400": { + "description": "Bad request – invalid input" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Create a new project", + "tags": [ + "projects" + ] + } + }, + "/projects/{id}": { + "get": { + "operationId": "ProjectController_findOne", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Numeric ID of the project to retrieve", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "Project object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectResponseDto" + } + } + } + }, + "403": { + "description": "Invalid user or project ID" + }, + "404": { + "description": "Project not found" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Retrieve a single project", + "tags": [ + "projects" + ] + }, + "put": { + "operationId": "ProjectController_update", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Numeric ID of the project to update", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateProjectDto" + } + } + } + }, + "responses": { + "200": { + "description": "Updated project object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectResponseDto" + } + } + } + }, + "404": { + "description": "Project not found" + }, + "500": { + "description": "Error updating project" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Update an existing project", + "tags": [ + "projects" + ] + }, + "delete": { + "operationId": "ProjectController_remove", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Numeric ID of the project to delete", + "schema": { + "type": "number" + } + } + ], + "responses": { + "204": { + "description": "Project deleted successfully (no content)" + }, + "404": { + "description": "Project not found" + }, + "500": { + "description": "Error deleting project" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Delete a project", + "tags": [ + "projects" + ] + } + }, + "/projects/{id}/add-collaborator": { + "patch": { + "description": "Add a collaborator to a project by providing either userId, username, or email. At least one must be provided.", + "operationId": "ProjectController_addCollaborator", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Numeric ID of the project to update", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddCollaboratorDto" + }, + "examples": { + "byUserId": { + "summary": "Add by User ID", + "value": { + "userId": 42 + } + }, + "byUsername": { + "summary": "Add by Username", + "value": { + "username": "john_doe" + } + }, + "byEmail": { + "summary": "Add by Email", + "value": { + "email": "john.doe@example.com" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated project object with collaborators", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectExResponseDto" + } + } + } + }, + "400": { + "description": "Bad request - no valid identifier provided" + }, + "404": { + "description": "Project or user not found" + }, + "500": { + "description": "Error Patching project" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Add a new collaborator", + "tags": [ + "projects" + ] + } + }, + "/projects/{id}/remove-collaborator": { + "delete": { + "description": "Remove a collaborator from a project by providing either userId, username, or email. At least one must be provided.", + "operationId": "ProjectController_removeCollaborator", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Numeric ID of the project to update", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoveCollaboratorDto" + }, + "examples": { + "byUserId": { + "summary": "Remove by User ID", + "value": { + "userId": 42 + } + }, + "byUsername": { + "summary": "Remove by Username", + "value": { + "username": "john_doe" + } + }, + "byEmail": { + "summary": "Remove by Email", + "value": { + "email": "john.doe@example.com" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated project object with collaborators", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectExResponseDto" + } + } + } + }, + "400": { + "description": "Bad request - no valid identifier provided" + }, + "403": { + "description": "Forbidden - cannot remove project creator" + }, + "404": { + "description": "Project or user not found" + }, + "500": { + "description": "Error remove collaborator on project" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Remove a collaborator", + "tags": [ + "projects" + ] + } + }, + "/projects/{id}/saveContent": { + "patch": { + "operationId": "ProjectController_saveProjectContent", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary", + "description": "Project file (zip, pdf, png, etc.)" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "File uploaded successfully" + }, + "403": { + "description": "Forbidden" + }, + "422": { + "description": "File validation failed" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Save project's content (Upload)", + "tags": [ + "projects" + ] + } + }, + "/projects/{id}/image": { + "post": { + "operationId": "ProjectController_uploadProjectImage", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary", + "description": "Project image file" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Image uploaded successfully" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Upload project image", + "tags": [ + "projects" + ] + }, + "get": { + "operationId": "ProjectController_getProjectImage", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "Signed cookies and CDN resource URL", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignedCdnResourceDto" + } + } + } + }, + "404": { + "description": "Image not found" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Get signed CDN access to project image", + "tags": [ + "projects" + ] + } + }, + "/projects/{id}/fetchContent": { + "get": { + "operationId": "ProjectController_fetchProjectContent", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "File fetched successfully", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "File not found" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Fetch project's content", + "tags": [ + "projects" + ] + } + }, + "/projects/{id}/saveCheckpoint/{name}": { + "post": { + "operationId": "ProjectController_saveCheckpoint", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "File uploaded successfully" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Save project's checkpoint", + "tags": [ + "projects" + ] + } + }, + "/projects/{id}/deleteCheckpoint/{name}": { + "delete": { + "operationId": "ProjectController_deleteCheckpoint", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "File deleted successfully" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Delete project's checkpoint", + "tags": [ + "projects" + ] + } + }, + "/projects/{id}/publish": { + "post": { + "operationId": "ProjectController_publish", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Project published successfully" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Publish project", + "tags": [ + "projects" + ] + } + }, + "/projects/{id}/unpublish": { + "post": { + "operationId": "ProjectController_unpublish", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "Project unpublished successfully" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Unpublish project", + "tags": [ + "projects" + ] + } + }, + "/projects/{id}/versions": { + "get": { + "operationId": "ProjectController_getVersions", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project versions retrieved successfully" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Get project versions", + "tags": [ + "projects" + ] + } + }, + "/projects/{id}/checkpoints": { + "get": { + "operationId": "ProjectController_getCheckpoints", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project checkpoints retrieved successfully" + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Get project checkpoints", + "tags": [ + "projects" + ] + } + }, + "/projects/{id}/versions/{version}": { + "get": { + "operationId": "ProjectController_getVersion", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "version", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project version retrieved successfully", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Fetch a project version", + "tags": [ + "projects" + ] + } + }, + "/projects/{id}/checkpoints/{checkpoint}": { + "get": { + "operationId": "ProjectController_getCheckpoint", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "checkpoint", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Project checkpoint retrieved successfully", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "403": { + "description": "Forbidden" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Fetch a project checkpoint", + "tags": [ + "projects" + ] + } + }, + "/multiplayer/list-hosts": { + "get": { + "operationId": "MultiplayerController_lookupHosts", + "parameters": [], + "responses": { + "200": { + "description": "A list of available game hosts is returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LookupHostsResponseDto" + } + } + } + }, + "400": { + "description": "Bad request (wrong project ID)." + }, + "500": { + "description": "Unhandled server error." + } + }, + "summary": "List available game hosts/sessions from the user's perspective", + "tags": [ + "multiplayer" + ] + } + }, + "/multiplayer/open-host": { + "post": { + "operationId": "MultiplayerController_openHost", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OpenHostRequestDto" + } + } + } + }, + "responses": { + "200": { + "description": "The game host/session has been successfully opened.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OpenHostResponseDto" + } + } + } + }, + "400": { + "description": "The user is already hosting a game session for this project." + }, + "404": { + "description": "The user or project was not found." + } + }, + "summary": "Open a new game host/session, with the caller being the game host", + "tags": [ + "multiplayer" + ] + } + }, + "/multiplayer/close-host": { + "delete": { + "operationId": "MultiplayerController_closeHost", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CloseHostRequestDto" + } + } + } + }, + "responses": { + "200": { + "description": "The game host/session has been successfully closed." + }, + "404": { + "description": "The user, project, or game session was not found." + } + }, + "summary": "Close an existing game host/session, with the caller being the game host", + "tags": [ + "multiplayer" + ] + } + }, + "/multiplayer/join-host": { + "patch": { + "operationId": "MultiplayerController_joinHost", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JoinHostRequestDto" + } + } + } + }, + "responses": { + "200": { + "description": "Successfully joined the game session.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JoinHostResponseDto" + } + } + } + }, + "400": { + "description": "User is already in the session or is the host." + }, + "404": { + "description": "Game session or user not found." + } + }, + "summary": "Join an existing game host/session as a player", + "tags": [ + "multiplayer" + ] + } + }, + "/multiplayer/leave-host": { + "patch": { + "operationId": "MultiplayerController_leaveHost", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LeaveHostRequestDto" + } + } + } + }, + "responses": { + "200": { + "description": "Successfully left the game session." + }, + "400": { + "description": "User is not part of the session or is the host." + }, + "404": { + "description": "Game session or user not found." + } + }, + "summary": "Leave a game host/session as a player", + "tags": [ + "multiplayer" + ] + } + }, + "/auth/login": { + "post": { + "operationId": "AuthController_login", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginDto" + } + } + } + }, + "responses": { + "201": { + "description": "User registered successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthResponseDto" + } + } + } + }, + "400": { + "description": "Bad request" + }, + "403": { + "description": "Cannot register as an admin" + }, + "409": { + "description": "Email or username already in use" + } + }, + "summary": "Authenticate a user and return an access token", + "tags": [ + "auth" + ] + } + }, + "/auth/register": { + "post": { + "operationId": "AuthController_register", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserDto" + } + } + } + }, + "responses": { + "201": { + "description": "User registered successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthResponseDto" + } + } + } + }, + "400": { + "description": "Bad request" + }, + "403": { + "description": "Cannot register as an admin" + }, + "409": { + "description": "Email already in use" + } + }, + "summary": "Register a new user and return an access token", + "tags": [ + "auth" + ] + } + }, + "/auth/google": { + "post": { + "operationId": "AuthController_loginWithGoogle", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "token": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Login successful with Google", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthResponseDto" + } + } + } + }, + "400": { + "description": "Invalid Google token" + } + }, + "summary": "Authenticate with Google OAuth token", + "tags": [ + "auth" + ] + } + }, + "/auth/refresh": { + "post": { + "operationId": "AuthController_refresh", + "parameters": [], + "responses": { + "201": { + "description": "Access token refreshed successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthResponseDto" + } + } + } + }, + "401": { + "description": "Refresh token missing or invalid" + } + }, + "summary": "Refresh the access token using refresh token cookie", + "tags": [ + "auth" + ] + } + }, + "/auth/logout": { + "post": { + "operationId": "AuthController_logout", + "parameters": [], + "responses": { + "200": { + "description": "Logout successful", + "content": { + "application/json": { + "schema": { + "example": { + "success": true + } + } + } + } + } + }, + "summary": "Remove refresh token cookie", + "tags": [ + "auth" + ] + } + }, + "/users/profile": { + "get": { + "operationId": "UserController_getProfile", + "parameters": [], + "responses": { + "200": { + "description": "Returns the current user profile", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserProfileResponseDto" + } + } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Get current user profile", + "tags": [ + "users" + ] + } + }, + "/users/{id}/profile-picture": { + "post": { + "operationId": "UserController_uploadProfilePicture", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "User ID", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary", + "description": "Profile picture file" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Profile uploaded" + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Upload a user's profile picture", + "tags": [ + "users" + ] + }, + "get": { + "operationId": "UserController_getProfilePicture", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "User ID", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "Signed cookies and CDN resource URL", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignedCdnResourceDto" + } + } + } + }, + "404": { + "description": "Not found" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Get signed CDN access to a user's profile picture", + "tags": [ + "users" + ] + } + }, + "/users": { + "get": { + "operationId": "UserController_findAll", + "parameters": [ + { + "name": "page", + "required": false, + "in": "query", + "description": "Page number", + "schema": { + "example": 1, + "type": "number" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Items per page", + "schema": { + "example": 10, + "type": "number" + } + }, + { + "name": "nickname", + "required": false, + "in": "query", + "description": "Filter by nickname", + "schema": { + "type": "string" + } + }, + { + "name": "email", + "required": false, + "in": "query", + "description": "Filter by email", + "schema": { + "type": "string" + } + }, + { + "name": "sortBy", + "required": false, + "in": "query", + "description": "Sort by field", + "schema": { + "type": "string", + "enum": [ + "id", + "username", + "email", + "createdAt" + ] + } + }, + { + "name": "order", + "required": false, + "in": "query", + "description": "Sort order", + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + } + ], + "responses": { + "200": { + "description": "Returns paginated list of users", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserListResponseDto" + } + } + } + }, + "401": { + "description": "Unauthorized" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Get all users with pagination and filtering", + "tags": [ + "users" + ] + } + }, + "/users/{id}": { + "get": { + "operationId": "UserController_findOne", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "User ID", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "Returns the user", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserSingleResponseDto" + } + } + } + }, + "400": { + "description": "Invalid ID format" + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "User not found" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Get a user by ID", + "tags": [ + "users" + ] + }, + "patch": { + "operationId": "UserController_update", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "User ID", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserDto" + } + } + } + }, + "responses": { + "200": { + "description": "User updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserSingleResponseDto" + } + } + } + }, + "400": { + "description": "Invalid input" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Insufficient permissions" + }, + "404": { + "description": "User not found" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Update a user by ID", + "tags": [ + "users" + ] + }, + "delete": { + "operationId": "UserController_remove", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "User ID", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "User deleted successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "statusCode": { + "type": "number", + "example": 200 + }, + "message": { + "type": "string", + "example": "User deleted successfully" + } + } + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Insufficient permissions" + }, + "404": { + "description": "User not found" + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Delete a user by ID", + "tags": [ + "users" + ] + } + }, + "/work-sessions/join/{id}": { + "post": { + "operationId": "WorkSessionController_join", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Project ID", + "schema": { + "type": "number" + } + } + ], + "responses": { + "201": { + "description": "The work session has been successfully created." + }, + "400": { + "description": "Bad request." + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Join a work session", + "tags": [ + "work-sessions" + ] + } + }, + "/work-sessions/leave/{id}": { + "post": { + "operationId": "WorkSessionController_leave", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Project ID", + "schema": { + "type": "number" + } + } + ], + "responses": { + "204": { + "description": "Successfully left the work session." + }, + "400": { + "description": "Bad request." + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Leave a work session", + "tags": [ + "work-sessions" + ] + } + }, + "/work-sessions/kick/{id}": { + "post": { + "operationId": "WorkSessionController_kick", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Project ID", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KickWorkSessionDto" + } + } + } + }, + "responses": { + "204": { + "description": "Successfully left the work session." + }, + "400": { + "description": "Bad request." + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Kick user from work session", + "tags": [ + "work-sessions" + ] + } + }, + "/work-sessions/info/{id}": { + "get": { + "operationId": "WorkSessionController_getInfo", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "description": "Project ID", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "Work session info retrieved successfully.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FetchWorkSessionDto" + } + } + } + }, + "404": { + "description": "Work session not found." + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Get work session info", + "tags": [ + "work-sessions" + ] + } + } + }, + "info": { + "title": "Naucto API", + "description": "The Naucto API documentation", + "version": "1.0", + "contact": {} + }, + "tags": [], + "servers": [], + "components": { + "securitySchemes": { + "JWT-auth": { + "scheme": "bearer", + "bearerFormat": "JWT", + "type": "http", + "description": "Enter your JWT : Bearer " + } + }, + "schemas": { + "ProjectResponseDto": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1, + "description": "The unique identifier of the project" + }, + "name": { + "type": "string", + "description": "The name of the project", + "example": "MySuperVideoGame" + }, + "shortDesc": { + "type": "string", + "description": "A short description of the project", + "example": "A 2D platformer game with pixel art graphics" + }, + "longDesc": { + "type": "object", + "description": "A detailed description of the project", + "example": "This game features multiple levels, power-ups, and boss fights.", + "nullable": true + }, + "iconUrl": { + "type": "object", + "description": "URL to the project icon", + "example": "https://example.com/icons/MySuperVideoGame.png", + "nullable": true + }, + "status": { + "type": "string", + "description": "The current status of the project", + "enum": [ + "IN_PROGRESS", + "COMPLETED", + "ARCHIVED" + ], + "example": "IN_PROGRESS", + "nullable": true + }, + "monetization": { + "type": "string", + "description": "The monetization strategy for this project", + "enum": [ + "NONE", + "ADS", + "PAID" + ], + "example": "NONE", + "nullable": true + }, + "price": { + "type": "object", + "example": 99.99, + "description": "The price of the project, if applicable", + "nullable": true + }, + "userId": { + "type": "number", + "example": 1, + "description": "The ID of the user who owns this project" + }, + "createdAt": { + "format": "date-time", + "type": "string", + "example": "2023-04-15T12:00:00Z", + "description": "The date and time when the project was created" + }, + "uniquePlayers": { + "type": "number", + "example": 123, + "description": "The number of unique players who have interacted with this project" + }, + "activePlayers": { + "type": "number", + "example": 42, + "description": "The number of currently active players in this project" + }, + "likes": { + "type": "number", + "example": 87, + "description": "The number of likes received by the project" + } + }, + "required": [ + "id", + "name", + "shortDesc", + "longDesc", + "iconUrl", + "status", + "monetization", + "price", + "userId", + "createdAt", + "uniquePlayers", + "activePlayers", + "likes" + ] + }, + "SignedUrlResponseDto": { + "type": "object", + "properties": { + "signedUrl": { + "type": "string", + "example": "https://cdn.example.com/files/project-123?Expires=1640995200&Signature=abc123", + "description": "The signed CloudFront URL for accessing the protected file" + } + }, + "required": [ + "signedUrl" + ] + }, + "UserBasicInfoDto": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1, + "description": "The unique identifier of the user" + }, + "username": { + "type": "string", + "example": "john_doe", + "description": "The username" + }, + "email": { + "type": "string", + "example": "john.doe@example.com", + "description": "The email address" + } + }, + "required": [ + "id", + "username", + "email" + ] + }, + "ProjectExResponseDto": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1, + "description": "The unique identifier of the project" + }, + "name": { + "type": "string", + "description": "The name of the project", + "example": "MySuperVideoGame" + }, + "shortDesc": { + "type": "string", + "description": "A short description of the project", + "example": "A 2D platformer game with pixel art graphics" + }, + "longDesc": { + "type": "object", + "description": "A detailed description of the project", + "example": "This game features multiple levels, power-ups, and boss fights.", + "nullable": true + }, + "iconUrl": { + "type": "object", + "description": "URL to the project icon", + "example": "https://example.com/icons/MySuperVideoGame.png", + "nullable": true + }, + "status": { + "type": "string", + "description": "The current status of the project", + "enum": [ + "IN_PROGRESS", + "COMPLETED", + "ARCHIVED" + ], + "example": "IN_PROGRESS", + "nullable": true + }, + "monetization": { + "type": "string", + "description": "The monetization strategy for this project", + "enum": [ + "NONE", + "ADS", + "PAID" + ], + "example": "NONE", + "nullable": true + }, + "price": { + "type": "object", + "example": 99.99, + "description": "The price of the project, if applicable", + "nullable": true + }, + "userId": { + "type": "number", + "example": 1, + "description": "The ID of the user who owns this project" + }, + "createdAt": { + "format": "date-time", + "type": "string", + "example": "2023-04-15T12:00:00Z", + "description": "The date and time when the project was created" + }, + "uniquePlayers": { + "type": "number", + "example": 123, + "description": "The number of unique players who have interacted with this project" + }, + "activePlayers": { + "type": "number", + "example": 42, + "description": "The number of currently active players in this project" + }, + "likes": { + "type": "number", + "example": 87, + "description": "The number of likes received by the project" + }, + "collaborators": { + "description": "The users collaborating on this project", + "type": "array", + "items": { + "$ref": "#/components/schemas/UserBasicInfoDto" + } + }, + "creator": { + "description": "The creator of this project", + "allOf": [ + { + "$ref": "#/components/schemas/UserBasicInfoDto" + } + ] + } + }, + "required": [ + "id", + "name", + "shortDesc", + "longDesc", + "iconUrl", + "status", + "monetization", + "price", + "userId", + "createdAt", + "uniquePlayers", + "activePlayers", + "likes", + "collaborators", + "creator" + ] + }, + "CreateProjectDto": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the project", + "example": "MySuperVideoGame" + }, + "shortDesc": { + "type": "string", + "description": "A short description of the project", + "example": "A 2D platformer game with pixel art graphics" + }, + "iconUrl": { + "type": "string", + "description": "URL to the project icon", + "example": "https://example.com/icons/MySuperVideoGame.png" + } + }, + "required": [ + "name", + "shortDesc" + ] + }, + "UpdateProjectDto": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the project", + "example": "MySuperVideoGame" + }, + "shortDesc": { + "type": "string", + "description": "A short description of the project", + "example": "A 2D platformer game with pixel art graphics" + }, + "longDesc": { + "type": "object", + "description": "A detailed description of the project", + "example": "This game features multiple levels, power-ups, and boss fights." + }, + "iconUrl": { + "type": "string", + "description": "URL to the project icon", + "example": "https://example.com/icons/MySuperVideoGame.png" + }, + "status": { + "type": "string", + "description": "Project status", + "enum": [ + "IN_PROGRESS", + "COMPLETED", + "ARCHIVED" + ], + "default": "IN_PROGRESS" + }, + "monetization": { + "type": "string", + "description": "Monetization type", + "enum": [ + "NONE", + "ADS", + "PAID" + ], + "default": "NONE" + }, + "price": { + "type": "number", + "description": "The price of the project", + "example": 99.99 + } + }, + "required": [ + "name", + "shortDesc" + ] + }, + "AddCollaboratorDto": { + "type": "object", + "properties": { + "userId": { + "type": "number", + "description": "User ID of the user to add as collaborator", + "example": 42 + }, + "username": { + "type": "string", + "description": "Username of the user to add as collaborator", + "example": "john_doe" + }, + "email": { + "type": "string", + "description": "Email of the user to add as collaborator", + "example": "john.doe@example.com" + } + } + }, + "RemoveCollaboratorDto": { + "type": "object", + "properties": { + "userId": { + "type": "number", + "description": "User ID of the user to remove as collaborator", + "example": 42 + }, + "username": { + "type": "string", + "description": "Username of the user to remove as collaborator", + "example": "john_doe" + }, + "email": { + "type": "string", + "description": "Email of the user to remove as collaborator", + "example": "john.doe@example.com" + } + } + }, + "SignedCdnResourceDto": { + "type": "object", + "properties": { + "resourceUrl": { + "type": "string", + "example": "https://cdn.example.com/projects/42/image", + "description": "The CDN URL for the resource (requires signed cookies)" + }, + "cookies": { + "type": "object", + "description": "Signed Edge cookies (also set as HTTP-only cookies)", + "example": { + "CloudFront-Expires": "1735660800", + "CloudFront-Signature": "base64-signature", + "CloudFront-Key-Pair-Id": "K1234567890", + "CloudFront-Policy": "base64-policy" + } + } + }, + "required": [ + "resourceUrl", + "cookies" + ] + }, + "LookupHostsResponseDtoHost": { + "type": "object", + "properties": { + "sessionUuid": { + "type": "string" + }, + "sessionVisibility": { + "type": "string", + "enum": [ + "PUBLIC", + "FRIENDS_ONLY", + "PRIVATE" + ] + }, + "playerCount": { + "type": "number" + } + }, + "required": [ + "sessionUuid", + "sessionVisibility", + "playerCount" + ] + }, + "LookupHostsResponseDto": { + "type": "object", + "properties": { + "hosts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LookupHostsResponseDtoHost" + } + } + }, + "required": [ + "hosts" + ] + }, + "OpenHostRequestDto": { + "type": "object", + "properties": { + "projectId": { + "type": "number" + }, + "visibility": { + "type": "string", + "enum": [ + "PUBLIC", + "FRIENDS_ONLY", + "PRIVATE" + ] + } + }, + "required": [ + "projectId", + "visibility" + ] + }, + "WebRTCOfferPeerICEServerConfig": { + "type": "object", + "properties": { + "urls": { + "type": "string" + }, + "username": { + "type": "object" + }, + "credentials": { + "type": "object" + } + }, + "required": [ + "urls" + ] + }, + "WebRTCOfferPeerOptsConfig": { + "type": "object", + "properties": { + "iceServers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WebRTCOfferPeerICEServerConfig" + } + } + }, + "required": [ + "iceServers" + ] + }, + "WebRTCOfferPeerOpts": { + "type": "object", + "properties": { + "config": { + "$ref": "#/components/schemas/WebRTCOfferPeerOptsConfig" + } + }, + "required": [ + "config" + ] + }, + "WebRTCOfferDto": { + "type": "object", + "properties": { + "signaling": { + "type": "string" + }, + "maxConns": { + "type": "number" + }, + "peerOpts": { + "$ref": "#/components/schemas/WebRTCOfferPeerOpts" + } + }, + "required": [ + "signaling", + "maxConns", + "peerOpts" + ] + }, + "OpenHostResponseDto": { + "type": "object", + "properties": { + "sessionUuid": { + "type": "string" + }, + "webrtcConfig": { + "$ref": "#/components/schemas/WebRTCOfferDto" + } + }, + "required": [ + "sessionUuid", + "webrtcConfig" + ] + }, + "CloseHostRequestDto": { + "type": "object", + "properties": { + "projectId": { + "type": "number", + "description": "ID of the project whose session to close" + } + }, + "required": [ + "projectId" + ] + }, + "JoinHostRequestDto": { + "type": "object", + "properties": { + "sessionUuid": { + "type": "string" + } + }, + "required": [ + "sessionUuid" + ] + }, + "JoinHostResponseDto": { + "type": "object", + "properties": { + "webrtcConfig": { + "$ref": "#/components/schemas/WebRTCOfferDto" + } + }, + "required": [ + "webrtcConfig" + ] + }, + "LeaveHostRequestDto": { + "type": "object", + "properties": { + "sessionUuid": { + "type": "string" + } + }, + "required": [ + "sessionUuid" + ] + }, + "LoginDto": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "User email address", + "example": "user@example.com" + }, + "password": { + "type": "string", + "description": "User password", + "example": "password123", + "minLength": 6 + } + }, + "required": [ + "email", + "password" + ] + }, + "AuthResponseDto": { + "type": "object", + "properties": { + "access_token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } + }, + "required": [ + "access_token" + ] + }, + "CreateUserDto": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "User email address", + "example": "user@example.com" + }, + "username": { + "type": "string", + "description": "User username", + "example": "xX_DarkGamer_Xx" + }, + "nickname": { + "type": "string", + "description": "User nick name", + "example": "JohnDoe" + }, + "password": { + "type": "string", + "description": "User password", + "example": "password123" + } + }, + "required": [ + "email", + "username", + "password" + ] + }, + "UserRoleDto": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "Role ID", + "example": 1 + }, + "name": { + "type": "string", + "description": "Role name", + "example": "Admin" + } + }, + "required": [ + "id", + "name" + ] + }, + "UserResponseDto": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "User ID", + "example": 1 + }, + "email": { + "type": "string", + "description": "User email address", + "example": "user@example.com" + }, + "username": { + "type": "string", + "description": "Username", + "example": "xX_DarkGamer_Xx" + }, + "nickname": { + "type": "string", + "description": "User nickname", + "example": "JohnDoe", + "nullable": true + }, + "roles": { + "description": "User roles", + "type": "array", + "items": { + "$ref": "#/components/schemas/UserRoleDto" + } + }, + "createdAt": { + "format": "date-time", + "type": "string", + "description": "User creation date", + "example": "2023-01-01T00:00:00.000Z" + }, + "updatedAt": { + "format": "date-time", + "type": "string", + "description": "User last update date", + "example": "2023-01-01T00:00:00.000Z" + } + }, + "required": [ + "id", + "email", + "username", + "nickname", + "createdAt", + "updatedAt" + ] + }, + "PaginationMetaDto": { + "type": "object", + "properties": { + "page": { + "type": "number", + "description": "Current page number", + "example": 1 + }, + "limit": { + "type": "number", + "description": "Number of items per page", + "example": 10 + }, + "total": { + "type": "number", + "description": "Total number of items", + "example": 100 + }, + "totalPages": { + "type": "number", + "description": "Total number of pages", + "example": 10 + } + }, + "required": [ + "page", + "limit", + "total", + "totalPages" + ] + }, + "UserListResponseDto": { + "type": "object", + "properties": { + "statusCode": { + "type": "number", + "description": "HTTP status code", + "example": 200 + }, + "message": { + "type": "string", + "description": "Response message", + "example": "Users retrieved successfully" + }, + "data": { + "description": "List of users", + "type": "array", + "items": { + "$ref": "#/components/schemas/UserResponseDto" + } + }, + "meta": { + "description": "Pagination metadata", + "allOf": [ + { + "$ref": "#/components/schemas/PaginationMetaDto" + } + ] + } + }, + "required": [ + "statusCode", + "message", + "data", + "meta" + ] + }, + "UserSingleResponseDto": { + "type": "object", + "properties": { + "statusCode": { + "type": "number", + "description": "HTTP status code", + "example": 200 + }, + "message": { + "type": "string", + "description": "Response message", + "example": "User retrieved successfully" + }, + "data": { + "description": "User data", + "allOf": [ + { + "$ref": "#/components/schemas/UserResponseDto" + } + ] + } + }, + "required": [ + "statusCode", + "message", + "data" + ] + }, + "UserProfileResponseDto": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "User ID", + "example": 1 + }, + "email": { + "type": "string", + "description": "User email address", + "example": "user@example.com" + }, + "username": { + "type": "string", + "description": "Username", + "example": "xX_DarkGamer_Xx" + }, + "nickname": { + "type": "string", + "description": "User nickname", + "example": "JohnDoe", + "nullable": true + }, + "roles": { + "description": "User roles", + "type": "array", + "items": { + "$ref": "#/components/schemas/UserRoleDto" + } + }, + "createdAt": { + "format": "date-time", + "type": "string", + "description": "User creation date", + "example": "2023-01-01T00:00:00.000Z" + }, + "updatedAt": { + "format": "date-time", + "type": "string", + "description": "User last update date", + "example": "2023-01-01T00:00:00.000Z" + }, + "message": { + "type": "string", + "description": "User profile message", + "example": "Profile retrieved successfully" + } + }, + "required": [ + "id", + "email", + "username", + "nickname", + "createdAt", + "updatedAt", + "message" + ] + }, + "UpdateUserDto": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "User email address", + "example": "user@example.com" + }, + "username": { + "type": "string", + "description": "User username", + "example": "xX_DarkGamer_Xx" + }, + "nickname": { + "type": "string", + "description": "User nick name", + "example": "JohnDoe" + }, + "password": { + "type": "string", + "description": "User password", + "example": "password123" + } + } + }, + "KickWorkSessionDto": { + "type": "object", + "properties": { + "userId": { + "type": "number", + "description": "The ID of the user participating in the work session", + "example": 1 + } + }, + "required": [ + "userId" + ] + }, + "FetchWorkSessionDto": { + "type": "object", + "properties": { + "users": { + "description": "The ID of the user participating in the work session", + "example": [ + 1, + 2, + 3 + ], + "type": "array", + "items": { + "type": "string" + } + }, + "host": { + "type": "number", + "description": "The ID of the session's host", + "example": 1 + }, + "project": { + "type": "number", + "description": "The ID of the project this work session belongs to", + "example": 1 + }, + "startedAt": { + "format": "date-time", + "type": "string", + "description": "The date and time when the work session started", + "example": "2023-04-15T12:00:00Z" + }, + "roomId": { + "type": "string", + "description": "The ID of the room for this work session", + "example": "room-12345" + }, + "roomPassword": { + "type": "string", + "description": "The password for the room of this work session", + "example": "password123" + } + }, + "required": [ + "users", + "host", + "project", + "startedAt", + "roomId", + "roomPassword" + ] + } + } + } +} \ No newline at end of file diff --git a/tool/generate-swagger.ts b/tool/generate-swagger.ts new file mode 100644 index 0000000..b924ebf --- /dev/null +++ b/tool/generate-swagger.ts @@ -0,0 +1,64 @@ +/* eslint-disable no-console */ +// Fine for this file, not part of the main project + +// This is only necessary for services that explicitly rely on ConfigService +const stubEnv: NodeJS.ProcessEnv = { + DATABASE_URL: "postgresql://stub:stub@localhost:5432/stub", + JWT_SECRET: "stub-secret-for-swagger-generation-only", + JWT_EXPIRES_IN: "7d", + JWT_REFRESH_EXPIRES_IN: "30d", + NODE_ENV: "development", + FRONTEND_URL: "http://localhost:3001", + GOOGLE_CLIENT_ID: "stub-google-client-id", + S3_ENDPOINT: "http://localhost:9000", + S3_REGION: "stub-region", + S3_ACCESS_KEY_ID: "stub-key", + S3_SECRET_ACCESS_KEY: "stub", + S3_BUCKET_NAME: "stub-bucket", + CDN_URL: "stub.cloudfront.net", + CLOUDFRONT_KEY_PAIR_ID: "stub-key-pair-id", + CLOUDFRONT_PRIVATE_KEY_PATH: "/dev/null", +}; + +Object.assign(process.env, stubEnv); + +(async () => { + try { + console.log("[swag-gen] Starting swagger generation..."); + + console.log("[swag-gen] Importing @nestjs/core..."); + const { NestFactory } = await import("@nestjs/core"); + console.log("[swag-gen] Imported @nestjs/core."); + + console.log("[swag-gen] Importing SwaggerAppModule..."); + const { SwaggerAppModule: AppModule } = await import("../src/swagger.app.module"); + console.log("[swag-gen] Imported SwaggerAppModule."); + + console.log("[swag-gen] Importing buildSwaggerDocument..."); + const { buildSwaggerDocument } = await import("../src/swagger"); + console.log("[swag-gen] Imported buildSwaggerDocument."); + + const fs = await import("fs"); + + console.log("[swag-gen] Creating NestJS application..."); + const app = await NestFactory.create(AppModule, { logger: ["error", "warn", "log", "debug", "verbose"] }); + console.log("[swag-gen] NestJS application created."); + + console.log("[swag-gen] Building swagger document..."); + const document = buildSwaggerDocument(app); + console.log("[swag-gen] Swagger document built."); + + console.log("[swag-gen] Writing swagger.json..."); + fs.writeFileSync("swagger.json", JSON.stringify(document, null, 2)); + console.log("[swag-gen] swagger.json written successfully."); + + console.log("[swag-gen] Closing NestJS application..."); + await app.close(); + console.log("[swag-gen] Done."); + } catch (err) { + console.error("[swag-gen] Failed to generate swagger.json", + err instanceof Error ? err.stack : String(err)); + process.exit(1); + } +})(); + diff --git a/tsconfig.json b/tsconfig.json index bafab82..472e701 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,13 +32,15 @@ "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ "paths": { "src/*": ["./*"], - "@prisma/*": ["prisma/*"], + "@ourPrisma/*": ["prisma/*"], "@auth/*": ["auth/*"], - "@projects/*": ["routes/project/*"], + "@project/*": ["routes/project/*"], + "@multiplayer/*": ["routes/multiplayer/*"], "@user/*": ["routes/user/*"], "@work-session/*": ["routes/work-session/*"], "@s3/*": ["routes/s3/*"], - "@common/*": ["common/*"] + "@common/*": ["common/*"], + "@webrtc/*": ["webrtc/*"] }, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */